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" 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 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();