Repository navigation
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
Merged
Conversation
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
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.
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/24alongside10.0.0.5/8reported 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
/0default 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 —
tscdoesn't build project references, sotsconfig.node.json(coveringvite.config.ts) was never typechecked. Switching totsc -bimmediately found a real error: the vitesttestblock added in an earlier PR isn't valid against vite'sUserConfig. Also redirected the composite build's output so it stops emittingvite.config.jsbeside the source.C-22 — no ESLint config or script existed at all.
react-hooksis 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
C:\tmp, which is predictable and not ACL'd; a plantedwpcap.libthere passes the existence check and links into the build. NowLOCALAPPDATA.cargo install tauri-driver --lockedwas unpinned.--lockedhonours that crate's lockfile but not which version installs, so it floated to newest-at-job-time.Correctness and docs
parse_filewalked a multi-GB capture aftermax_packetsjust 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, wheretruncatedalready marks it a floor.?on anOption). Rewritten and verified by extracting the block and compiling it verbatim against the pinned toolchain.NETSCLI_PCAP=1 curl … | bashscopes the variable tocurl, sobashnever saw it and the non-pcap build installed silently. Demonstrated: old form →unset, new form →1.netscli servelook impossible, a 404'd/vite.svg, an unignored multi-GBtarget-pcap/, a repo-wide*.csvplus 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.