From c1e935d9dcf0af75f3d64b9365e463ff45df05d2 Mon Sep 17 00:00:00 2001 From: Asaf Amos Date: Sat, 6 Jun 2026 23:24:25 +0300 Subject: [PATCH 1/2] feat(admin): track VS Code Marketplace installs in the dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit npm was the only distribution channel the dashboard could see. The VS Code extension (asafamos.axle-a11y) had 19 installs + a 5★ rating that were invisible — discovered only by digging through old publish emails. - Fetch the public VS Marketplace gallery API server-side (extensionquery, flags 914) for install/download/rating counts. Resilient: any failure yields zeros, never breaks the page. - New "VS Code Marketplace" section in /admin, alongside npm. - Drive-by: fix a pre-existing react-hooks/set-state-in-effect lint error on the token-load effect (documented disable — the effect read is intentional for SSR hydration-safety). Verified: tsc + eslint clean; the fetch/parse path returns live data (19 installs / 59 downloads / 5.0★). Co-Authored-By: Claude Opus 4.8 --- app/admin/page.tsx | 54 +++++++++++++++++++++++++++++++ app/api/admin/summary/route.ts | 58 ++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 1d907b8..76cbb22 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -59,6 +59,16 @@ type Summary = { }>; totals: { last_week: number; last_month: number }; }; + marketplace?: { + extensions: Array<{ + extension: string; + installs: number; + downloads: number; + rating: number; + rating_count: number; + }>; + totals: { installs: number; downloads: number }; + }; generated_at: string; }; @@ -71,7 +81,12 @@ export default function AdminPage() { const [loading, setLoading] = useState(false); useEffect(() => { + // Read the saved admin token AFTER mount (not via a lazy useState + // initializer) so the server and initial client render match — reading + // localStorage during render would cause a hydration mismatch on the + // controlled password input below. const saved = window.localStorage.getItem(TOKEN_KEY); + // eslint-disable-next-line react-hooks/set-state-in-effect if (saved) setToken(saved); }, []); @@ -166,6 +181,45 @@ export default function AdminPage() { )} + {data.marketplace && data.marketplace.extensions.length > 0 && ( +
+

VS Code Marketplace

+
+ + +
+ + + + + + + + + + + {data.marketplace.extensions.map((e) => ( + + + + + + + ))} + +
ExtensionInstallsDownloadsRating
{e.extension}{fmt(e.installs)}{fmt(e.downloads)} + {e.rating_count > 0 + ? `${e.rating.toFixed(1)}★ (${e.rating_count})` + : "—"} +
+
+ )}

Revenue (Polar)

{polarError && ( diff --git a/app/api/admin/summary/route.ts b/app/api/admin/summary/route.ts index 27a6e39..88fa3b8 100644 --- a/app/api/admin/summary/route.ts +++ b/app/api/admin/summary/route.ts @@ -282,10 +282,68 @@ export async function GET(req: Request) { { last_week: 0, last_month: 0 } ); + // VS Code Marketplace — the one distribution channel npm can't see. Public + // gallery API; resilient (any failure yields zeros, never breaks the page). + const VSCODE_EXTENSIONS = ["asafamos.axle-a11y"]; + const marketplaceData = await Promise.all( + VSCODE_EXTENSIONS.map(async (ext) => { + try { + const res = await fetch( + "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery", + { + method: "POST", + cache: "no-store", + headers: { + "Content-Type": "application/json", + Accept: "application/json;api-version=3.0-preview.1", + }, + body: JSON.stringify({ + filters: [{ criteria: [{ filterType: 7, value: ext }] }], + flags: 914, + }), + } + ); + const json = (await res.json()) as { + results?: Array<{ + extensions?: Array<{ + statistics?: Array<{ statisticName: string; value: number }>; + }>; + }>; + }; + const stats = json?.results?.[0]?.extensions?.[0]?.statistics ?? []; + const stat = (name: string) => + Number(stats.find((s) => s.statisticName === name)?.value ?? 0); + return { + extension: ext, + installs: stat("install"), + downloads: stat("downloadCount"), + rating: stat("averagerating"), + rating_count: stat("ratingcount"), + }; + } catch { + return { + extension: ext, + installs: 0, + downloads: 0, + rating: 0, + rating_count: 0, + }; + } + }) + ); + const marketplaceTotals = marketplaceData.reduce( + (acc, row) => ({ + installs: acc.installs + row.installs, + downloads: acc.downloads + row.downloads, + }), + { installs: 0, downloads: 0 } + ); + return NextResponse.json({ stats: kvData, polar: polarData, npm: { packages: npmData, totals: npmTotals }, + marketplace: { extensions: marketplaceData, totals: marketplaceTotals }, generated_at: new Date().toISOString(), }); } From 8af22a64562deaa6eca0ad0a46d9b6cf066649bc Mon Sep 17 00:00:00 2001 From: Asaf Amos Date: Sat, 6 Jun 2026 23:41:45 +0300 Subject: [PATCH 2/2] chore(raycast): rename extension off the "Axle" trademark MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The WordPress.org review flagged "Axle" as colliding with the existing "Axle AI" project; we renamed the WP plugin to "AsafAmos Accessibility Scanner". The Raycast extension carried the same "Axle" name, so apply the same rename before resubmitting (the raycast/extensions PR #27308 was auto-closed for inactivity — reviving it as "Axle" risks the same flag). - name: axle -> asafamos-accessibility-scanner - title: "Axle — Accessibility Scanner" -> "AsafAmos — Accessibility Scanner" - command subtitles + README/CHANGELOG headers updated to match. - Kept references to the axle hosted backend (axle-iota.vercel.app) — that's the real API, not the listing identity (same approach as the WP plugin). Co-Authored-By: Claude Opus 4.8 --- packages/axle-raycast/CHANGELOG.md | 4 ++-- packages/axle-raycast/README.md | 6 +++--- packages/axle-raycast/package.json | 8 ++++---- packages/axle-raycast/src/statement.tsx | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/axle-raycast/CHANGELOG.md b/packages/axle-raycast/CHANGELOG.md index 68487e4..7345134 100644 --- a/packages/axle-raycast/CHANGELOG.md +++ b/packages/axle-raycast/CHANGELOG.md @@ -1,6 +1,6 @@ -# axle — Accessibility Scanner Changelog +# AsafAmos — Accessibility Scanner Changelog ## [Initial Release] - 2026-04-20 - Scan URL for Accessibility: run axe-core 4.11 against any public URL and render violations in a Raycast list grouped by severity, with links to axe-core docs. -- Open Hebrew Accessibility Statement Generator: opens the free axle generator aligned with Israeli תקנה 35. +- Open Hebrew Accessibility Statement Generator: opens the free accessibility statement generator aligned with Israeli תקנה 35. diff --git a/packages/axle-raycast/README.md b/packages/axle-raycast/README.md index 8f68d05..3591c05 100644 --- a/packages/axle-raycast/README.md +++ b/packages/axle-raycast/README.md @@ -1,10 +1,10 @@ -# axle for Raycast +# AsafAmos — Accessibility Scanner (Raycast) Scan any URL for WCAG 2.1 / 2.2 AA violations without leaving Raycast. ## Commands -- **Scan URL for Accessibility** — prompt for a URL, run the axle scanner, show a filterable list of violations with severity and affected-element counts. Press Enter on any row for the offending HTML and a link to the WCAG reference. +- **Scan URL for Accessibility** — prompt for a URL, run the scanner, show a filterable list of violations with severity and affected-element counts. Press Enter on any row for the offending HTML and a link to the WCAG reference. - **Open Hebrew Accessibility Statement Generator** — one-click open of the free `תקנה 35`-aligned statement generator. ## Under the hood @@ -13,7 +13,7 @@ Commands call the public axle API at `https://axle-iota.vercel.app/api/scan`. No ## Install -Once listed in the Raycast Store: search "axle". +Once listed in the Raycast Store: search "accessibility scanner". ## Dev diff --git a/packages/axle-raycast/package.json b/packages/axle-raycast/package.json index e37266a..8dfd586 100644 --- a/packages/axle-raycast/package.json +++ b/packages/axle-raycast/package.json @@ -1,7 +1,7 @@ { "$schema": "https://www.raycast.com/schemas/extension.json", - "name": "axle", - "title": "Axle — Accessibility Scanner", + "name": "asafamos-accessibility-scanner", + "title": "AsafAmos — Accessibility Scanner", "description": "Scan any URL for WCAG 2.1 / 2.2 AA accessibility violations without leaving Raycast. Results appear as a list; pick any violation to see the offending element and suggested fix.", "icon": "command-icon.png", "author": "asafamos", @@ -14,7 +14,7 @@ { "name": "scan", "title": "Scan URL for Accessibility", - "subtitle": "axle", + "subtitle": "AsafAmos", "description": "Run an axe-core scan against any public URL and show violations.", "mode": "view", "arguments": [ @@ -29,7 +29,7 @@ { "name": "statement", "title": "Open Hebrew Accessibility Statement Generator", - "subtitle": "axle", + "subtitle": "AsafAmos", "description": "Opens the free Hebrew statement generator — aligned with Israeli תקנה 35.", "mode": "no-view" } diff --git a/packages/axle-raycast/src/statement.tsx b/packages/axle-raycast/src/statement.tsx index e0bf869..fdae6a1 100644 --- a/packages/axle-raycast/src/statement.tsx +++ b/packages/axle-raycast/src/statement.tsx @@ -2,5 +2,5 @@ import { open, showHUD } from "@raycast/api"; export default async function Statement() { await open("https://axle-iota.vercel.app/statement"); - await showHUD("Opened axle Hebrew statement generator"); + await showHUD("Opened the Hebrew accessibility statement generator"); }