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(), }); }