Skip to content

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

Merged
fstubner merged 2 commits into
mainfrom
fix/packaging-and-docs
Aug 15, 2026
Merged

fstubner merged 2 commits into
mainfrom
fix/packaging-and-docs

Conversation

@fstubner

Copy link
Copy Markdown
Owner

B-37 — wrong subnet mask on multi-homed adapters

Windows adapter prefixes were matched by family only, so the first IPv4 prefix on an adapter was applied to every IPv4 address on it. An adapter with 192.168.1.5/24 alongside 10.0.0.5/8 reported one of them 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 Linux and macOS runners never compile it. The selection rule is pure, so it's mirrored as a testable function that runs everywhere, plus a Windows-only test asserting the mirror still agrees with the real one — otherwise the fix would be verified on one of three platforms.

Tooling gaps that were hiding real bugs

C-21 — tsc doesn't 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 in an earlier PR isn't valid against vite's UserConfig. Also redirected the composite build's output so it stops emitting vite.config.js beside the source.

C-22 — no ESLint config or script existed at all. react-hooks is precisely the rule set that catches stale closures and wrong dependency arrays — the shape of A-13, B-16 and B-18. First run over a never-linted codebase: 2 errors, 3 warnings. Both errors fixed; the warnings are deliberate dep arrays and stay visible. Wired into CI.

Security and supply chain

  • B-33 — dev scripts defaulted the Npcap SDK to C:\tmp, which is predictable and not ACL'd; a planted wpcap.lib there passes the existence check and links into the build. Now LOCALAPPDATA.
  • B-34 — cargo install tauri-driver --locked was unpinned. --locked honours that crate's lockfile but not which version installs, so it floated to newest-at-job-time.

Correctness and docs

  • C-06 — parse_file walked a multi-GB capture after max_packets just to count. Stopping early outright would under-report the "N packets" the GUI shows, so the count runs to completion for realistic files and gives up only past a 10M ceiling, where truncated already marks it a floor.
  • C-23 — the core README example did not compile (wrong arity, wrong method, ? on an Option). 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 installed silently. Demonstrated: old form → unset, new form → 1.
  • C-18/19/20/27/28/33/35 — missing Windows install paths, a dependency diagram that made netscli serve look impossible, a 404'd /vite.svg, an unignored multi-GB target-pcap/, a repo-wide *.csv plus a dead negation, stale example versions, and a packaging doc describing circular hash verification.

Verified, not assumed

B-29, B-30 and B-32 were already fixed in earlier work — I checked rather than re-fixing them.

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.
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.
@fstubner
fstubner merged commit bf3d82e into main Aug 15, 2026
16 of 17 checks passed
@fstubner
fstubner deleted the fix/packaging-and-docs branch August 15, 2026 03:23
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…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant