From 4912c60535682bc6d79011e9daa1f7e67bcd16ad Mon Sep 17 00:00:00 2001 From: Felix Stubner Date: Sat, 12 Sep 2026 23:02:31 +0100 Subject: [PATCH 1/3] Only claim a download total once both sources have reported renderDownloads summed GitHub release assets and crates.io but wrote the label as soon as either resolved, so "total" was asserted before it was true. Measured on the running site: the same page showed "Downloads: 178 total" with only crates.io in, and "Downloads: 2,635 total" once the GitHub count landed -- a fifteenfold difference under one label. Track whether each source has settled separately from its count: a rate-limited source never sets a value, so gating on the counts alone would leave the label waiting forever. Settle in .finally so a failed or rate-limited fetch settles too. Until both are in, the figure is a lower bound and says so. Verified in both directions by forcing the crates.io fetch to fail: partial renders "2,458 so far; one source has not reported", complete renders "2,636 total". --- site/src/scripts/landing-page.ts | 38 +++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 6 deletions(-) diff --git a/site/src/scripts/landing-page.ts b/site/src/scripts/landing-page.ts index 88b590a..10075ac 100644 --- a/site/src/scripts/landing-page.ts +++ b/site/src/scripts/landing-page.ts @@ -80,13 +80,30 @@ export function initLandingPage(repo: string): void { never resolves. */ let githubDownloads: number | null = null; let cratesDownloads: number | null = null; + // Whether each source has finished, which is not the same as whether it + // produced a number. A rate-limited source never sets its count, so + // gating on the counts alone would leave the label waiting forever. + let githubSettled = false; + let cratesSettled = false; const renderDownloads = () => { if (githubDownloads === null && cratesDownloads === null) return; const total = (githubDownloads ?? 0) + (cratesDownloads ?? 0); + // "total" is a claim about both sources, so only make it once both + // have reported. Measured on this page: "Downloads: 178 total" with + // only GitHub in, then "Downloads: 2,635 total" once crates.io + // landed -- same label, same page, a fifteenfold difference. Until + // both are in, the number is a lower bound and now says so. + const complete = githubSettled && cratesSettled + && githubDownloads !== null && cratesDownloads !== null; const el = document.getElementById("downloads"); if (el) { - el.textContent = `${fmtDownloads(total)} ${total === 1 ? "download" : "downloads"}`; - el.dataset.totalDownloads = `Downloads: ${fmt(total)} total`; + // fmtDownloads already appends "+" above 1000; don't double it. + const shown = fmtDownloads(total); + const text = complete || shown.endsWith("+") ? shown : `${shown}+`; + el.textContent = `${text} ${total === 1 ? "download" : "downloads"}`; + el.dataset.totalDownloads = complete + ? `Downloads: ${fmt(total)} total` + : `Downloads: ${fmt(total)} so far; one source has not reported`; el.setAttribute("aria-label", el.dataset.totalDownloads); el.hidden = false; } @@ -102,9 +119,15 @@ export function initLandingPage(repo: string): void { const n = d?.crate?.downloads; if (typeof n !== "number") return; cratesDownloads = n; - renderDownloads(); }) - .catch(() => {}); + .catch(() => {}) + // `finally`, not the success path: a failed or rate-limited fetch + // settles this source too, and the label has to know that to stop + // withholding the word "total". + .finally(() => { + cratesSettled = true; + renderDownloads(); + }); fetch(`https://api.github.com/repos/${repo}/releases?per_page=100`) .then((r) => (r.ok ? r.json() : null)) @@ -118,7 +141,6 @@ export function initLandingPage(repo: string): void { ), 0, ); - renderDownloads(); const latest = releases.find((release) => !release.draft && !release.prerelease) ?? releases.find((release) => !release.draft); const versionEl = document.getElementById("latest-version"); @@ -130,7 +152,11 @@ export function initLandingPage(repo: string): void { refreshMetricSeparators(); } }) - .catch(() => {}); + .catch(() => {}) + .finally(() => { + githubSettled = true; + renderDownloads(); + }); })(); initCopyButtons(); From 84b2160aae48814477fe3885f4567a8b52e32b2f Mon Sep 17 00:00:00 2001 From: Felix Stubner Date: Sat, 12 Sep 2026 23:02:32 +0100 Subject: [PATCH 2/3] Use the short winget identifiers in the install docs The landing page and FAQ said `winget install netscli`; the install docs said `winget install fstubner.netscli`. Both resolve, so this was inconsistency rather than error, and the short form is what the landing page leads with. The full identifiers stay documented in one line, because a bare name match can become ambiguous if another netscli ever enters the catalog and `fstubner.netscli` cannot. --- site/src/content/docs/docs/install.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/site/src/content/docs/docs/install.md b/site/src/content/docs/docs/install.md index 821a28b..3b16e49 100644 --- a/site/src/content/docs/docs/install.md +++ b/site/src/content/docs/docs/install.md @@ -9,8 +9,8 @@ NetsCLI publishes command-line binaries and desktop installers through GitHub Re | Platform | Recommended path | Installs | | --- | --- | --- | -| Windows | `winget install fstubner.netscli` | CLI and TUI | -| Windows | `winget install fstubner.netscli.gui` | Desktop app | +| Windows | `winget install netscli` | CLI and TUI | +| Windows | `winget install netscli-gui` | Desktop app | | macOS | Homebrew or install script | CLI and TUI | | Linux | Install script, Homebrew, AUR, or release artifact | CLI and TUI | | Rust users | `cargo install netscli` | CLI and TUI from crates.io | @@ -20,15 +20,19 @@ NetsCLI publishes command-line binaries and desktop installers through GitHub Re Use winget for the hash-verified install path: ```powershell -winget install fstubner.netscli +winget install netscli ``` The desktop app is distributed separately: ```powershell -winget install fstubner.netscli.gui +winget install netscli-gui ``` +Both short names resolve today. The full identifiers are `fstubner.netscli` +and `fstubner.netscli.gui`, and they cannot become ambiguous — use those if a +short name ever matches more than one package in the catalog. + Scoop is also supported, for both the CLI and the desktop app: ```powershell From 622bba739907f00e6a9a387ee6899e1e6fd038d0 Mon Sep 17 00:00:00 2001 From: Felix Stubner Date: Sat, 12 Sep 2026 23:02:32 +0100 Subject: [PATCH 3/3] Bump chromedriver to 152 to match the installed Chrome visual:check drives Chrome through chromedriver, and the pinned 151 crashes against Chrome 152 with a GetHandleVerifier stack trace rather than failing cleanly -- so the gate reported nothing at all. NOT verified locally: the sandbox here records the lockfile change but discards the postinstall binary download, leaving chromedriver.exe at 151 on disk. The bump is correct for a clean `npm ci`; it has not been proven to fix the crash on this machine. --- site/package-lock.json | 1 + site/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/site/package-lock.json b/site/package-lock.json index 8e99dbe..cc664aa 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -12,6 +12,7 @@ "devDependencies": { "@astrojs/check": "^0.9.10", "@axe-core/cli": "^4.13.0", + "chromedriver": "^152.0.3", "linkedom": "^0.18.13", "selenium-webdriver": "^4.48.0", "typescript": "^5.5.0" diff --git a/site/package.json b/site/package.json index 2d6cf19..11b3466 100644 --- a/site/package.json +++ b/site/package.json @@ -28,6 +28,7 @@ "devDependencies": { "@astrojs/check": "^0.9.10", "@axe-core/cli": "^4.13.0", + "chromedriver": "^152.0.3", "linkedom": "^0.18.13", "selenium-webdriver": "^4.48.0", "typescript": "^5.5.0"