Repository navigation
build(deps): bump the tauri-js group across 1 directory with 2 updates - #111
Merged
fstubner merged 1 commit intoAug 15, 2026
Merged
Conversation
dependabot
Bot
force-pushed
the
dependabot/npm_and_yarn/apps/netscli-gui/tauri-js-00502e25c4
branch
3 times, most recently
from
July 13, 2026 07:10
43905d4 to
2e865f3
Compare
dependabot
Bot
force-pushed
the
dependabot/npm_and_yarn/apps/netscli-gui/tauri-js-00502e25c4
branch
2 times, most recently
from
July 27, 2026 07:09
fc9de0a to
7978a02
Compare
Owner
|
@dependabot rebase |
dependabot
Bot
force-pushed
the
dependabot/npm_and_yarn/apps/netscli-gui/tauri-js-00502e25c4
branch
from
August 14, 2026 19:28
7978a02 to
e9cfeb0
Compare
fstubner
added a commit
that referenced
this pull request
Aug 14, 2026
Widening the gate to all 14 routes immediately found a real bug on the newly covered /docs/packet-capture/: `code` and `a` inside a Starlight aside inherited the global mint accent, which is tuned for contrast against the #111 page background rather than a callout's own coloured one. On the amber caution variant that measured 1.53:1 for code and 3.53:1 for links, against a 4.5:1 AA threshold. They now use --sl-color-asides-text-accent, which Starlight sets per variant, so note/tip/danger stay correct too. Measured after: 6.77:1 and 8.34:1. The rule needs its own file loading last, rather than sitting in 05-docs-surfaces.css where it belongs, because earlier files declare `.sl-markdown-content a` and `:not(pre) > code` with !important and silently override it. That is the B-23 tax in miniature: placement is load-order dependent and the cascade cannot be reasoned about locally. Two harness flaws found while fixing it, both of which made a green local run meaningless: - ensurePreview() reused whatever answered on port 4322. A long-running `astro dev` server also listens there and serves from source with different CSS processing, so the gate scanned something other than dist/ -- it passed locally while CI failed. It now refuses to reuse an existing server unless A11Y_REUSE_SERVER=1 says it really is the build. - Without `headless`, axe launches a headed Chrome that takes its colour scheme from the OS and ignores --force-dark-mode, so both passes rendered the same theme and one was never tested. Verified the gate genuinely catches this now: with the CSS reverted it reports the 4 violations locally, matching CI exactly. It did not before.
fstubner
added a commit
that referenced
this pull request
Aug 15, 2026
… B-26, B-38) (#174) * Fix site sitemap duplication and widen the a11y gate to every route B-24: three sitemaps shipped -- a hand-written public/sitemap.xml passed through to dist/ with all 13 lastmod values frozen at 2026-07-02, plus the sitemap-index.xml and sitemap-0.xml that @astrojs/sitemap generates on every build. robots.txt pointed at the stale hand-written one, so the always-accurate generated index was never referenced. Deleted the hand-written copy and repointed robots.txt. B-26: the a11y gate hard-coded 7 of 14 routes. The seven it missed -- /docs/cli/, /mcp/, /tui/, /operations/, /packet-capture/, /core-library/ and /result-model/ -- hold the heaviest table markup, which docs-header.ts then wraps at runtime on every docs page. Routes are now discovered from the build output, so pages cannot be added without being covered. Falls back to the old list if dist/ is missing, because an empty route list would otherwise pass silently. Ran it: all 14 routes are clean in both light and dark themes. The seven newly covered pages had no violations -- they are simply gated now. B-38: three whole-document MutationObservers plus scroll/resize listeners were never disconnected. Today that only costs memory, since the module runs once per full page load. It becomes a real leak the moment view transitions are enabled, because initDocsHeader re-runs on astro:page-load and would stack a second full set on the first, each with rAF callbacks that themselves mutate the DOM. Teardown is now registered against astro:before-swap and init is re-entrant, so enabling <ClientRouter /> later is a one-line change rather than a debugging session. astro check: 42 files, 0 errors. * Move the docs-header teardown registry into its own module The teardown added for B-38 pushed docs-header.ts to 379 lines, past its 360 transition cap. I did not run the size gate locally before pushing -- CI caught it. The registry is a coherent unit on its own, so it moves out rather than the file being trimmed to fit. * Fix callout contrast and stop the a11y gate scanning the wrong server Widening the gate to all 14 routes immediately found a real bug on the newly covered /docs/packet-capture/: `code` and `a` inside a Starlight aside inherited the global mint accent, which is tuned for contrast against the #111 page background rather than a callout's own coloured one. On the amber caution variant that measured 1.53:1 for code and 3.53:1 for links, against a 4.5:1 AA threshold. They now use --sl-color-asides-text-accent, which Starlight sets per variant, so note/tip/danger stay correct too. Measured after: 6.77:1 and 8.34:1. The rule needs its own file loading last, rather than sitting in 05-docs-surfaces.css where it belongs, because earlier files declare `.sl-markdown-content a` and `:not(pre) > code` with !important and silently override it. That is the B-23 tax in miniature: placement is load-order dependent and the cascade cannot be reasoned about locally. Two harness flaws found while fixing it, both of which made a green local run meaningless: - ensurePreview() reused whatever answered on port 4322. A long-running `astro dev` server also listens there and serves from source with different CSS processing, so the gate scanned something other than dist/ -- it passed locally while CI failed. It now refuses to reuse an existing server unless A11Y_REUSE_SERVER=1 says it really is the build. - Without `headless`, axe launches a headed Chrome that takes its colour scheme from the OS and ignores --force-dark-mode, so both passes rendered the same theme and one was never tested. Verified the gate genuinely catches this now: with the CSS reverted it reports the 4 violations locally, matching CI exactly. It did not before. * Beat the theme-scoped !important rules that were overriding the fix The callout rule was still losing in the light theme. Earlier files declare html[data-theme="light"] .sl-markdown-content :not(pre) > code with !important -- specificity (0,2,3) -- while the obvious selector here is only (0,2,2), so it lost despite loading later. Repeating .starlight-aside takes this to (0,3,2), which outranks it on class count without depending on data-theme being present. Also switched from --sl-color-asides-text-accent to inheriting the aside's own text colour. The accent measured only 4.53:1 in light -- passing, but with so little margin that a rounding difference between axe versions could flip it. Inheriting gives 14.96:1 light and 9.18:1 dark, and cannot drift when a token changes. Measured in both themes by forcing data-theme directly rather than trusting Chrome flags, after two local runs had already misled me.
Owner
|
@dependabot rebase |
Bumps the tauri-js group with 2 updates in the /apps/netscli-gui directory: [@tauri-apps/api](https://github.com/tauri-apps/tauri) and [@tauri-apps/cli](https://github.com/tauri-apps/tauri). Updates `@tauri-apps/api` from 2.11.0 to 2.11.1 - [Release notes](https://github.com/tauri-apps/tauri/releases) - [Commits](https://github.com/tauri-apps/tauri/compare/@tauri-apps/api-v2.11.0...@tauri-apps/api-v2.11.1) Updates `@tauri-apps/cli` from 2.11.2 to 2.11.4 - [Release notes](https://github.com/tauri-apps/tauri/releases) - [Commits](https://github.com/tauri-apps/tauri/compare/@tauri-apps/cli-v2.11.2...@tauri-apps/cli-v2.11.4) --- updated-dependencies: - dependency-name: "@tauri-apps/api" dependency-version: 2.11.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: tauri-js - dependency-name: "@tauri-apps/cli" dependency-version: 2.11.3 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: tauri-js ... Signed-off-by: dependabot[bot] <support@github.com>
dependabot
Bot
force-pushed
the
dependabot/npm_and_yarn/apps/netscli-gui/tauri-js-00502e25c4
branch
from
August 15, 2026 04:03
e9cfeb0 to
b9076b5
Compare
fstubner
deleted the
dependabot/npm_and_yarn/apps/netscli-gui/tauri-js-00502e25c4
branch
August 15, 2026 04:19
fstubner
added a commit
that referenced
this pull request
Aug 17, 2026
) Two findings from the acceptance pass. 1. Arrow keys did nothing in the docs search. Pagefind's default UI leaves focus in the input and ignores ArrowDown/ArrowUp, so results were reachable only by tabbing. Tab and Enter did work, so search was usable -- but the down arrow is the key most people try first, and it appeared to do nothing at all. Focus moves for real rather than painting a highlight and tracking `aria-activedescendant`. The results are anchors, so focus gets Enter, middle-click and screen-reader announcement for free, and there is no second notion of "selected" to keep in sync with the rendered list. Results are re-queried on every keypress, because Pagefind re-renders the list as the query changes and any cached node would be stale. Down from the input enters the list; up from the first result returns to the input, so the query is one keypress away rather than Shift+Tab. Up from the input wraps to the last result. Verified on the deployed build: start -> INPUT Down -> result: Packet capture Down -> result: Packet capture Down -> result: CLI Capture Up -> result: Packet capture Up -> result: Packet capture Up -> INPUT 2. The mobile docs menu was see-through, and dark in light theme. The panel background was a hard-coded `rgba(27, 32, 40, 0.96)`, which failed twice over. The 4% let the page read through the open menu, with body text crossing the menu labels. And because the colour was hard-coded rather than tokenised, the panel stayed dark in light theme -- a dark drawer over a #fbfbfb page, with white showing through it. Now `var(--sl-color-bg-sidebar)`: #111 in dark, #fbfbfb in light, opaque in both. Measured after the fix, and the panel follows the theme. The same report noted the first sidebar group label sitting behind the breadcrumb bar. That does not reproduce now -- it was the breadcrumb showing *through* the translucent panel, so the opacity fix resolved it. Confirmed with elementFromPoint: the panel's own content is topmost across its full area. astro check clean, build passes, a11y 14 routes x both themes 0 violations, file-size guard clean.
fstubner
added a commit
that referenced
this pull request
Aug 31, 2026
Four review items, and the light theme underneath two of them.
THE LIGHT THEME
Whole sections of the landing stack were still painting the dark theme's
values. Measured, in the light theme:
- "Get started" heading: 1.03:1. `section:nth-of-type(even)` painted an
opaque #151515 band under it.
- The 404 page's heading: 1.06:1. Same cause, `#111` on that page.
- Every copy button: an SVG stroked `rgba(255,255,255,.34)` in BOTH themes,
so white on white -- each one rendered as an empty square.
- The "Desktop app" button: fill hard-coded #191b1e, so the one primary
button on the page stayed dark charcoal on white, and its border gradient
ended on the dark theme's cyan.
- The same button's gradient LABEL had a stop failing in each theme at
opposite ends: #005a1e at 2.23:1 on the dark page, #1edcff at 1.59:1 on
the light one. Half the word was near-invisible either way, and had been
since the button was written.
The first two are the same bug twice: the base layer of a multi-line
`background:` shorthand, sitting alone on its own line. The pass that mapped
every other opaque background onto a surface token matched a hex on the same
line as its property, so it never saw either. A search for that exact shape
finds no others.
WHY NOTHING CAUGHT IT
axe-core files text it cannot resolve a background for -- over a gradient, or
under an ancestor with a pseudo-element -- under `incomplete`, and
`@axe-core/cli --exit` fails on `violations` only. Run directly against the
landing page: 0 violations, 74 passes, 48 incomplete. The 1.03:1 heading was
in the incomplete pile. Gradient-filled text also sets
`-webkit-text-fill-color: transparent`, so axe reads a transparent foreground
and skips the element entirely.
scripts/contrast-sweep.mjs closes that. It composites the ancestor stack
itself -- walking up through translucent layers to an opaque one, as a browser
paints it -- and checks every rendered text node on all 14 routes in both
themes. Sabotage-checked against the restored #151515: axe reports "no
accessibility violations in either theme" and exits 0; the sweep exits 1 and
names them. It runs alongside axe, not instead of it.
The three gradient stops are now tokens and are checked by
scripts/design-tokens.mjs, which grew 204 -> 228 pairs. Sabotage-checked
there too: restoring #005a1e fails at 2.23:1.
THE FOUR REVIEW ITEMS
The contents rail did not stick. Starlight fixes the whole rail, so the
margin that started it below the title band pinned it 133px below the header
for the whole page -- measured still at y=193 with the band 1378px off
screen. The rail goes back into normal flow and the panel is sticky, so it
starts under the band and rises to the header. Two things had to change with
it: the offset became padding on the rail, because as a margin it collapsed
out and the rail overran the page by exactly 133px; and `.right-sidebar` lost
`overflow-x: hidden`, which quietly computes overflow-y to `auto` and made
the rail a scroll container that captured the sticky.
Next/Previous drew label and title at gray-1 against white -- a step nobody
can see. The title keeps white, the label drops to the body step, and the
hover rule that collapsed them back together no longer applies to the title.
The hero's two install commands swapped rows: the script one-liner is in the
wide slot beside the button, the package-manager command below. They are
keyed by route now rather than by rank, because Linux had them the other way
round and the wide row was getting whichever the table happened to call
primary.
Content was NOT reviewed. Spot-checked the four safety limits on the docs
Overview against crates/netscli-core: /16, 4096 ports, 500 ms, 256
concurrency all match the constants. That is four claims on one page, not a
content review.
Gates: axe 0 violations across 14 pages in both themes, contrast sweep clean
on 14 routes in both themes, 228 contrast pairs >= 4.5:1, dead CSS 14/14,
astro check, changelog dates, file size, app doc links, wordmark inset.
Baseline re-recorded.
Known and not fixed: the hero's script command is 624px of text in a 351px
slot, so it is still truncated -- the swap widened what shows, it did not fix
it. A short URL on netscli.com would; that is a decision about where the
install script is served from.
fstubner
added a commit
that referenced
this pull request
Sep 1, 2026
netscli-wordmark.png is a dark-theme asset and there is only one of it. The gradient runs green -> cyan and the cyan half is picked for a near-black bar. Measured across its 34,754 opaque pixels: mean ink rgb(12,167,132) is 6.18:1 on #111 and 3.05:1 on white, and the cyan end rgb(27,213,235) is 1.79:1 on white -- the right side of the logo barely separates from a light page. The colours that read best on #111 are exactly the ones that fail hardest on white. No gate caught this and none should have: WCAG exempts logotypes from the contrast rules, so axe and scripts/contrast-sweep.mjs are both correctly silent. It is a visual call, reported by eye. brightness(.62), computed with the same filter maths browsers apply: mean ink becomes rgb(8,103,82) at 6.84:1, and the weakest pixel goes from 1.79:1 to 4.43:1. A uniform multiply rather than a second asset, and that is a real tradeoff -- it shifts the cyan towards teal instead of re-picking the gradient for a light background. A light-theme PNG would be more faithful and would also be a second file to keep in step with this one and with scripts/wordmark-inset.mjs, which derives the inset ratio from the asset's own alpha. One declaration and reversible until the hue shift bothers someone. Carried on --netscli-mark-filter in tokens.css because the landing bar and the docs bar are separate components with their own scoped styles, and this is one decision rather than two. Verified resolving as brightness(0.62) on both bars in the light theme and none in dark. Nav.astro's `.mark` also loses `transition: filter 200ms` and its `.mark:hover{filter:brightness(1)}`. Both were no-ops -- base and hover were each brightness(1), so the transition had nothing to animate -- and left in place the hover rule would have undone the darkening the moment the pointer touched the logo. astro check clean, wordmark inset 0.0778 measured against declared, dead CSS 14/14.
fstubner
added a commit
that referenced
this pull request
Sep 1, 2026
netscli-wordmark.png is a dark-theme asset and there is only one of it. The gradient runs green -> cyan and the cyan half is picked for a near-black bar. Measured across its 34,754 opaque pixels: mean ink rgb(12,167,132) is 6.18:1 on #111 and 3.05:1 on white, and the cyan end rgb(27,213,235) is 1.79:1 on white -- the right side of the logo barely separates from a light page. The colours that read best on #111 are exactly the ones that fail hardest on white. No gate caught this and none should have: WCAG exempts logotypes from the contrast rules, so axe and scripts/contrast-sweep.mjs are both correctly silent. It is a visual call, reported by eye. brightness(.62), computed with the same filter maths browsers apply: mean ink becomes rgb(8,103,82) at 6.84:1, and the weakest pixel goes from 1.79:1 to 4.43:1. A uniform multiply rather than a second asset, and that is a real tradeoff -- it shifts the cyan towards teal instead of re-picking the gradient for a light background. A light-theme PNG would be more faithful and would also be a second file to keep in step with this one and with scripts/wordmark-inset.mjs, which derives the inset ratio from the asset's own alpha. One declaration and reversible until the hue shift bothers someone. Carried on --netscli-mark-filter in tokens.css because the landing bar and the docs bar are separate components with their own scoped styles, and this is one decision rather than two. Verified resolving as brightness(0.62) on both bars in the light theme and none in dark. Nav.astro's `.mark` also loses `transition: filter 200ms` and its `.mark:hover{filter:brightness(1)}`. Both were no-ops -- base and hover were each brightness(1), so the transition had nothing to animate -- and left in place the hover rule would have undone the darkening the moment the pointer touched the logo. astro check clean, wordmark inset 0.0778 measured against declared, dead CSS 14/14.
fstubner
added a commit
that referenced
this pull request
Sep 1, 2026
The star count, download count and version are the only third-party evidence above the fold, and the line was the smallest thing on the page: 12px at rgb(89,100,120), directly under a 17px subhead. Legible at 5.77:1, but reading as a footnote. 13px and the body ink step: measured 7.71:1 light and 11.45:1 dark. The separators keep the muted step so the line still reads as one group rather than four competing items. The `.sep` colour was a hard-coded #555 -- a dark-theme grey the light theme had no say over, and the one part of this line that would not have moved with the rest. On the token now, which also fixes it in dark, where #555 on #111 measured 2.6:1 against 6.26:1 now. Contrast sweep clean on 14 routes both themes, axe clean both themes, dead CSS 14/14.
fstubner
added a commit
that referenced
this pull request
Sep 2, 2026
Three fixes to the light homepage, all from measuring the page rather than looking at it. CENTRE. Every text block in the surfaces section is 118px and every visual beside it is 218-296px, and the grid was `align-items: start` -- so each row had 100-178px of empty column under its paragraph, about 560px of void in a 1608px section. That is what made the tour read as ragged rather than as four matched rows. `center` puts each text block on its visual's centre line; measured offset is now 0 on all four. SCALE. The shadows were hand-written blacks -- rgba(0,0,0,.32), .4, .42 and .6 -- tuned when the only page was near-black. On #111 a hard black shadow is depth; on #fbfbfb it is a heavy grey halo, and the two big ones were the most dated thing on the light page. Three tokens now, --netscli-lift-sm/md/lg, so the call sites stay in proportion to each other, with light values drawn from the hairline channel rather than black. The same correction the nav's scroll shadow already got. GROUP. The FAQ is a quarter of the page and was thirteen near-identical closed pills. The grouping that would break it up already existed and was doing nothing: a .72rem muted label with 22px between groups against 8px between rows is a 14px difference, which nobody reads as a boundary. 40px between groups, and the title takes the strong ink with a hairline under it, so five groups read as five blocks. Measured light: card text centred on its visual (offset 0, all four); shadows rgba(17,24,39,.1) at 10/28 against rgba(0,0,0,.4) at 12/40 in dark; FAQ group gap 40px with rgb(17,24,39) titles. No horizontal overflow. One process note, because it nearly cost an hour: the first verification run reported the shadows unchanged. The source and the built CSS were both correct -- the DEV SERVER was serving a stale copy of code-surface.css, having been started before that file was edited. Restarting it showed the right values. Component-scoped styles hot-reloaded fine in the same run, which is what made it look like a real bug. Contrast sweep clean on 14 routes both themes, axe clean both themes, 196 token pairs >= 4.5:1, dead CSS 14/14, astro check clean.
fstubner
added a commit
that referenced
this pull request
Sep 2, 2026
…ips, and a lot of measuring (#336) * Navy bands on the light landing page, and one shadow layer instead of two Two reports, both measured. The scroll shadow was two dark layers, not one. The bar casts a blurred box-shadow AND a hard 22px gradient from ::after, and they stack. The previous pass softened only the box-shadow, leaving the heavier of the two untouched. Composited on rgb(251,251,251): the shadow alone gives rgb(225,226,228), the gradient then takes it to rgb(200,202,205) -- a 1.59:1 step in a band directly under the bar, which reads as a grey smear rather than depth. The gradient earns its place in the dark theme, where it is rgba(0,0,0,.42) against a near-black page. On white the blur is the whole effect, so in the light theme it drops to nothing rather than to a smaller number. The DOCS header had the identical defect -- same 0.12 gradient over the same blur, afterOpacity 1 in light -- and the two bars are meant to match, so it gets the same treatment. Only the landing bar was reported. The bands were a tint, not a band. Light theme measured page rgb(251,251,251) against band rgb(238,241,247): 1.09:1, which nobody can see. Dark mode is 1.12:1 and reads fine, because a dark page needs less, so this is scoped to the light theme and dark is untouched. #install and footer, not #install and #surfaces: two bands running back to back read as one heavy slab. The FAQ sits between these two and gives the page a rhythm, and the weight lands at the bottom where the install commands are. The ramp is re-pointed on the subtree rather than restated per element, the same technique the code surfaces use. The rgb CHANNELS invert with it -- hairlines, borders and the ::before accent wash are rgba() call sites and those channels are near-black in the light theme. Two things this needed that the code-surface remap did not: - an explicit `color: var(--netscli-text)`. The section inherits `color` from <body>, where the token already resolved to the light ramp, so re-pointing the token alone left text that sets no colour of its own dark on navy. Measured rgb(68,81,106) until that line. - the light ramp restored inside `.reco`. That card stays a light card on the navy band, so it needs the light ink back -- the same trap `.faq-command` hit, and why `.reco` was pulled out of the code-surface remap. Measured after: install and footer rgb(20,28,46), their text rgb(215,220,229), h2 rgb(244,244,244), lead rgb(154,164,184); .reco still rgb(242,245,250) with rgb(68,81,106) ink; #surfaces still rgb(238,241,247). Both bars afterOpacity 0 in light, 1 in dark. Dark theme identical to before throughout. Contrast sweep clean on 14 routes both themes, axe clean both themes, 196 token pairs >= 4.5:1, dead CSS 14/14, astro check clean. * Deepen the light band instead of inverting it; navy stays on the footer Reworks the previous commit after measuring what the navy #install band actually cost. The .alt and .tryblock command blocks are #10151b. On the old light band they separated at 16.21:1; on the navy band, 1.08:1. The commands a visitor came to copy disappeared into the band while the .reco card -- near-white, 15.56:1 -- became the loudest thing in the section. Contrast had moved from inside the section, where it was working, to around it, where it is decoration. It also broke the alternation. #surfaces and #install are meant to read as the same kind of thing; inverting one left 1.09:1 against 16.43:1, two sections that look nothing alike. So: a deeper tint, not an inversion. #dae1ee measures 1.27:1 against the page -- visible as a band -- and the command blocks still clear 13.96:1 on it. It also lifts the .reco card off the band from 1.03:1 to 1.20:1, so the recommended install finally reads as a card rather than as more band. A band-specific token rather than a deeper --netscli-surface: that token is also the hero's chips, the surface cards, the changelog fade and the lightbox, none of which asked to get darker. .surfaces-section is in the selector list as well as section:nth-of-type (even), and has to be. Only #install is an even section -- the hero is a div, so the sections count surfaces(1), install(2), faq(3) -- and Surfaces.astro paints its own band from --netscli-surface in a component style. The first build after this change tinted #install and left #surfaces at the old value, which is exactly the mismatch being removed and is invisible from either file alone. The footer keeps the navy. It is the one place it costs nothing: no code panels, no cards, one line of text, and a dark footer is a conventional page anchor. Measured light: both bands rgb(218,225,238) at 1.27:1, command blocks 13.96:1 on both, .reco 1.20:1, footer rgb(20,28,46) at 16.43:1 with rgb(215,220,229) ink. Dark theme unchanged: bands rgb(25,29,37) at 1.12:1, footer transparent. Contrast sweep clean on 14 routes both themes, axe clean both themes, 196 token pairs >= 4.5:1, dead CSS 14/14, astro check clean. * No bands on the light page; keep the navy footer and give the cards an edge Third and final treatment for the light landing page, after two that were built, measured and rejected. 1.09:1 the original tint. Invisible; not a band at all. 16.43:1 #install inverted to dark navy. Measured worse where it counts -- the .alt and .tryblock command blocks are #10151b, so they went from 16.21:1 on a light band to 1.08:1 on a navy one. The commands a visitor came to copy vanished into the band while the container shouted. 1.27:1 a deeper tint, #dae1ee. Coherent and visible, rejected on looks: #surfaces and #install are adjacent, so it tinted 59.6% of the page and read as a second background rather than punctuation. White also wins on the measurement, not only on taste: the command blocks separate at 17.72:1 against the page, better than the 13.96:1 they managed on the deeper tint. Structure comes from the content -- dark code blocks, the hero screenshot, card borders -- rather than from full-bleed fills. Two things went with the band: - The .reco card needed a real border. Its fill is rgb(242,245,250) against a rgb(251,251,251) page: 1.06:1, which is not a card. While it sat on a tint that did not matter; on white the border is the only thing marking it, and at .08 it composited to 1.17:1 -- barely more visible than the fill it outlined. .20 gives 1.52:1, a hairline you can see. Light theme only; in dark the lift channel is white and the card already reads. - The radial accent wash on every even section was part of the band treatment. With no band under it, it is a green smudge at 1.08:1 rather than depth, so it is off in the light theme. .surfaces-section is in the selector list as well as section:nth-of-type (even), and has to be: only #install is an even section -- the hero is a div, so the sections count surfaces(1), install(2), faq(3) -- and Surfaces.astro paints its own band in a component style. Clearing one and not the other leaves two sections that should match looking nothing alike, which is invisible from either file alone. The footer keeps the navy. It is the one place a dark band costs nothing: no code panels, no cards, one line of text. Measured light: every section rgb(251,251,251), footer rgb(20,28,46) at 16.43:1, command blocks 17.72:1 on the page, .reco border 1.52:1. Dark unchanged: bands rgb(25,29,37) at 1.12:1, footer transparent, .reco border rgba(255,255,255,.08). Contrast sweep clean on 14 routes both themes, axe clean both themes, dead CSS 14/14, astro check clean. * Shorten the nav group separator so it stops splitting the bar The rule between the scroll links and the navigate links -- FAQ | Docs -- was `border-inline-start` on the first link of the second group. Those links are deliberately the full height of the bar, so the underline marking the current page can sit on the bar's bottom edge, which meant the border drew a 59px line floor to ceiling. It read as a divider cutting the bar in two rather than as a gap between two groups. An 18px pseudo-element instead, centred on the text it sits between. Same colour and same position, a third of the height. Both bars, because they are meant to match: the landing bar in Nav.astro and the docs bar in Header.astro each had their own copy of the border. The stacked mobile menu already divides the two groups with a horizontal rule above the second group, so the vertical one is switched off there. Measured: link height 59px landing / 60px docs, rule now 18px in both, the border gone (0px). astro check clean, dead CSS 14/14. Contrast sweep and axe not re-run for this commit -- it adds one 1px decorative pseudo-element and changes no text colour -- CI runs both on the PR. * Alternating bands on the light page, odd sections tinted The hero is a div, so the sections count surfaces(1), install(2), faq(3): odd tints surfaces and faq and leaves install white, so no two banded sections ever touch. That adjacency is what mattered. Four treatments were measured getting here, and the two that failed failed for different reasons: 1.09:1 the original tint. Invisible; not a band. 16.43:1 #install inverted to dark navy. Worse where it counts -- the command blocks are #10151b and went from 16.21:1 on a light band to 1.08:1 on a navy one. 1.27:1 a deeper tint on BOTH surfaces and install. Visible and coherent, rejected on looks: those two are adjacent, so it tinted 59.6% of the page in one run and read as a second background. 1:1 no bands. Measurably best for the content -- command blocks at 17.72:1 on the page against 13.96:1 on the deeper tint -- and too flat to give the page structure. So the tint can be quiet, because alternation does the work it was being asked to do alone: #e9ecf3 is 1.143:1 against the page and every banded section has a white one above and below it. Command blocks still clear 15.5:1 on it. This only became possible once .reco got a real border. Before that #install had to be tinted -- its cards are near-white and had nothing else to sit on -- which forced surfaces and install to share a treatment and made true alternation impossible. Measured light: surfaces rgb(233,236,243) 1.143:1, install rgb(251,251,251) 1:1, faq rgb(233,236,243) 1.143:1, footer navy; command blocks 15.507:1 on the band and 17.722:1 on white. Dark unchanged: surfaces and install both rgb(25,29,37) at 1.118:1. Dark still bands the EVEN section plus surfaces, so the two themes mark different sections. Left that way because only the light page was being reworked; noted in the CSS. Contrast sweep clean on 14 routes both themes, axe clean both themes, dead CSS 14/14, astro check clean. * Command chips follow the theme; code samples stay dark Splits one rule into two, because they were two different objects wearing the same clothes. A CHIP is a single command with a copy button: the two in the hero and every row in the install section. A CODE SURFACE is something you read: the JSON and config samples, the terminal panel, the docs. On a light page the difference shows. A chip sits beside the "Desktop app" button and behaves like it -- you click it -- so two solid #10151b bars next to an outlined button read as mismatched weights rather than as a set. A sample has no such neighbour and keeps the dark surface in both themes, which is the rule the rest of code-surface.css describes and which is unchanged here. Chips get their own token set, defaulting to the code surface so the DARK theme is byte-identical -- on a dark page there is nothing to mismatch, and the two kinds of thing should look the same there. Only the light theme pulls them apart, to the same #f2f5fa surface and the same 1.52:1 edge the .reco card uses, so a chip and a card read as one family. A token indirection rather than a light-theme rule restating the ramp: the navy footer already carries one copy of the light ink values and .reco another, and a third is where they would start to drift. .alts is included because it IS the divider -- the rows sit on it with a 1px gap so its background shows through between them, and left on the code token it would have drawn near-black hairlines between two light rows. Measured light: hero chip, .cmd and .alt all rgb(242,245,250) with rgb(68,81,106) text at 7.3:1; the surfaces sample still rgb(16,21,27) at 13.32:1; the copy button inside a chip now light with dark ink. Dark unchanged: every one of them rgb(16,21,27) at 13.32:1. This reverses the earlier "all code surfaces, both themes" choice for chips only, deliberately and at the user's direction. Contrast sweep clean on 14 routes both themes, axe clean both themes, 196 token pairs >= 4.5:1, dead CSS 14/14, astro check clean. * Move the download cue inside the button "Windows . .msi installer" was a <p> sibling of the button and the third child of .hero-cta-main, a 200px|1fr grid, so it landed in row 2 column 1 -- hanging under the button while the command chip beside it had nothing underneath. The row read as lopsided and the caption looked detached from the thing it describes. It is the button's second line now, stacked under the label inside the anchor, so it moves and hit-tests with the button. The cue itself stays: clicking starts a download immediately and nothing else on the page names the file or the platform first. Two things the move needed: - the same translateX(8px) the label carries. That nudge optically centres the text against the caret column on the right, and with two lines both have to carry it or they sit on different centres. - -webkit-text-fill-color reset. The label paints itself with a gradient and a transparent fill; inherited, that renders the cue as nothing. The mobile rule that gave the cue its own flex order is gone with it -- it was placing a sibling that no longer exists. scripts/landing/os-tabs.ts still finds it by id and rewrites the text with the href, unchanged. Measured: cue inside the button (wrap.contains true), button 190x50 with label and cue stacked, cue rgb(89,100,120) with a matching fill rather than transparent, button and command chip centred on the same line. Dead CSS 14/14, astro check clean. * Alternate the surface cards light and dark; drop the pale section tint The section-level band is gone and the rhythm moved down a level, to the thing it is actually separating: the four surface cards. Cards 2 and 4 sit on a full-bleed #1c2634 band, cards 1 and 3 on the page. The tint it replaces was structurally right and the wrong colour -- a pale blue-grey at 1.143:1 reads as washed out rather than as a band. Five treatments were measured getting here; the reasons each failed are in the note in global.css, because three of them failed for reasons a screenshot does not show. A pseudo-element carries the full-bleed, because the card sits inside `.w`, which is width-capped and centred: a background on the card alone stops at the reading column and reads as a panel rather than a band. `inset-block` is half the 90px inter-card gap at each end so consecutive bands meet exactly instead of leaving a stripe of page colour between them. The card ALSO carries the same background on itself, and that is not redundant. The contrast sweep composites real ancestor backgrounds and cannot see a ::before, so with the pseudo alone it read the MCP card's body text as rgb(154,164,184) on the white page -- 2.42:1, a fail. It was right to: if the pseudo ever stops painting, that is exactly what a reader gets. The element's own background covers its box, the pseudo extends it to the viewport, same value, invisible join. The code sample on a dark card is separated by an edge, not a fill. `.codeblock` is #10151b against a #1c2634 band: 1.20:1, and no navy that still reads as dark does better -- the lightest tried, #273246, reached 1.42:1 while costing contrast everywhere else. Its border goes from .08 to .18 on these cards instead. The on-dark ink values are now defined once in code-surface.css and referenced by both the navy footer and these cards. They were two separate copies of the same six values, which is how they would eventually have disagreed. Both themes. They cannot carry equal weight: the band is 14.75:1 against the light page and 1.24:1 against the dark one, because on a dark page a band can only be a lift. Measured light: cards 2 and 4 rgb(28,38,52) at 14.75:1 vs page, their headings 13.87:1 and body text 6.09:1; cards 1 and 3 on the page at 17.14:1 and 5.77:1. No horizontal overflow from the 100vw bleed (document 1577px against a 1582px viewport). Contrast sweep clean on 14 routes both themes, axe clean both themes, 196 token pairs >= 4.5:1, dead CSS 14/14, astro check clean. * Start the band on the first card, band the install section, add an inner edge Phase and reach, then depth. PHASE. The surface band moves from even cards to odd, so the rhythm down the page is now: Desktop app (band), Terminal UI, Command line (band), MCP server, Get started (band), FAQ, footer (band). Alternating throughout, and starting on the first card rather than the second. REACH. #install is a band too. This was built once before as dark navy and reverted, because the .alt and .cmd command blocks were #10151b and disappeared into it at 1.08:1 -- the commands a visitor came to copy, lost in their own container. That objection no longer holds, and not because the measurement was wrong: chips follow the theme now, so on a light page they are #f2f5fa and sit ON the band at 13.96:1 instead of inside it. The band became possible because the chips changed. .reco needs the light ramp restored inside it on a light page, the same trap as before -- it is a light card on a dark band. Not in dark, where it is a dark card and the on-dark ramp is already right, hence the theme scope. The Try-it panel stays a dark code surface and takes the same .18 border edge the surface cards give their code samples, because #10151b on #1c2634 is 1.20:1 and no fill fixes that. DEPTH. Every band carries a paired inset shadow, top and bottom, with a negative spread so each stays a thin gradient at its own edge instead of washing the band. Reads as recessed rather than laid on, and softens the light/dark joins. One token, because three hand-tuned copies is how three bands end up at three different depths. On the surface cards it goes on the pseudo-element, which is the part that spans the viewport, so the edge runs the whole band rather than stopping at the reading column. Measured light: cards 1 and 3 and #install all rgb(28,38,52); cards 2 and 4 and #faq on the page; footer rgb(20,28,46). Chip on the install band 13.96:1, section heading 13.87:1, .reco still light with rgb(68,81,106) ink. Inset shadow resolving on all three band kinds in light, and on the cards and #install in dark. No horizontal overflow from the 100vw bleed. Contrast sweep clean on 14 routes both themes, axe clean both themes, 196 token pairs >= 4.5:1, dead CSS 14/14, astro check clean. * Lift the hero's social proof out of footnote size The star count, download count and version are the only third-party evidence above the fold, and the line was the smallest thing on the page: 12px at rgb(89,100,120), directly under a 17px subhead. Legible at 5.77:1, but reading as a footnote. 13px and the body ink step: measured 7.71:1 light and 11.45:1 dark. The separators keep the muted step so the line still reads as one group rather than four competing items. The `.sep` colour was a hard-coded #555 -- a dark-theme grey the light theme had no say over, and the one part of this line that would not have moved with the rest. On the token now, which also fixes it in dark, where #555 on #111 measured 2.6:1 against 6.26:1 now. Contrast sweep clean on 14 routes both themes, axe clean both themes, dead CSS 14/14. * Clip the band bleed so it stops making a horizontal scrollbar The surface bands are full-bleed via `inset-inline: calc(50% - 50vw)`, and 100vw counts the classic scrollbar while the layout box does not. Each side overhangs by half a scrollbar and the right one produces a real horizontal scrollbar: measured 5px of overflow at 1280, 1440 and 1600 -- every width, with a 10px scrollbar. `overflow-x: clip` on the section, not `hidden`. On an element whose other axis is visible, `hidden` computes to `auto`, making it a scroll container and capturing any `position: sticky` inside -- the exact trap that broke the docs contents rail earlier in this branch's history. The check that missed this was mine: the probe compared `scrollWidth > innerWidth`, and innerWidth INCLUDES the scrollbar, so a 5px overflow read as 5px of slack. `scrollWidth > clientWidth` is the test, and it is what the numbers above use. Measured after: overflow 0 at 1280, 1440 and 1600, and the band's computed inset-inline still scales with the viewport (-79px, -159px, -239px), so the bleed itself is intact. Dead CSS 14/14, astro check clean. * Cut the page back to two dark bands: install and the footer The surface cards lose their band. With every other card inverted, the page went white -> navy -> white -> navy -> white -> navy -> white -> navy between the hero and the footer: seven inversions, each a 14.75:1 step. That is striping, not sectioning. Gradient joins were tried first as a softener and were worse. Fading #1c2634 into #fbfbfb runs through a muddy mid-grey, so 64px of it at every edge read as a blur artifact rather than a transition -- and spreading a 14.75:1 step does not shrink it, which is the tell that the edge was never the problem. Reverted rather than committed. What is left is two dark moments: the install section, where the band gives the call to action its own weight, and the footer, which closes the page. Three edges instead of seven, and the four surface cards read as one continuous tour. Going with them, because they only existed for the band: the full-bleed pseudo-element, `position: relative` on .surface, and the `overflow-x: clip` added one commit ago to contain that pseudo's 100vw bleed. The horizontal scrollbar it fixed is gone at the source now -- nothing on the page uses a viewport-width bleed any more. The machinery that stays earns its place elsewhere: --netscli-band-dark and the shared on-dark ink values are used by the install band, and the chip tokens are what let the install band exist at all. Measured light: all four surface cards and #surfaces and #faq on rgb(251,251,251); #install rgb(28,38,52); footer rgb(20,28,46). Horizontal overflow 0 at 1600. Dark unchanged: the cards keep the existing rgb(25,29,37) section tint they always had. Contrast sweep clean on 14 routes both themes, axe clean both themes, 196 token pairs >= 4.5:1, dead CSS 14/14, astro check clean. * Centre the surface rows, put shadows on a scale, group the FAQ Three fixes to the light homepage, all from measuring the page rather than looking at it. CENTRE. Every text block in the surfaces section is 118px and every visual beside it is 218-296px, and the grid was `align-items: start` -- so each row had 100-178px of empty column under its paragraph, about 560px of void in a 1608px section. That is what made the tour read as ragged rather than as four matched rows. `center` puts each text block on its visual's centre line; measured offset is now 0 on all four. SCALE. The shadows were hand-written blacks -- rgba(0,0,0,.32), .4, .42 and .6 -- tuned when the only page was near-black. On #111 a hard black shadow is depth; on #fbfbfb it is a heavy grey halo, and the two big ones were the most dated thing on the light page. Three tokens now, --netscli-lift-sm/md/lg, so the call sites stay in proportion to each other, with light values drawn from the hairline channel rather than black. The same correction the nav's scroll shadow already got. GROUP. The FAQ is a quarter of the page and was thirteen near-identical closed pills. The grouping that would break it up already existed and was doing nothing: a .72rem muted label with 22px between groups against 8px between rows is a 14px difference, which nobody reads as a boundary. 40px between groups, and the title takes the strong ink with a hairline under it, so five groups read as five blocks. Measured light: card text centred on its visual (offset 0, all four); shadows rgba(17,24,39,.1) at 10/28 against rgba(0,0,0,.4) at 12/40 in dark; FAQ group gap 40px with rgb(17,24,39) titles. No horizontal overflow. One process note, because it nearly cost an hour: the first verification run reported the shadows unchanged. The source and the built CSS were both correct -- the DEV SERVER was serving a stale copy of code-surface.css, having been started before that file was edited. Restarting it showed the right values. Component-scoped styles hot-reloaded fine in the same run, which is what made it look like a real bug. Contrast sweep clean on 14 routes both themes, axe clean both themes, 196 token pairs >= 4.5:1, dead CSS 14/14, astro check clean. * One control height across the hero CTA The Desktop app button carries two lines and the command chips carry one, so left to their content they came out 50px against 38px -- three controls in a small area at two different heights, which reads as a mistake rather than as hierarchy. 44px for all three, on --netscli-cta-height so the number is stated once. It fits the button's two lines without inflating the chips much past the 38px they were. Set on the wrapper and on the chips rather than on the inner anchor: box-sizing is border-box globally, so the number is the outer height on both, borders included, which is where the button's extra 2px lived. min-height alone was not enough. The button's label inherits body's line-height of 1.6, which makes a 13px line 21px tall and held the button at 50 regardless of the minimum. At 1.15 the label is 15px and the button lands on the shared height. Measured: button 44, both chips 44, in both themes, with the download cue still rendering its full text. Dead CSS 14/14, astro check clean. Contrast sweep and axe not re-run for this commit -- it changes two lengths and a line-height, no colours -- CI runs both on the PR. * Inline code follows the theme, and land the theme-icon swap INLINE CODE. `code` in prose was pinned to the code surface, so on a white page a near-black chip landed mid-sentence -- `netscli`, `--json`, `netscli serve` punching holes through the surfaces copy. It rides the chip tokens now, the same ones the command chips use: light chip with a dark green ink on a light page, unchanged dark chip on a dark one. Block samples are untouched and still dark in both themes, which is the distinction the chip note in code-surface.css describes -- a word in a sentence is neither a control nor a sample, but it is certainly not a block of terminal output. --netscli-chip-code-fg carries the ink because the dark theme's #86efac is a light-on-dark green: measured 1.5:1 on the light chip, so it could not simply carry over. #0e5122 is 8.66:1 there. That switch exposed a second, real failure the sweep would have caught: .faq .answer code was pinned to --netscli-code-inline-fg back when its chip was dark in both themes, so it kept the light-on-dark green and landed on the new light chip at 1.28:1. On the theme-following token now: 13.06:1 dark, 8.66:1 light. THEME ICON. The landing control's three-icon swap was already written -- markup in Nav.astro, rules in theme-control.css, the choice recorded by the pre-paint script in Page.astro and updated by theme-select.ts -- and was sitting uncommitted in the working tree. Verified end to end rather than assumed, and committed: fresh, nothing stored, system dark -> choice auto, theme dark, laptop fresh, nothing stored, system light -> choice auto, theme light, laptop choose Light -> sun choose Dark -> moon choose Auto -> laptop So the default already follows the operating system in both directions, and the icon does track the selection. The docs bar swaps through Starlight's own ThemeProvider and was measured doing so too -- laptop, sun, moon -- which is why only the landing copy needed its own mechanism. Contrast sweep clean on 14 routes both themes, axe clean both themes, 196 token pairs >= 4.5:1, dead CSS 14/14, astro check clean. * Lift the recommended card, unify the Get started dividers, shade the band edges Four fixes in the install section and at the band joins. CARD IN A CARD. The .reco card and the .cmd chip inside it were identical: both rgb(242,245,250) with the same rgba(17,24,39,.2) border. Nothing distinguished the two objects because nothing about them differed -- it read as a box drawn inside an identical box. The card is white with a shadow now and the chip keeps the chip surface, so it goes band -> lifted card -> recessed control. Measured light: card 15.26:1 off the band, chip a 1.09:1 step below the card, card text 7.98:1. That exposed a second one in DARK, which the navy band had introduced and nothing had looked at: --netscli-surface-raised is #20242b and the band is #1c2634, so the card sat at 1.02:1 -- not a card at all. #26313f gives it 1.16:1, which is as much as a dark page allows. And a third, caused by the fix: `#install .reco` is id-scoped and outranked `html[data-theme='light'] .reco`, so the light card went dark at 1.65:1. The light rule is `html[data-theme='light'] #install .reco` now. Measured in both themes afterwards rather than assumed. DIVIDERS. The tab strip's rule was `border-bottom: 1px solid var(--netscli-surface-raised)` -- a PAGE surface, so on a light page it resolved to a solid near-white #f2f5fa line sitting against a navy band, brighter than the 2px accent underline level with it and a different kind of mark from every other divider in the section. On the hairline channel now, which inverts with the band's ink. BAND EDGES. A small outward shadow at each band's top and bottom, paired with the inset that was already there: the inset darkens the band's inner lip, this shades the page just outside it, so a dark section and a light one no longer simply abut. Deliberately small -- the join should read as a seam, not as a floating panel. One token, used by #install and the footer. Also corrected a comment in global.css that still described the surface cards as carrying alternating bands. They stopped two commits ago; the comment now records all six treatments and why each was rejected. Contrast sweep clean on 14 routes both themes, axe clean both themes, 196 token pairs >= 4.5:1, dead CSS 14/14, astro check clean. * Chips carry their own accent; the FAQ command rows follow the theme TWO CONTRAST FAILURES, both from a token inherited across a boundary it should not have crossed. The install band re-points the accent to its on-dark values so headings and links read on navy. A control sitting on a LIGHT chip on that band inherited them anyway: the Download button's hover was #86efac on an #f2f5fa row -- measured 1.19:1, unreadable, and it is the hover state of the only button in that section. The chip remap carries --netscli-accent, -rgb and -bright now, so anything accent-coloured inside a chip follows the chip rather than the band. Measured after: 7.49:1 light, 11.17:1 dark unchanged. The FAQ answers' command rows were still pinned dark in both themes, from when every chip was. Once the surrounding page went light they read as black bars down a light card. On the chip tokens now: 7.3:1 light, 13.32:1 dark. Both were found by measuring a state a screenshot does not show -- one is a hover, the other only appears inside an opened <details>. A note on what did NOT change: the divider between the Scoop and Installer rows was reported as thicker than the one between Scoop and PowerShell. Measured, they are identical -- 1px, rgba(17,24,39,.14), no row borders, the same 1px flex gap in both groups. The difference is sub-pixel: the rows have fractional heights (64, 91.08 and 64, 43.75), so at a fractional device pixel ratio the two gaps land at different offsets -- measured 0.848 and 0.625 of a device pixel at dpr 1.25 -- and antialias differently. There is no CSS inconsistency to fix; making them render identically means giving the rows integer heights, which is a different change and worth deciding on rather than sneaking in here. Contrast sweep clean on 14 routes both themes, axe clean both themes, dead CSS 14/14, astro check clean. * Trim the hero subhead from three lines to two 242 characters became 144, and the rendered block went from three lines to two, which lifts the whole CTA block up the page. The packet-capture clause went. It read "with packet capture in capture-enabled CLI builds", and it was added for a good reason: an earlier version listed capture alongside the other operations and so implied the desktop installer directly below it included capture, which no published one does. Saying nothing makes no claim at all, which is the same protection for none of the length. Capture is covered properly in the FAQ and in /docs/install/#packet-capture, which is where anyone looking for it will be. The second sentence went with it: "all on one Rust core" carries what "each interface calls the same Rust core, so results stay consistent" carried, and the reader who wants the longer version is already in the docs. Traceroute stays absent for the reason it has been since it was removed -- the sentence attributes what it lists to all four interfaces and the MCP server has no traceroute tool. The meta and og descriptions are untouched. They are a different job with a different length budget, and og:description still names capture, correctly scoped to capture-enabled CLI builds. Measured: 144 characters, 2 rendered lines at 1400px against 3 before. Contrast sweep clean on 14 routes both themes, axe clean both themes, astro check clean, dead CSS 14/14. * Make the band's inset edge actually visible The inset shadow was applied and computing correctly the whole time, and rendered as nothing. `inset 0 11px 15px -11px` puts the negative spread equal to the offset, which pulls the shadow's edge back to exactly the border box and leaves the 15px blur almost nothing to draw. Proved by sampling the rendered pixels down from the band's top edge rather than by reading the computed value, which looked fine: before -2: rgb(27,36,50) +2: rgb(28,38,52) ... +24: rgb(28,38,52) after -2: rgb(250,250,250) +2: rgb(16,22,31) ... +24: rgb(27,37,52) Before, the whole span varied by one or two values out of 255. After, there is a ~20px ramp from near-black at the edge up to the band colour. `inset 0 14px 18px -10px rgba(0,0,0,.7)`: the spread is now smaller than the offset so the shadow starts inside the box with room to fall off, and the alpha is high enough to register against a surface that is already dark. The lesson for the next one of these: a computed box-shadow that reads correctly says the declaration parsed, not that anything was drawn. For a shadow on a dark surface those are very different claims, and only sampling pixels tells them apart. Contrast sweep clean on 14 routes both themes, axe clean both themes, dead CSS 14/14, astro check clean. * Trim global.css back under the 300-line guard The six-treatment note added two commits ago pushed the file to 309 lines and CI's file size guard caught it. Every measured number is kept -- they are the point of the comment -- and the prose around them is shorter. Worth noting for the next trim: `wc -l` reported 300 where the guard reported 301. The guard counts the final line even without a trailing newline, so aim a couple of lines under rather than exactly at the limit. File size guard passes, dead CSS 14/14, astro check clean, build clean.
fstubner
added a commit
that referenced
this pull request
Sep 3, 2026
* initial commit: netscli v0.1.0
A Rust network scanner with four surfaces backed by a shared core:
- netscli-core — library (host discovery, port scan, DNS, OUI lookup)
- netscli-cli — CLI binary and ratatui TUI (released as `netscli`)
- netscli-mcp — MCP server (JSON-RPC over stdio)
- netscli-gui — Tauri 2 + React desktop app
Repo scaffolding:
- 67 passing tests (unit + integration, per-OS where applicable)
- GitHub Actions CI: lint, cross-platform test matrix, release build
- Release workflow + release-drafter for tagged builds
- GitHub Pages landing site at netscli.com with Cloudflare Web Analytics
- crates.io publishing guide in docs/PUBLISHING.md
- MIT license
* site: add favicon
Simple bold 'n' in the same green-cyan gradient as the ANSI Shadow
wordmark, on a dark rounded square matching the site background.
SVG-only (supported in every evergreen browser), served from the
Pages root as /favicon.svg with matching apple-touch-icon.
* site: use the NETSCLI wordmark 'N' for the favicon
Lifted the ANSI Shadow 'N' block (6 rows x 10 chars) from
docs/assets/netscli-wordmark.svg verbatim, same font stack and
green-cyan gradient, on the same dark rounded square. Reads as a
zoomed-in slice of the header logo rather than an abstract 'n' shape.
* site: shrink favicon N glyph for breathing room
* site: visual polish pass
- <title> expanded to "netscli — A modern network scanner" and synced
across og:title / twitter:title.
- og:image + twitter:card + twitter:image so social shares get a real
preview card (points at gui-dashboard.png for now; a dedicated 1200x630
card can come later).
- theme-color meta so mobile browser chrome matches the dark UI.
- Nav ANSI wordmark bumped from 3.5px to 5.5px so the NETSCLI letters
are actually perceivable instead of reading as gradient texture.
- Dropped the forced <br> in the hero h1; max-width:15ch on the
heading produces the same two-line shape at desktop widths while
letting smaller viewports wrap naturally instead of breaking
mid-clause.
- Green outer ring + soft glow on the hero quick-install command,
matching the wordmark's teal/green hue.
- Cargo install promoted to the first install card with a hint
explaining why it's the cleanest path for Rust users. Symmetric
packet-capture hint added to the Windows card. install-hint colour
lightened from #666 (~3.2:1 on #111, fails WCAG AA) to #8a8a8a
(~5.3:1, passes AA for small text).
* site: second polish pass
Hero and typography:
- Live social proof strip below the hero sub: GitHub stars + cumulative
release asset downloads, fetched from the public GitHub API on load.
Em-dash placeholders stay visible on rate-limit/failure (never shows
a misleading zero).
- Fix stray em dash in the hero sub paragraph.
- Sub max-width 500px -> 640px so three-line wrap instead of four at
tablet widths.
- "Windows & Cargo options" link picks up a dotted underline + a down
arrow so it telegraphs "click to jump to install section". View
source link keeps its GitHub mark as its own affordance.
Copy buttons:
- Always visible at 70% opacity on touch devices via @media (hover: none).
- Copy feedback timeout 1500ms -> 2500ms so the "Copied!" confirmation
doesn't vanish before the user looks at it.
Install section:
- Quick-install command wraps on <=600px instead of truncating with an
ellipsis that hid the critical `| bash`.
- "Then try" -> dedicated .try-title heading matching the install-label
tier but one size up so it anchors the right column.
Nav and footer:
- Wordmark hover lights up with a subtle brightness lift and green
drop-shadow so it reads as interactive.
- "Built with Rust, ratatui, Tauri, hickory, sqlx" acknowledgment row
in the footer.
SEO / hygiene:
- robots.txt with sitemap pointer.
- sitemap.xml with the single URL (trivial, but completes the SEO
handshake for search engines).
* site: fix Lighthouse CLS + accessibility findings
CLS (was 0.241, target < 0.1):
- Explicit width/height on every <img>. Browsers compute intrinsic
aspect ratio from the attribute pair and reserve vertical space
before the image loads, eliminating the "content jumps down when
images arrive" shift that was driving the score.
- height:auto on .bigshot img and .surface-visual img so CSS's
width:100% scales the reserved space without stretching.
LCP:
- fetchpriority="high" on the hero screenshot (first meaningful image).
- loading="lazy" on below-fold surface images (scan screenshot and
TUI SVG) so they don't compete with the hero for early bandwidth.
Accessibility (was 93, contrast failures):
- Badge color #777 -> #9a9a9a (was ~4.2:1, below AA for small text).
- Hero sub #888 -> #a3a3a3, section lead #888 -> #a3a3a3, surface
body #999 -> #a8a8a8, hero links #888 -> #a3a3a3, nav links #999
-> #b0b0b0, footer #555/#777 -> #888/#9a9a9a, built-with #555/#888
-> #888/#a8a8a8. All now clear 4.5:1 against #111.
- Copy button #999 -> #c8c8c8 (on #222 background), with border alpha
bumped for additional edge contrast.
- Wrap hero + sections in a <main> landmark so screen readers get the
single main-content region they expect.
* site: serve WebP variants of hero + scan screenshots
Wrap the dashboard + scan images in <picture> with a WebP source and
the existing PNG as fallback. Saves 54 KiB total (dashboard 55->33,
scan 48->16) with no visible quality loss. Scan WebP is resized to
976x710 to match exactly 2x its display dimensions, so it stays crisp
on retina while dropping the 40+ KiB Lighthouse flagged.
gui-dashboard stays at native 1375x1000 because it's the hero / LCP
target — the extra resolution is worth the bytes for retina crispness
on the above-the-fold image.
PNGs retained for the ~3% of browsers without WebP support.
* site: drop accidental gui-scan-976.png intermediate from previous commit
* site: fix mobile overflow + nav logo distortion
Horizontal scrollbar on mobile: the CLI surface card's codeblock uses
white-space:pre with content wider than ~400px viewports. Without
min-width:0 on the grid children, the codeblock forced the entire
.surface grid wider than its container, which cascaded up through the
section and overflowed the viewport. Adding min-width:0 to .surface > *
(including .flip > *) lets the codeblock's own overflow-x:auto contain
the overflow where it belongs, restoring the normal responsive flow for
every sibling element in the section.
Nav wordmark on mobile: the ANSI Shadow art relies on Unicode box-drawing
characters (___ ___ ___) rendered through the system monospace stack. At
5.5px on Android, Roboto Mono's hinting for those glyphs distorts heavily
(rows mis-align, letters collapse into each other). On desktop the same
markup renders fine.
Swap to a .mark-plain fallback below 600px: bold "netscli" at 17px in
the same green-to-cyan gradient, still monospace for the developer-tool
feel, still links to /. Desktop keeps the ANSI art intact.
* site: SEO + GEO improvements (structured data, richer meta, better alt)
Changes aimed at making netscli.com show up well in search and, more
importantly these days, in AI answer engines (Perplexity, Google AI
Overviews, ChatGPT search) without paid distribution.
Meta:
- <title> expanded to include the load-bearing keywords: "Rust network
scanner with CLI, TUI, desktop app, and MCP server". Title tag is
still the single highest-weight SEO signal.
- Meta description extended to call out the LLM/MCP angle explicitly,
which is our most distinctive differentiator for queries like "mcp
network scanner" or "ai agent network tools".
- Added `author` and `keywords` meta tags.
- Added `og:site_name`.
- Twitter/OG titles and descriptions kept in lockstep with the new
text.
Structured data (JSON-LD):
- SoftwareApplication schema declares this is a free MIT-licensed
Rust developer tool with screenshots and download URL. Google uses
this for rich results in web search; AI answer engines use it to
decide the page is authoritative for "what is netscli" queries.
- FAQPage schema with six plain-English Q/A pairs (what is it, how to
install, Claude Code/Cursor integration, OS support, open source,
libpcap requirement). AI engines cite FAQ answers directly in their
responses, so this is the single biggest GEO lever on a landing
page.
Image alt text:
- gui-dashboard.png, gui-scan.png, and tui-discover.svg now describe
what's in the image, not just that one exists. Better for screen
readers, better for image search indexing, better for AI engines
that OCR or summarize images for context.
Sitemap lastmod bumped to today so crawlers requery.
* site: rasterize wordmark + TUI preview, add visible FAQ section
Wordmark + TUI preview:
- Installed resvg (Rust CLI, pure-Rust SVG renderer) and rasterized
docs/assets/netscli-wordmark.svg and site/tui-discover.svg to PNG at
2x retina. For the wordmark the PNG is 16 KB; for the TUI preview
71 KB PNG + 48 KB WebP via ffmpeg.
- Replaced the nav wordmark (CSS text with ANSI Shadow box-drawing
glyphs at 5.5px) with <img src="assets/netscli-wordmark.png">. Now
every device sees the same crisp raster instead of relying on the
system monospace font's handling of U+2500..U+259F block characters,
which Android's Noto/Roboto Mono renders inconsistently at that
size.
- Replaced the tui-discover.svg <img> with <picture> serving the WebP
primary and PNG fallback. SVG source still exists in repo for future
re-rendering but the page no longer embeds it.
- Dropped the .mark-plain mobile-fallback rule: the PNG handles both
viewports now, so there's no need for the alternate text node.
FAQ section:
- New <section id="faq"> between Install and footer with six collapsible
<details> blocks mirroring the FAQPage JSON-LD landed earlier. Gives
AI answer engines the visible content to anchor their citations, and
gives humans a straight-answer path to common questions without
scrolling the install instructions.
- Nav now links to the FAQ anchor.
- Styled to match the existing section tone: subtle translucent card
per question, custom +/− marker via ::before, hover darken, smooth
open/close transition.
* site: migrate to Astro
Replaces the hand-rolled 683-line index.html + styles.css with a
proper Astro scaffold. Produces an identical production HTML output
(build goes to site/dist/) while giving us:
- a reusable starter shape for future project landings — all
per-project content now lives in site/src/data/site.ts and every
component reads from there. Forking the landing for a new product
means editing one file.
- real components (Nav, Hero, Surfaces, Install, Faq, Footer) with
a single Page layout that owns meta, OG, JSON-LD, and analytics.
- structured data generated from the same data file that the visible
FAQ section reads from, so the JSON-LD can't drift from the UI.
- an obvious path to a blog / docs / changelog later via Astro
content collections.
Scaffold:
- site/package.json, astro.config.mjs, tsconfig.json, .gitignore
- site/src/data/site.ts — single-source-of-truth content
- site/src/styles/global.css — extracted verbatim from the old <style>
- site/src/layouts/Page.astro — meta, OG, JSON-LD, Cloudflare beacon
- site/src/components/*.astro — Nav, Hero, Surfaces, Install, Faq, Footer
- site/src/pages/index.astro — composition + inline client script for
copy buttons and GitHub-API-backed social proof
Static assets moved to site/public/ (favicon, CNAME, robots.txt,
sitemap.xml, screenshots, wordmark PNG, TUI preview PNG+WebP).
Build workflow (.github/workflows/pages.yml):
- added actions/setup-node@v5 + `npm ci` + `astro build`
- upload path changed from `site` to `site/dist`
- deploy split into its own job that depends on build, matching the
official Astro + GitHub Pages pattern
site/README.md rewritten to document the new layout and cover the
"use this as a template" flow for other projects.
Local dev: `cd site && npm install && npm run dev` on port 4321.
Production build: `npm run build` -> site/dist/. Verified clean
accessibility tree snapshot with all sections present, live GitHub
API fetches, and working copy buttons.
* site: fix FAQ horizontal overflow on mobile
Long URLs inside <code> spans in the FAQ answers don't break by default
(CSS treats a URL as a single word) and were pushing the .faq details
cards wider than the viewport on phones. The curl and iwr commands in
the "How do I install netscli?" answer were the worst offenders.
Add overflow-wrap: anywhere + word-break: break-word to .faq .answer,
word-break: break-all to .faq .answer code, and overflow-wrap: anywhere
to .faq .answer a. All three together let URLs and code tokens break at
any character so the cards stay inside the viewport.
Verified with preview at 375px width: documentElement scroll width now
matches the viewport (no horizontal scrollbar), FAQ cards measure 327px
inside 375px viewport after the 24px side padding.
* site: extract section headings into site.copy for template reuse
Pull the three hardcoded section titles and leads (Surfaces: "Four
interfaces, one library" / "Every surface calls the same netscli-core
…", Install: "Get started" / "Install, then run. …", FAQ: "FAQ" / "The
questions people actually ask…") out of Surfaces.astro, Install.astro,
and Faq.astro and into a new site.copy field in src/data/site.ts.
Components now read {heading, leadHtml} per section. With this change,
every visible per-project string in the site lives in site.ts; the
components have zero netscli-specific content. Mirrors the change
landing in template-landing-page-astro.
* release: v0.2.0
Bump all workspace crates + companions to 0.2.0. CHANGELOG.md stamped
with the v0.2.0 section (mdns capability, typed errors, feature-gated
db, cargo-audit CI, completions + man + sigstore, packaging templates)
and the version reference links.
- crates/netscli-core/Cargo.toml: 0.1.1 -> 0.2.0
- crates/netscli-mcp/Cargo.toml: 0.1.1 -> 0.2.0
+ netscli-core path-dep version pin 0.1.1 -> 0.2.0
- apps/netscli-cli/Cargo.toml: 0.1.1 -> 0.2.0
+ both netscli-core and netscli-mcp path-dep pins updated
- apps/netscli-gui/src-tauri/Cargo.toml + tauri.conf.json + package.json
- site/src/data/site.ts version field used in SoftwareApplication JSON-LD
- packaging/{homebrew,scoop,aur} version fields (URLs automatically
resolve to the v0.2.0 release once tagged)
All 67+ tests green. cargo clippy --all-targets -- -D warnings clean.
cargo fmt --check clean.
* docs+site: advertise Homebrew tap + Scoop bucket install paths
After creating the fstubner/homebrew-tap and fstubner/scoop-bucket
repos this afternoon, the install UX on Mac + Windows is now native
package-manager style. Surface that prominently.
README.md:
- New "Homebrew (macOS + Linux)" and "Scoop (Windows)" subsections at
the top of Installation, ahead of the existing curl/iwr scripts.
site/src/data/site.ts:
- Install grid goes from 3 entries to 5:
1. Homebrew (macOS + Linux) — brew tap fstubner/tap && brew install
2. Scoop (Windows) — scoop bucket add ... && scoop install
3. Cargo — cargo install netscli
4. Linux / macOS script — curl ... install.sh | bash
5. Windows PowerShell script — iwr ... install.ps1 | iex
Winget and AUR still to come; they'll slot in after PR acceptance /
namespace-registration.
Verified locally: 5 cards render in order, no mobile overflow at
375px viewport, live GitHub-API social proof still works.
* site: PageSpeed fixes — right-size images, inline CSS, preconnect hints
Lighthouse flagged three LCP/FCP issues on the deployed site:
1. Three images were oversized for their display slots:
- netscli-wordmark.png 2400×480 → 720×144 (nav slot is 160×32; 720
gives ~4.5x retina headroom, plenty for any DPR)
- gui-dashboard.webp 1375×1000 → 1268×922 (hero slot is 634×461)
- tui-discover.webp 1640×930 → 1268×718 (surface slot is 634×359)
Combined: 112 KiB → 86 KiB (~26 KiB saved). resvg handled the
wordmark SVG -> PNG; ffmpeg -c:v libwebp -quality 85 handled the
WebP re-encodes at the smaller scale.
2. The Astro-generated CSS (~3 KiB gzipped) was being linked as a
separate render-blocking stylesheet. Switched astro.config.mjs
build.inlineStylesheets from 'auto' to 'always'. Verified
stylesheetLinks.length === 0 in the rendered page.
3. No preconnect hints for the two third-party origins we hit
asynchronously. Added <link rel="preconnect"> for
api.github.com (stars + downloads fetches) and
static.cloudflareinsights.com (analytics beacon, only when
configured). Cuts ~150-300ms off first-request latency by
warming DNS + TLS in parallel with HTML parsing.
Verified locally:
- 0 stylesheetLinks, 1 inline <style>
- 2 preconnect hints in <head>
- wordmark naturalWidth === 720, hero naturalWidth === 1268
- No console errors
* docs+site: advertise AUR install path (netscli-bin)
netscli-bin is now live on the Arch User Repository:
https://aur.archlinux.org/packages/netscli-bin
Same prebuilt binary as the GitHub release, packaged with shell
completions + man page generated at install time from the binary
itself (same pattern Homebrew and Scoop use).
README.md: new "AUR (Arch Linux)" subsection after Scoop.
site/src/data/site.ts: install grid now has 6 entries; AUR slots in
third, ahead of the script-based installs.
Verified locally: 6 cards render, no horizontal overflow at desktop
1280px or mobile 375px.
* docs+site: advertise winget install path (microsoft/winget-pkgs)
PR #363074 merged on 2026-04-22; manifests live under
manifests/f/fstubner/netscli/0.2.0/ on master. Add `winget install
fstubner.netscli` between Homebrew and Scoop in the README install
section and the Astro landing-page install grid.
* deps: bump astro 5.18.1 -> 6.1.9 and rustls-webpki 0.103.12 -> 0.103.13
Clears the actionable Dependabot alerts opened 2026-04-21..04-24:
- astro #18, #19 (medium, GHSA-j687-52p2-xcff): fixed in 6.1.6+. The
static landing uses only stable Astro APIs, so the 5 -> 6 migration
needed zero source changes; build output is byte-identical aside
from a slight CSS hash bump.
- rustls-webpki #26 (high, GHSA-82j2-j2ch-gfr8): patch bump.
Six alerts in the same batch were dismissed instead of patched:
- 5x openssl FPs (#21-25): the openssl crate does not appear in our
Cargo.lock at all; we use rustls for TLS.
- rand #20 (low): vulnerable 0.7.3 line is transitive via Tauri's
kuchikiki path; documented in .cargo/audit.toml as RUSTSEC-2026-0097.
* site: redesign install section with OS tabs and per-OS recommended commands
The flat 7-entry install grid was cramped and forced every visitor to
scan options that don't apply to their OS. New design:
- Three OS tabs (Windows / macOS / Linux). Visitor's OS is auto-detected
via navigator.userAgentData.platform with fallback to navigator.platform
and finally macOS.
- Each tab surfaces one recommended command (always-visible Copy button)
plus alternative install methods as compact rows. Recommended choice
per OS: Winget on Windows, Homebrew on macOS, install.sh on Linux.
- Winget command shortened from `winget install fstubner.netscli` to
`winget install netscli` — uses the Moniker we set in the manifest.
- Try it sidebar restructured: each example command is its own copyable
row instead of one big block.
- Layout uses a 2-row CSS grid with explicit grid-row placement so OS
tabs span row 1 col 1 only and the Try it card sits level with the
recommended card on the left, not above it.
- Mobile breakpoint at 800px stacks both columns.
Data shape in site.ts changed from a flat InstallEntry[] to a per-OS
byPlatform: Record<Platform, InstallEntry[]> with position 0 as the
recommended entry. Cross-platform methods (Cargo, Homebrew, install.sh)
are duplicated across the relevant arrays — small data redundancy in
exchange for unambiguous per-OS ordering and zero render-time filtering.
Spec: docs/superpowers/specs/2026-04-29-install-section-redesign-design.md
* site: retint install section to brand green (#24)
The OS-tabbed install section landed using sky-blue accents (#0ea5e9 /
#7dd3fc / #9aceeb). The rest of netscli.com uses a single brand green
accent rgba(10,174,122,*) — the install section reads as a different
product. Retint to match:
- Active OS tab underline: #0ea5e9 -> #0aae7a
- Always-visible Copy button on the recommended hero: blue tint stack
(#102532/#1e3a5f/#9aceeb) -> green tint (rgba alpha + #0aae7a)
- Alternative-row command color: #9aceeb -> #ccc (neutral, matches
the existing .cmd palette; hierarchy carried by font size + card
border, not color)
No HTML or JS changes; pure CSS.
* site+docs: lead with TUI on landing page; correct README inaccuracies (#25)
The desktop app currently can't be installed via Homebrew, Winget,
Scoop, AUR, or the install scripts -- they all ship the CLI/TUI
binary only, and no prebuilt GUI installers (.msi/.dmg/.AppImage/
.deb) are attached to the GitHub release. Featuring the desktop app
in the hero and as the first surface card sets an expectation that
package-manager installs don't fulfill. Until prebuilt GUI installers
ship, lead with the TUI on the landing page and add an honest
"build-from-source only" caveat in the README.
Landing page (site/src/data/site.ts):
- Hero image: /gui-dashboard.{png,webp} -> /assets/tui-discover.{png,webp}
- Hero subhead drops "desktop app" mention; leads with TUI / CLI / MCP
- OG image (social sharing previews): tui-discover instead of gui-dashboard
- Surfaces reordered: Terminal UI, Command line, MCP server, Desktop app
(Desktop now last, with an inline note about build-from-source)
- MCP server card: "10 tools" -> "nine by default (ten with pcap)" to
match the actual server.rs count
README accuracy fixes (cross-referenced against actual code, not other
docs):
- Line 36 / 417: "nine tools" vs "10 tools" was contradictory; both
builds clarified. Default release: 9 tools (mdns enabled by default in
Cargo.toml). pcap-enabled release: 10. Verified against
crates/netscli-mcp/src/server.rs.
- Winget cmd: `winget install fstubner.netscli` -> `winget install
netscli`. Uses the Moniker we set in the locale manifest.
- Wording fix: "Ships preinstalled on Windows 10/11" referred to
netscli; rephrased to clarify winget itself is preinstalled.
- TUI command list at line 244-257 was missing /mdns. Added (verified
against apps/netscli-cli/src/tui.rs:164).
- GUI Application section: added a heads-up that the desktop app is
build-from-source only.
* release: ship prebuilt GUI installers + bump to 0.2.1 (#30)
The desktop app has been "build from source only" since launch. None of
the package managers ship it, none of the GitHub release assets ship it.
This commit closes that gap by adding a Tauri build matrix to the release
workflow that produces .msi (Windows), .dmg (macOS aarch64 + x86_64),
.deb, and .AppImage (Linux x86_64) per release, sigstore-signed alongside
the CLI binaries.
What changed
- .github/workflows/release.yml: new `gui` job that runs `npm run tauri
build` on each native runner, renames bundle outputs to a predictable
netscli-gui-{os}-{arch}.{ext} scheme, generates SHA256s, sigstore-signs
via cosign, and uploads to the same release as the CLI binaries.
- Bumped workspace version 0.2.0 -> 0.2.1 across:
apps/netscli-cli/Cargo.toml, apps/netscli-gui/{package.json,
src-tauri/{Cargo.toml,tauri.conf.json}}, crates/netscli-{core,mcp}/Cargo.toml,
site/src/data/site.ts.
- CHANGELOG.md: new 0.2.1 section documenting GUI installers, the
--concurrency flag, sysinfo + pnet bumps, and the ipnetwork
hold-pattern.
- README GUI Application section: replace the "build from source only"
heads-up with actual download instructions, including the macOS
unsigned-Gatekeeper workaround.
Out of matrix scope (kept lean for v0.2.1, can add later)
- Linux ARM64 GUI builds
- Windows ARM64 .msi
- macOS notarization (paid Apple Developer cert decision)
After this lands, drafting a v0.2.1 GitHub release fires the workflow
and produces the GUI assets.
* site: restore alternating left/right rhythm in surfaces section (#31)
After the TUI-first reorder in PR #25, the surfaces section ended up with
a non-alternating layout: TUI(default), CLI(default), MCP(flip), Desktop(default)
— pattern A,A,B,A. The visual rhythm broke.
Adds flip:true to the Terminal UI card so the pattern becomes
B,A,B,A — every other card swaps which side the visual sits on, which
is what the .flip class is for (per the SurfaceCard interface comment:
'flips text and visual sides for alternating rhythm').
* site: add 4 SEO-targeted FAQ entries (#26)
Adds FAQ entries that surface for visitors searching for traditional
network scanners (Angry IP Scanner, Advanced IP Scanner), generic
home-network device discovery, free-OS-network-scanner queries, and
nmap alternatives.
Each entry has both a plain-text 'a' (used in JSON-LD schema for AI
answer engines and Google) and an HTML-rich 'aHtml' (rendered on the
visible page). Combined Windows/macOS/Linux into one entry rather than
three separate ones to avoid near-duplicate-content penalties.
FAQ count: 6 -> 10.
* site: feature prebuilt GUI installers on the Desktop surface card (#35)
* site: feature prebuilt GUI installers on the Desktop surface card
Now that release.yml ships .msi / .dmg / .deb / .AppImage with every
release (PR #30), the landing page should let visitors download the
desktop app directly instead of leaving them to find the GitHub
releases page on their own.
What changed
- SurfaceCard interface gains an optional `downloads: SurfaceDownload[]`
field. Surfaces.astro renders a button row below the card body when
present.
- Desktop surface card body: drop "build-from-source only" caveat
(stale; PR #30 fixed that). New body: "Prebuilt installers attached
to every GitHub release — sigstore-signed, no build-from-source
needed." with five download buttons:
Windows .msi, macOS Apple Silicon .dmg, macOS Intel .dmg,
Linux .deb, Linux .AppImage
- All buttons point at /releases/latest/download/<asset>, so they
auto-track the most recent release without needing a site update on
every patch version.
- New CSS for .surface-downloads / .surface-download-btn matching the
brand-green palette already used by .has-copy.always-show. Buttons
wrap onto multiple rows on mobile (flex-wrap:wrap), confirmed at
375px viewport.
Layout review covered in the same pass: 3 sections (Surfaces →
Install → FAQ), 4 surfaces with the correct alternating flip pattern
[T,F,T,F], 3 OS install tabs, 10 FAQ entries, mobile breakpoint
handles the new download row cleanly.
* site: also surface .msi/.dmg/.deb/.AppImage in install section's footer note
The Desktop surface card now features the GUI installers prominently
with download buttons (added in b692648). Polishing the install
section's binariesNote to mention them too — visitors who land on
'Get started' shouldn't have to scroll back up to discover the GUI
download path.
Before: 'Or grab binaries from the latest release.'
After: 'Or grab CLI binaries and desktop installers (.msi / .dmg /
.deb / .AppImage) from the latest release.'
* site: surface desktop app downloads in the hero, beside the curl install (#36)
Visitors who land on netscli.com see a CLI curl one-liner immediately
but have to scroll past the surfaces section to discover the desktop
app even exists. Adds a small secondary install row directly under
the quick-install card:
Or get the desktop app: Windows · macOS · Linux
Each platform is a direct download link to the matching .msi / .dmg /
.AppImage from the latest GitHub release. Visually quieter than the
primary curl card so the CLI install keeps focal weight, but it's
right there for visitors who want a window.
Mobile (375px): fits on one row after using "Or get" instead of "Or
download" — the longer label was wrapping "Linux" onto a hanging
second row.
* release: v0.2.2 lockfile fix + workspace bump (#39)
The v0.2.1 release.yml run failed all 17 build matrix entries with
'lock file needs to be updated but --locked was passed'. Root cause:
Dependabot's #17 (tokio 1.48 -> 1.52.1) updated only the direct-dep
entries in Cargo.lock; tokio 1.52.1 transitively requires
socket2 >= 0.6.3 which never made it into the committed lockfile
(stayed at 0.6.1). CI lint paths use plain `cargo build` which
auto-regenerates, so the drift stayed invisible until the strict
--locked release build hit it.
What this commit does:
- Regenerates Cargo.lock so socket2 0.6.3 is recorded alongside
the existing 0.5.10. No application source changes.
- Bumps workspace + GUI + site versions 0.2.1 -> 0.2.2 across:
apps/netscli-cli/Cargo.toml, apps/netscli-gui/{package.json,
src-tauri/{Cargo.toml,tauri.conf.json}}, crates/netscli-{core,mcp}/Cargo.toml,
site/src/data/site.ts.
- CHANGELOG.md gets a 0.2.2 'Fixed' entry plus a note on v0.2.1's
status (crates.io has 0.2.1 but the GitHub release has no assets;
package managers should pick up 0.2.2).
Verified locally: `cargo build --locked --release -p netscli` clean
on Windows. CI will exercise the same on ubuntu/macos/windows.
* release: v0.2.3 fix tauri version skew + AUR publish path (#40)
v0.2.2 CLI shipped successfully but the GUI installer matrix and the
AUR publish job both failed independently. This is the recovery
release that fixes both.
Tauri version skew (4 GUI build failures)
- Cargo.toml had tauri = "2.0.0" which the resolver locked to tauri
2.9.5; npm @tauri-apps/api: ^2 resolved to 2.10.1. Tauri's CLI
rejects same-major different-minor as a version mismatch.
- Loosen the Rust constraint to tauri = "2" so cargo can resolve to
the latest 2.x; ran npm update --save so both sides land on 2.11.0.
- Verified locally: `cd apps/netscli-gui && npm ci && npm run tauri
build` produced NetsCLI_0.2.3_x64_en-US.msi cleanly on Windows.
AUR job (publish.yml)
- Rendered PKGBUILD was being written to /tmp/PKGBUILD but the
KSXGitHub/github-actions-deploy-aur action runs in a Docker
container that mounts $GITHUB_WORKSPACE only — files in /tmp on
the runner are invisible inside the container, surfacing as a
confusing `bash: --command: invalid option` error.
- Render now writes to packaging/aur/PKGBUILD (workspace-relative)
before the deploy step picks it up.
Workspace bumped 0.2.2 -> 0.2.3 across the same 7 files as before.
CHANGELOG section explains both fixes plus the v0.2.2 status.
* release: v0.2.4 fix GUI bundle path + AUR action version (#41)
v0.2.3's release.yml GUI matrix built the bundles correctly but the
collect step looked in the wrong directory; v0.2.3's publish.yml AUR
job hit a regression in the deploy action. Both surface as different
errors than v0.2.2/v0.2.3 hit, and both root-cause to incorrect
infrastructure assumptions in the workflows.
GUI bundle path
- BUNDLE_ROOT was set to apps/netscli-gui/src-tauri/target/<TARGET>/
release/bundle. Cargo workspaces always write to the workspace-root
target/ regardless of the cwd, so Tauri bundles land at
target/<TARGET>/release/bundle/. Fixed to use the workspace-root
path.
AUR action
- Pinned to KSXGitHub/github-actions-deploy-aur@v2.7.0 (April 2024),
which has a `bash: --command: invalid option` regression in its
container entrypoint. Bumped to @v4.1.3 (current stable). v4 has
identical input names so no other workflow changes needed.
Workspace bumped 0.2.3 -> 0.2.4. Both CHANGELOGs explain the
release-pipeline failures so future readers can trace what happened.
* release: v0.2.5 windows scan fix + GUI styling fix + 13 dep bumps (#60)
Highlights since v0.2.4 (13 PRs merged):
- Security: hickory-resolver 0.24 -> 0.26 closes RUSTSEC-2026-0119
(CPU exhaustion via O(n^2) name compression in hickory-proto). The
source migration to the new TokioResolver builder pattern landed in
PR #55; the temporary audit.toml ignore added in PR #52 was removed.
- Fixed: GUI discover/sweep returned a single host on Windows because
detect_default_ipv4_subnet picked up the host /32 prefix from
ipconfig::Adapter::prefixes() instead of the network /24. New
testable helper pick_ipv4_subnet_from_prefixes filters to
network-shaped prefixes (length 1..=30, not multicast/link-local)
and truncates host bits, matching the Linux path. (#59)
- Fixed: Dashboard 'Recent Scans' rendered with wrong colors and not
as list rows because the .history-item button didn't reset
user-agent button styles. Explicit reset added. (#59)
- Changed: 11 transitive dependency bumps including coupled
crossterm/tui-textarea/mdns-sd in PR #58. ratatui 0.30 deferred
upstream (tui-textarea hasn't published a compatible release yet).
- Added: publish.yml fans out to Homebrew Cask, Scoop extras, Winget
GUI manifest, and AUR netscli-gui-bin in parallel on every tagged
release (#54), with manifest templates in PR #53.
Workspace bumped 0.2.4 -> 0.2.5. CHANGELOG covers the user-visible
items; full PR list links from the release notes.
* release: v0.2.6 fix in-app version display + Tauri capabilities + refactor wave (#69)
Cuts v0.2.6 carrying the post-v0.2.5 work that accumulated on main
plus a real bug a Winget moderator caught.
Headline fix:
- App.tsx had a stale 'const APP_VERSION = "0.1.0"' that powered
both the bottom-bar version readout and the About dialog. Was
never bumped alongside package.json / tauri.conf.json / Cargo.toml,
so v0.2.4 users saw '0.1.0' in the GUI even though Windows
registry / AppsAndFeatures correctly read '0.2.4'. Caught in
microsoft/winget-pkgs#368471.
Fix wires APP_VERSION to package.json at build time via Vite's
'define' block. ts adds a '__APP_VERSION__' global declared in
src/types/globals.d.ts. Builds verified locally: bundle now
contains '0.2.6' as the substituted value.
Also carrying since v0.2.5:
- Tauri capabilities for window controls (#62 — title-bar buttons
on Windows finally work)
- 5 refactor PRs (#63-#67) — main.rs 1870 -> 527 lines, App.tsx
1480 -> 931 lines, tui/state.rs 2226 -> 1349 lines split into 8
focused module files
- CI minute-spend cut ~70% per PR (#61)
Workspace bumped 0.2.5 -> 0.2.6. CHANGELOG entry summarizes the
user-visible fix and links the internal refactors. site/version
also bumped.
* Polish GUI release readiness
Squashes the GUI refresh/professionalization, docs, release validation, Winget guidance, and security/engineering cleanup work from PR #87.
* build(deps): bump astro from 6.1.9 to 6.1.10 in /site
Squash-merges the clean site Astro dependency update after local combined validation passed.
* build(deps): bump devalue from 5.7.1 to 5.8.1 in /site
Squash-merges the clean site devalue dependency update after local combined validation passed.
* Land v0.3.0 baseline: GUI update-check CSP, DNS privacy gap, doc drift (#123)
First of the stacked v0.3.0 release PRs; carries the release/v0.3.0 checkpoint baseline (site redesign, GUI workspace, core/MCP ops) plus PR1's fixes. Later PRs in the stack (#124-#130) clear the maintainability debt this baseline introduces.
* Decompose starlight.css and global.css into per-concern files (#125)
starlight.css (4,965 lines) was an append-only chronological QA-fix
history rather than a topic-organized stylesheet: cascade behavior for
many selectors depends on which pass came later in file order, not on
topic grouping, and this repo has no visual-regression tooling to
safely verify a full @layer-based reorganization. Given that, this
does a verified lossless, order-preserving split instead:
- Removes one confirmed-safe duplicate: the second "Absolute final
mobile overrides. These must remain at EOF" block was a
byte-identical subset of the first occurrence; diffed to confirm
removing it changes nothing computed.
- Splits the remaining content at its existing natural boundaries
(section comments, or blank-line rule boundaries within the one
1,080-line uncommented span) into 28 numbered, named files under
site/src/styles/starlight/, each under the maintainability cap
except one 315-line cohesive close-out pass (granted a small
transition exception rather than fragmented).
- astro.config.mjs's customCss lists all 28 files in the original
relative order, with a comment warning against reordering without
checking cascade dependencies first.
global.css (1,370 lines) already had clear per-section comments
mapping to landing components. Verified via grep across every
template (including site.ts's dynamically injected set:html content)
which selectors are single-owner vs. genuinely shared before moving
anything:
- Hero/Surfaces/Faq/Footer/Install/Nav/404/changelog-exclusive
sections move into new <style is:global> blocks in their owning
component, preserving original relative order where a selector
(.install-grid) is intentionally redefined between two sections.
- is:global is used deliberately: some selectors target set:html
content, which doesn't receive Astro's scope-hash attribute, so a
scoped style would silently fail to match it.
- Shared primitives (tokens, .w/.lead/buttons/copy-button/lightbox,
the generic `section` element rule) stay in global.css, now 149
lines.
- Noted but did not remove apparent dead CSS (.install-card/-label/
-hint, .btn/.btn-w/.btn-o) found during the audit — separate
cleanup, different risk profile than relocation.
Verified: node scripts/check-file-size.mjs (both files no longer
listed), astro check (136 pre-existing errors unrelated to this
change, confirmed identical on release/v0.3.0), astro build (clean,
14 pages), test:a11y (2 pre-existing markup-only violations,
unrelated since no HTML changed), and live rendering checks via the
preview browser at desktop and mobile widths: zero console errors,
zero failed requests, computed styles/colors/widths matched expected
values on nav/hero/surfaces-section/footer/sidebar-pane/main-pane/
main-frame.
A true topic-based @layer reorganization of starlight.css is flagged
as follow-up work requiring dedicated visual-regression tooling.
* Decompose site content/scripts and fix broken changelog rendering (#126)
Fixes a real production bug found while decomposing changelog-page.ts:
changelog.astro's <script define:vars> tag contained a relative
`import`, but Astro's define:vars scripts aren't run through Vite's
import resolution, so it 404s at runtime. The release list has been
silently stuck on "Loading release notes..." with no console error.
Fixed by splitting into two script tags: a define:vars script that
only stashes server-computed data on window, and a plain <script>
(mirroring Header.astro's already-working pattern) that imports
normally and reads the data back off window. Verified fixed live: the
release list and timeline now render real content with zero console
errors.
- Delete the dead site/src/scripts/docs-header.ts (its initDocsHeader
had drifted from Header.astro's live inline script with unshipped
features - mobile section-nav repositioning, an anchor-link copy
enhancer) and replace it with a typed, verbatim extraction of
Header.astro's actual live script, imported via a plain <script>.
Verified via a live scroll-event test that data-docs-scrolled still
updates correctly. The unshipped features are not resurrected here -
promoting them would be a behavior change beyond decomposition scope.
- Split site/src/data/site.ts (517 lines) into
site/src/data/site-content/{types,version,meta,hero,surfaces,install,
faq,footer}.ts, reassembled by a thin site.ts all existing consumers
still import unchanged. Verified via a side-by-side build comparison
against the pre-split baseline. Deliberately kept FAQ's `a` and
`aHtml` both explicitly authored rather than deriving `a` from
`aHtml`: `a` is embedded verbatim into JSON-LD structured data, and
some `aHtml` entries contain structural markup that would garble the
derived plain text.
- Split site/src/scripts/changelog-page.ts (737 lines) into
changelog/{types,markdown-inline,summarize,markdown-block,timeline,
release-list}.ts with real types throughout.
- Delete the orphaned scripts/split-site-css.mjs and
scripts/patch-site-scripts.mjs (patch-site-scripts.mjs is in fact the
source of the changelog script-tag bug above).
- Wire check-file-size.mjs and test:a11y into site.yml. Before wiring
in the a11y gate, ran it and fixed 2 real pre-existing violations
that would have made it immediately red: scrollable-region-focusable
on JS-created table-scroll wrappers and a static code block (add
tabIndex/role/aria-label), and a missing accessible name on the
logo-only nav link (add aria-label). Re-verified 0 violations across
all 7 routes after fixing.
Fourth in the sequenced audit-remediation batch.
* Revert site/ to pre-v0.3.0 redesign state (roll back accidental deploy) (#135)
* Revert site/ to pre-v0.3.0 redesign state (2026-06-02)
The docs section, redesigned landing page, and all related CSS/content
decomposition work were developed on branches that only ever targeted
release/v0.3.0, so pages.yml (which deploys on push to main) never
ran against any of it. Merging the v0.3.0 release PRs into main this
week caused pages.yml to deploy this unfinished work to netscli.com
for the first time - none of it was ready to ship.
This reverts site/ to the tree at 63b3e5f ("Polish GUI release
readiness", 2026-06-02), the last commit that was actually live
before any of this work landed. Nothing outside site/ is touched.
Once the docs/redesign work is ready, it should be redeployed
deliberately rather than as a side effect of an unrelated merge.
* Add transition exceptions for site.ts/global.css reverted to monolithic form
* Make site a11y CI step skip gracefully when test:a11y is absent
site/ is temporarily reverted to a pre-redesign state that predates
the test:a11y script; --if-present avoids a hard CI failure until the
redesign (and its a11y tooling) is actually ready to ship.
* Restore the site redesign, and add desktop install routes for all three platforms (#159)
* Restore and harden site redesign
* Add desktop app install routes for all three platforms
The "Get started" section listed only CLI install methods on every OS
tab. The desktop app — which is what most visitors landing on the page
actually want — was reachable only from the hero's dropdown or a single
trailing sentence pointing at the releases page.
Each OS panel now has two labelled groups, desktop first:
Windows winget install fstubner.netscli.gui / Scoop / .msi
macOS brew install --cask / Apple Silicon .dmg / Intel .dmg
Linux yay -S netscli-gui-bin / .AppImage / .deb
All four package-manager routes were verified live against their
registries before being published here, and all five direct download
URLs return 200 against the latest release.
Notes on the specific commands:
- The macOS cask is fully qualified (`fstubner/tap/netscli`) and passes
--cask because the tap holds a Formula and a Cask sharing the token
`netscli`, so a bare `brew install netscli` is ambiguous.
- The .dmg and .msi rows carry a hint that the installers are unsigned.
Better to say so here than to let someone hit an unexplained
Gatekeeper or SmartScreen dialog and assume the download is malware.
Supporting changes:
- InstallEntry gains an optional `href`, so an entry can be a direct
download rendered as a link rather than a copyable command. Command
and download rows are visually consistent but only commands get a
copy button.
- byPlatform is now Record<Platform, PlatformInstall> with `cli` and
`desktop` arrays, keeping the existing "position 0 is recommended"
convention within each.
- The footer note no longer has to carry the desktop installers, so it
now points at checksum/cosign verification instead.
Also fixes the hero download button handing every Mac visitor the Apple
Silicon build — an arm64 .dmg does not run on Intel. It now defaults to
the Intel build, which Rosetta 2 runs on Apple Silicon too, and upgrades
to arm64 via getHighEntropyValues(). The previous code read
`userAgentData.architecture` directly, which is always undefined: it is
a high-entropy hint only available through that async call.
Verified: astro check (0 errors), build, file-size guard, axe on all 7
gated routes (0 violations), no horizontal overflow at 375px, OS tab
switching keeps aria-selected/hidden in sync, and no copy buttons
attached to download links.
* Fix light-theme contrast, and make the a11y gate test both themes
CI caught 14 color-contrast violations on every docs page that a local
run reported clean. The gate was environment-dependent: Starlight picks
its theme from `prefers-color-scheme`, so it only ever tested whichever
one the runner's Chrome preferred. A developer on a dark-mode OS got a
green check while CI (Ubuntu, no preference, therefore light) failed.
That is how a light-theme contrast bug reached a branch whose handover
notes recorded "0 violations on every route".
Two fixes:
- `--sl-color-gray-3` in the light theme was #718096, which is 3.88:1 on
--sl-color-bg (#fbfbfb) — under the 4.5:1 WCAG AA floor for
normal-size text. It is the secondary/small-text colour for the docs
footer, built-with row, and mobile section labels, much of it at 12px.
Now #667387, which is 4.65:1. The dark theme has its own gray-3 and is
untouched.
- a11y.mjs now runs axe twice, once per theme, and fails if either does.
Chrome has no "force light" switch (light is the default with no OS
preference), so the light pass is the plain run and the dark pass adds
--force-dark-mode. Also adds an 'error' handler on the spawn: without
it a missing axe binary left the promise pending and hung the job
until the CI timeout rather than failing.
Verified: all 15 elements axe flagged now clear their threshold in the
light theme, and the full gate passes 7 routes × 2 themes locally.
* Pin the a11y check to the runner's matched chromedriver
The check started failing with "session not created: This version of
ChromeDriver only supports Chrome version 151 / Current browser version
is 150" on both theme passes. Nothing about the site changed: the
`chromedriver` npm package that `@axe-core/cli` depends on downloads
whatever is newest at install time, so the day ChromeDriver 151 shipped
it stopped matching the Chrome on the runner image.
GitHub's Ubuntu images ship a Chrome and a ChromeDriver that are already
matched and set CHROMEWEBDRIVER to the directory holding the latter. Use
it when present via --chromedriver-path, and otherwise fall back to
whatever axe resolves itself, which is what should happen locally — this
machine has Chrome 151 and chromedriver 151, so the env var is unset and
the flag is not passed.
No workflow change needed; CHROMEWEBDRIVER is already exported by the
runner. A11Y_CHROMEDRIVER_PATH overrides it, and a path that does not
exist warns and falls back rather than failing.
* Document how to get packet capture, and every publish channel (#163)
Two documentation defects about the same subject, plus the per-channel
publishing reference that did not exist.
## Packet capture was promised but never explained
The site referenced packet capture across 14 files, including a six-step
desktop workflow, while `grep -rniE "NETSCLI_PCAP|--features pcap|-pcap"
site/src/` returned zero matches. It is a compile-time feature and every
published artifact is built without it:
- release.yml:212 — GUI installers deliberately omit --features pcap
- apps/netscli-cli/Cargo.toml:38 — default = []
So every visitor installing from netscli.com got a build where the
documented workflow cannot run, with no explanation and no remedy.
install.md now leads with that fact and gives the three routes to a
capture-capable build (NETSCLI_PCAP=1 install script, the -pcap release
assets, or --features pcap from source), notes there is no desktop
installer with capture at all, and lists the per-platform runtime
requirements.
packet-capture.md gains a caution at the top pointing at it.
Also fixes the capability check. The docs told users to run
`netscli pcap --check`, but args.rs:226 gates the whole Pcap subcommand
behind #[cfg(feature = "pcap")] — on a standard build clap reports an
unrecognized subcommand rather than anything useful. `netscli doctor`
works on every build and is now the documented check.
## PUBLISHING.md covered one channel of six
It was crates.io only. It now documents all six — crates.io, GitHub
Releases, Homebrew, Scoop, winget, AUR — with, per channel: what
publishes, which of the nine jobs does it, where it deploys, what secret
it needs, and the failure modes worth recognising. Adds a troubleshooting
table mapping each error the publish scripts can emit to its cause, and
states the split: RELEASE.md is the process, PUBLISHING.md is the
per-channel reference.
Records two things that were only tribal knowledge: the Homebrew Formula
and Cask share the token `netscli` so a bare `brew install netscli` is
ambiguous, and the winget version directories are keyed by name so
editing one to hold another version's content files a conflicting
duplicate.
## RELEASE.md corrections
- "five distribution channels" -> six.
- Self-contradiction: claimed "5 jobs, one per channel" and then
described four more GUI jobs. It is nine.
- Asset counts were wrong and I propagated them before checking
against the workflow: the matrices yield 11 CLI assets and 5 GUI
installers, not 13 and 4. The Linux GUI entry emits both .deb and
.AppImage from one matrix row.
- "Action pin policy" still said the actions were tag-pinned with SHA
pinning "on the radar". They were SHA-pinned in #158; rewritten to
describe the current state and how to bump one by hand.
- Stale v0.2.1/PR #30 example replaced with a pointer to the
seven-file version-bump list.
- `brew install --cask netscli` -> the unambiguous fully-qualified form.
Verified: astro check 0 errors, site builds, file-size guard passes, the
/docs/install/#packet-capture anchor resolves, and every count above was
read back out of the workflow YAML rather than copied from the old docs.
* Add per-PR site previews on Cloudflare Pages (#165)
Reviewing a site change currently means building it locally or deploying
to production. This adds a preview URL per PR.
Production is untouched. netscli.com stays on GitHub Pages via pages.yml,
which remains manual-only after the accidental-deploy incident. This uses
a separate Cloudflare Pages project and always passes an explicit
`--branch=pr-<N>`, which Cloudflare treats as a preview; there is no code
path in the workflow that produces a production deployment.
Two things about preview builds are not obvious and would have caused
real damage:
- A preview is still a *production* Astro build, so
`import.meta.env.PROD` is true and the Cloudflare Web Analytics
beacon fires. Every PR deploy would have reported into netscli.com's
real analytics property, quietly polluting the numbers with CI.
- Preview URLs would be crawlable.
NETSCLI_PREVIEW=1 turns both off, and the workflow verifies both before
deploying rather than trusting the build — it fails the job if any page
lacks the robots meta or if the beacon is still present.
Getting the noindex complete took two passes. The first only covered
src/layouts/Page.astro, which is the landing, changelog and 404 pages —
the docs are rendered by Starlight's own layout, so 11 of 14 pages were
still indexable. Starlight's `head` config now injects the same tag.
The verification step is what caught that, which is the argument for
having it.
Also fixes the 404 being indexable and self-canonicalised (finding B-25
from the assessment). The `noindex` prop the preview mode needed made it
a one-line change, so it is set explicitly there rather than only in
previews.
The workflow runs today and reports "not configured" until
CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID exist, so it does not fail
every PR in the meantime. Fork PRs are skipped, since secrets are not
exposed to them and the job would otherwise fail on an empty token.
docs/PUBLISHING.md covers the one-off project creation and both secrets.
Verified: preview build noindexes 14/14 pages with no beacon; normal
build unchanged at 1/14 (the 404) with the beacon intact; astro check
0 errors; file-size guard passes.
* Fix sitemap duplication and widen the a11y gate to every route (B-24, B-26, B-38) (#174)
* Fix site sitemap duplication and widen the a11y gate to every route
B-24: three sitemaps shipped -- a hand-written public/sitemap.xml passed
through to dist/ with all 13 lastmod values frozen at 2026-07-02, plus the
sitemap-index.xml and sitemap-0.xml that @astrojs/sitemap generates on
every build. robots.txt pointed at the stale hand-written one, so the
always-accurate generated index was never referenced. Deleted the
hand-written copy and repointed robots.txt.
B-26: the a11y gate hard-coded 7 of 14 routes. The seven it missed --
/docs/cli/, /mcp/, /tui/, /operations/, /packet-capture/, /core-library/
and /result-model/ -- hold the heaviest table markup, which docs-header.ts
then wraps at runtime on every docs page. Routes are now discovered from
the build output, so pages cannot be added without being covered. Falls
back to the old list if dist/ is missing, because an empty route list
would otherwise pass silently.
Ran it: all 14 routes are clean in both light and dark themes. The seven
newly covered pages had no violations -- they are simply gated now.
B-38: three whole-document MutationObservers plus scroll/resize listeners
were never disconnected. Today that only costs memory, since the module
runs once per full page load. It becomes a real leak the moment view
transitions are enabled, because initDocsHeader re-runs on
astro:page-load and would stack a second full set on the first, each with
rAF callbacks that themselves mutate the DOM. Teardown is now registered
against astro:before-swap and init is re-entrant, so enabling
<ClientRouter /> later is a one-line change rather than a debugging
session.
astro check: 42 files, 0 errors.
* Move the docs-header teardown registry into its own module
The teardown added for B-38 pushed docs-header.ts to 379 lines, past its
360 transition cap. I did not run the size gate locally before pushing --
CI caught it.
The registry is a coherent unit on its own, so it moves out rather than
the file being trimmed to fit.
* Fix callout contrast and stop the a11y gate scanning the wrong server
Widening the gate to all 14 routes immediately found a real bug on the
newly covered /docs/packet-capture/: `code` and `a` inside a Starlight
aside inherited the global mint accent, which is tuned for contrast
against the #111 page background rather than a callout's own coloured
one. On the amber caution variant that measured 1.53:1 for code and
3.53:1 for links, against a 4.5:1 AA threshold. They now use
--sl-color-asides-text-accent, which Starlight sets per variant, so
note/tip/danger stay correct too. Measured after: 6.77:1 and 8.34:1.
The rule needs its own file loading last, rather than sitting in
05-docs-surfaces.css where it belongs, because earlier files declare
`.sl-markdown-content a` and `:not(pre) > code` with !important and
silently override it. That is the B-23 tax in miniature: placement is
load-order dependent and the cascade cannot be reasoned about locally.
Two harness flaws found while fixing it, both of which made a green local
run meaningless:
- ensurePreview() reused whatever answered on port 4322. A long-running
`astro dev` server also listens there and serves from source with
different CSS processing, so the gate scanned something other than
dist/ -- it passed locally while CI failed. It now refuses to reuse an
existing server unless A11Y_REUSE_SERVER=1 says it really is the build.
- Without `headless`, axe launches a headed Chrome that takes its colour
scheme from the OS and ignores --force-dark-mode, so both passes
rendered the same theme and one was never tested.
Verified the gate genuinely catches this now: with the CSS reverted it
reports the 4 violations locally, matching CI exactly. It did not before.
* Beat the theme-scoped !important rules that were overriding the fix
The callout rule was still losing in the light theme. Earlier files declare
html[data-theme="light"] .sl-markdown-content :not(pre) > code
with !important -- specificity (0,2,3) -- while the obvious selector here is
only (0,2,2), so it lost despite loading later. Repeating .starlight-aside
takes this to (0,3,2), which outranks it on class count without depending on
data-theme being present.
Also switched from --sl-color-asides-text-accent to inheriting the aside's
own text colour. The accent measured only 4.53:1 in light -- passing, but
with so little margin that a rounding difference between axe versions could
flip it. Inheriting gives 14.96:1 light and 9.18:1 dark, and cannot drift
when a token changes.
Measured in both themes by forcing data-theme directly rather than trusting
Chrome flags, after two local runs had already misled me.
* Close the packaging, tooling and documentation long tail (B-33, B-34, B-37, C-06/18/19/20/21/22/23/24/27/28/33/35) (#176)
* Close the packaging, tooling and documentation long tail
B-37: Windows adapter prefixes were matched by family only -- the first
IPv4 prefix on the adapter was applied to every IPv4 address on it. A
multi-homed adapter (192.168.1.5/24 alongside 10.0.0.5/8) therefore
reported one of its addresses with the wrong mask, and anything deriving a
subnet from that scanned the wrong range. Now picks the longest prefix that
actually contains the address, ignores a /0 default entry, and falls back
to a host route rather than borrowing an unrelated mask.
That function is Windows-only, so the Linux and macOS runners never
compile it. The selection rule is pure, so it is mirrored as a testable
function that runs on every platform, plus a Windows-only test asserting
the mirror still agrees with the real one.
B-33: dev PowerShell scripts defaulted the Npcap SDK path to C:\tmp, which
is predictable and not ACL'd -- a planted Lib\x64\wpcap.lib there passes
the existence check and links into the developer's build. Defaults to
LOCALAPPDATA now. docs/RELEASE.md steers maintainers through these during
the release gate.
B-34: `cargo install tauri-driver --locked` was unpinned. --locked honours
that crate's lockfile but says nothing about which version is installed,
so it floated to whatever was newest at job time -- an unreviewed
dependency bump on the job that drives the real app.
C-21/C-22: `tsc` does not build project references, so tsconfig.node.json
-- covering vite.config.ts -- was never typechecked. Switching to `tsc -b`
immediately found a real error: the vitest `test` block added earlier is
not valid against vite's UserConfig. Fixed by importing defineConfig from
vitest/config. Also redirected the composite build's output to
node_modules/.tmp so it stops emitting vite.config.js beside the source.
Added ESLint (there was none, which AGENTS.md acknowledged), scoped to the
rules tied to bugs this project has shipped -- react-hooks above all, since
A-13, B-16 and B-18 were all stale closures or wrong dep arrays. Wired into
CI. First run over a never-linted codebase: 2 errors, 3 warnings. Both
errors fixed; the warnings are deliberate dep arrays and stay visible.
C-06: parse_file kept walking a capture after max_packets purely to finish
the count. Stopping early outright would have under-reported the "N
packets" the GUI shows, so the count now runs to completion for any
realistic file and only gives up past a 10M ceiling, where `truncated`
already marks the figure as a floor.
C-23: the core README example did not compile -- wrong arity, wrong method
name, and `?` on an Option in an anyhow fn. Rewritten and verified by
extracting the block and compiling it verbatim against the pinned
toolchain.
C-24: `NETSCLI_PCAP=1 curl ... | bash` scopes the variable to curl, so bash
never saw it and the non-pcap build was installed silently. Demonstrated
the old form leaves it unset and the new form does not.
C-18: the Windows install docs offered strictly fewer paths than the
landing page -- no Scoop, no install.ps1.
C-19: the dependency diagram omitted the CLI -> MCP edge, which is what
makes `netscli serve` work, and showed all three as siblings.
C-20: index.html referenced /vite.svg, but there is no public/ dir, so the
webview 404'd on every load.
C-27: target-pcap/ (multi-GB, created by the documented Windows workflow)
was not ignored.
C-28: `*.csv` was repo-wide and would swallow legitimate fixtures; scoped
to the root. Removed the dead negation below `/*.png`, which never matched
anything since that pattern is root-only.
C-33: stale example versions (0.1.1/0.1.2) replaced with placeholders so
they cannot drift again.
C-35: packaging/README described the sidecar as the source of the hash,
which was circular; the scripts re-hash the downloaded asset now.
B-29, B-30, B-32 were already fixed in earlier work; verified rather than
assumed.
* Untrack the working notes that git add -A swept in
HANDOVER.md and the two assessment documents are deliberately untracked
working notes, not repo content. A blanket `git add -A` in the previous
commit staged them.
Removed from the index only -- the files stay on disk untouched.
* Remove provably dead declarations from the site CSS stack (B-23) (#177)
B-23: 29 stylesheets, 4,945 lines, 1,219 !important, resolved purely by
load order -- "no rule can be changed by editing the file that appears to
own it". That is not something to rewrite by hand; a blind consolidation
of 4,900 interdependent lines is how a site breaks silently.
So this takes the provably safe half first. scripts/css-shadowing.mjs
parses every stylesheet in load order and finds declarations that can
never take effect, because a later rule with the identical selector, the
identical media context, and at least equal importance always wins. It is
deliberately conservative -- it never reasons about whether one selector
subsumes another -- so it undercounts, which is the right direction.
scripts/css-prune.mjs deletes those and drops any rule left empty.
Result: 163 declarations and 46 rules removed. 4,945 -> 4,670 lines,
1,219 -> 1,109 !important.
Verified, not assumed: computed styles for 37 properties were captured
for every element across 8 structurally distinct pages in both themes,
before and after -- 8,774 element-theme pairs. Zero differ. astro check
clean, and the a11y gate passes on all 14 routes in both themes.
That verification earned its keep. The first attempt removed 370
declarations and the comparison found a real regression: the search
dialog's mobile cancel button lost its justify-content. The analysis was
right; the pruner was wrong -- it matched declaration text file-wide, so
`justify-content: center` was deleted from the first rule containing that
text rather than from the rule it was flagged in. It is now scoped to the
exact rule body, and refuses rather than guessing when it cannot locate
one. That refusal is why the count dropped from 370 to 163: the analyser
strips comments, so a rule containing one no longer matches the source.
What is left: 207 further declarations are provably dead but currently
unreachable by the pruner (comment-bearing rules), and the structural
problem itself -- the chronological "-closeout / -final / -guardrails"
file naming, and the ~1,100 remaining !important -- is untouched. That
part needs design judgement rather than a tool, and is safest done once
the site's shipping status is settled. Both scripts stay in the repo so
the measurement can be repeated.
* Fix the mobile menu, the copy button covering commands, the lightbox, the dead skip link, and every contrast failure (#190)
* Make the screenshot lightbox work, and point the skip link at something
Two defects an independent review found, both confirmed live in the browser
before and after.
1. The lightbox had no styling at all.
`src/scripts/landing/lightbox.ts` was complete — focus trap, Escape,
backdrop click, focus restore, full ARIA — but no rule for any of its
classes existed in the codebase. So the element it appends to <body>
rendered as a plain static block: measured on the running site at 1270 x
51px, at y=5167 of a 5234px document, showing a stray "×" and an empty
caption. Clicking a screenshot set the image source and moved focus,
which scrolled the visitor to the bottom of the page rather than opening
anything. It also sat permanently in the a11y tree as a visible
role="dialog" aria-modal="true".
The CSS is written rather than the feature deleted, because the
behaviour was already finished and correct. It has to be global: the
element is created by JS and appended to <body>, so Astro's scoped-style
hashing can never match it.
Verified on the running site — closed: display:none, 0x0, and the
document is 67px shorter. Open: fixed, covers the viewport, z-index above
the skip link, body scroll locked, image fits the viewport. Full keyboard
round trip passes — focus a screenshot, Enter ope…
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.
Bumps the tauri-js group with 2 updates in the /apps/netscli-gui directory: @tauri-apps/api and @tauri-apps/cli.
Updates
@tauri-apps/apifrom 2.11.0 to 2.11.1Release notes
Sourced from @tauri-apps/api's releases.
... (truncated)
Commits
6f6ab12apply version updates (#15409)728c8d4fix(cli): skip building bundles when usingtauri android run(#15473)e25f45crefactor: remove impl clone on inner menus (#15553)fbcf1b0chore(deps): update dependency eslint-plugin-security to v4.0.1 (#15545)828f710fix(cli): respect src/bin required-features (fix: #15325) (#15427)ed8fd41chore(cli): lesser verboseureq_protolog (#15552)50b0237fix(android): escape special characters instrings.xml(#15549)800223ddocs: fix some missing and wrong docs (#15548)5075c81fix: checkis_maximizableininternal_toggle_maximize(#15550)532c22achore(deps-dev): bump vite from 8.0.5 to 8.0.16 (#15547)Updates
@tauri-apps/clifrom 2.11.2 to 2.11.4Release notes
Sourced from @tauri-apps/cli's releases.
Commits
8909f22apply version updates (#15598)67ffa19fix(bundler): make .desktop and .DirIcon relative symlinks (#15596)c5c8b2bdocs(readme): desktop platforms -> operating systems, closes #1558980d437ffix(#15580): bump memmap2 (#15587)469ecc8test: regenerate stale macOS acl snapshot for has_app_acl (#15576)5be0cb8fix(ci): change strip to debuginfo for cloudflare worker (#15574)4bbd497chore(deps): bump cloudflare worker versions (#15573)5712549chore(deps): update dependency rollup to v4.62.0 (#15551)c8faedfci: test on node 20,22 instead of node 18,203641e26refactor: simplify menu code (#15559)