Skip to content

Commit 9f7c4e1

Browse files
chrisbbreuerclaude
andcommitted
feat: add requested versions dashboard to registry
Shows versions requested via CLI that aren't built yet, with request counts and last-requested dates, helping prioritize future builds. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 89f8c09 commit 9f7c4e1

File tree

5 files changed

+167
-1
lines changed

5 files changed

+167
-1
lines changed

packages/registry/dashboard/pages/overview.stx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
<div class="max-w-7xl mx-auto flex items-center justify-between">
2323
<a href="/dashboard" class="text-xl font-bold text-white hover:text-slate-300">Pantry Dashboard</a>
2424
<div class="flex items-center gap-4">
25-
<a href="/dashboard" class="text-slate-300 hover:text-white text-sm">Overview</a>
25+
<a href="/dashboard" class="text-white text-sm font-semibold">Overview</a>
26+
<a href="/dashboard/requested-versions" class="text-slate-300 hover:text-white text-sm">Requested Versions</a>
2627
<a href="/health" class="text-slate-400 hover:text-white text-sm">Health</a>
2728
<a href="/dashboard/logout" class="text-slate-400 hover:text-red-400 text-sm">Logout</a>
2829
</div>

packages/registry/dashboard/pages/package.stx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
<a href="/dashboard" class="text-xl font-bold text-white hover:text-slate-300">Pantry Dashboard</a>
3131
<div class="flex items-center gap-4">
3232
<a href="/dashboard" class="text-slate-300 hover:text-white text-sm">Overview</a>
33+
<a href="/dashboard/requested-versions" class="text-slate-300 hover:text-white text-sm">Requested Versions</a>
3334
<a href="/health" class="text-slate-400 hover:text-white text-sm">Health</a>
3435
<a href="/dashboard/logout" class="text-slate-400 hover:text-red-400 text-sm">Logout</a>
3536
</div>
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<script server>
2+
const allRequests = props.requests || []
3+
const page = props.page || 1
4+
const perPage = props.perPage || 25
5+
const totalRequests = allRequests.length
6+
const totalPages = Math.max(1, Math.ceil(totalRequests / perPage))
7+
const startIdx = (page - 1) * perPage
8+
const pagedRequests = allRequests.slice(startIdx, startIdx + perPage)
9+
const totalRequestCount = allRequests.reduce((sum, r) => sum + (r.requestCount || 0), 0)
10+
const uniquePackages = new Set(allRequests.map(r => r.packageName)).size
11+
12+
const formatDate = (iso) => {
13+
if (!iso) return 'N/A'
14+
const d = new Date(iso)
15+
return d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' })
16+
}
17+
</script>
18+
19+
<!DOCTYPE html>
20+
<html lang="en">
21+
<head>
22+
<meta charset="UTF-8">
23+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
24+
<title>Requested Versions - Pantry Dashboard</title>
25+
</head>
26+
<body class="bg-slate-900 text-white min-h-screen">
27+
<nav class="bg-slate-800 border-b border-slate-700 px-6 py-4">
28+
<div class="max-w-7xl mx-auto flex items-center justify-between">
29+
<a href="/dashboard" class="text-xl font-bold text-white hover:text-slate-300">Pantry Dashboard</a>
30+
<div class="flex items-center gap-4">
31+
<a href="/dashboard" class="text-slate-300 hover:text-white text-sm">Overview</a>
32+
<a href="/dashboard/requested-versions" class="text-white text-sm font-semibold">Requested Versions</a>
33+
<a href="/health" class="text-slate-400 hover:text-white text-sm">Health</a>
34+
<a href="/dashboard/logout" class="text-slate-400 hover:text-red-400 text-sm">Logout</a>
35+
</div>
36+
</div>
37+
</nav>
38+
39+
<main class="max-w-7xl mx-auto px-6 py-8">
40+
<h1 class="text-2xl font-bold mb-2">Requested Versions</h1>
41+
<p class="text-slate-400 mb-6 text-sm">Versions requested via the CLI that are not yet built in the registry. These represent potential build targets.</p>
42+
43+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
44+
<div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
45+
<div class="text-sm text-slate-400 mb-1">Total Requests</div>
46+
<div class="text-2xl font-bold">{{ totalRequestCount.toLocaleString() }}</div>
47+
</div>
48+
<div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
49+
<div class="text-sm text-slate-400 mb-1">Unique Versions</div>
50+
<div class="text-2xl font-bold">{{ totalRequests.toLocaleString() }}</div>
51+
</div>
52+
<div class="bg-slate-800 rounded-lg p-6 border border-slate-700">
53+
<div class="text-sm text-slate-400 mb-1">Unique Packages</div>
54+
<div class="text-2xl font-bold">{{ uniquePackages.toLocaleString() }}</div>
55+
</div>
56+
</div>
57+
58+
<div class="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
59+
<div class="px-6 py-4 border-b border-slate-700 flex items-center justify-between">
60+
<h2 class="text-lg font-semibold">Missing Versions (by request count)</h2>
61+
<span class="text-sm text-slate-400">Page {{ page }} of {{ totalPages }}</span>
62+
</div>
63+
64+
@if (totalRequests === 0)
65+
<div class="px-6 py-12 text-center text-slate-400">
66+
<p class="text-lg mb-2">No version requests recorded yet</p>
67+
<p class="text-sm">When users request versions that don't exist in the registry, they'll appear here.</p>
68+
</div>
69+
@else
70+
<table class="w-full">
71+
<thead>
72+
<tr class="text-slate-400 text-sm border-b border-slate-700">
73+
<th class="py-2 px-4 text-left w-12">#</th>
74+
<th class="py-2 px-4 text-left">Package</th>
75+
<th class="py-2 px-4 text-left">Version</th>
76+
<th class="py-2 px-4 text-right">Requests</th>
77+
<th class="py-2 px-4 text-right">Last Requested</th>
78+
</tr>
79+
</thead>
80+
<tbody>
81+
@foreach (pagedRequests as req, index)
82+
<tr class="border-b border-slate-700 hover:bg-slate-700/50">
83+
<td class="py-3 px-4 text-slate-400">{{ startIdx + index + 1 }}</td>
84+
<td class="py-3 px-4">
85+
<a href="/dashboard/package/{{ encodeURIComponent(req.packageName) }}" class="text-blue-400 hover:text-blue-300">{{ req.packageName }}</a>
86+
</td>
87+
<td class="py-3 px-4">
88+
<span class="bg-slate-700 text-slate-200 px-2 py-0.5 rounded text-sm font-mono">{{ req.version }}</span>
89+
</td>
90+
<td class="py-3 px-4 text-right font-semibold">{{ (req.requestCount || 0).toLocaleString() }}</td>
91+
<td class="py-3 px-4 text-right text-slate-400 text-sm">{{ formatDate(req.lastRequestedAt) }}</td>
92+
</tr>
93+
@endforeach
94+
</tbody>
95+
</table>
96+
97+
@if (totalPages > 1)
98+
<div class="px-6 py-3 border-t border-slate-700 flex items-center justify-center gap-2">
99+
@if (page > 1)
100+
<a href="/dashboard/requested-versions?page={{ page - 1 }}" class="px-3 py-1 bg-slate-700 rounded text-sm hover:bg-slate-600">Previous</a>
101+
@endif
102+
@for (let i = 1; i <= totalPages; i++)
103+
@if (i === page)
104+
<span class="px-3 py-1 bg-blue-600 rounded text-sm font-bold">{{ i }}</span>
105+
@else
106+
<a href="/dashboard/requested-versions?page={{ i }}" class="px-3 py-1 bg-slate-700 rounded text-sm hover:bg-slate-600">{{ i }}</a>
107+
@endif
108+
@endfor
109+
@if (page < totalPages)
110+
<a href="/dashboard/requested-versions?page={{ page + 1 }}" class="px-3 py-1 bg-slate-700 rounded text-sm hover:bg-slate-600">Next</a>
111+
@endif
112+
</div>
113+
@endif
114+
@endif
115+
</div>
116+
</main>
117+
</body>
118+
</html>

packages/registry/src/analytics.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export interface AnalyticsStorage {
5555
trackEvent(event: AnalyticsEvent): Promise<void>
5656
trackMissingVersion(packageName: string, version: string, userAgent?: string): Promise<void>
5757
getMissingVersionRequests(packageName: string, limit?: number): Promise<MissingVersionRequest[]>
58+
getAllMissingVersionRequests(limit?: number): Promise<MissingVersionRequest[]>
5859
getPackageStats(packageName: string): Promise<PackageStats | null>
5960
getTopPackages(limit?: number): Promise<Array<{ name: string, downloads: number }>>
6061
getDownloadTimeline(packageName: string, days?: number): Promise<Array<{ date: string, count: number }>>
@@ -431,6 +432,31 @@ export class DynamoDBAnalytics implements AnalyticsStorage {
431432
return requests.slice(0, limit)
432433
}
433434

435+
async getAllMissingVersionRequests(limit = 100): Promise<MissingVersionRequest[]> {
436+
// Scan for all VERSION_REQUEST# items
437+
const result = await this.db.scan({
438+
TableName: this.tableName,
439+
FilterExpression: 'begins_with(PK, :prefix)',
440+
ExpressionAttributeValues: {
441+
':prefix': { S: 'VERSION_REQUEST#' },
442+
},
443+
})
444+
445+
const requests: MissingVersionRequest[] = result.Items.map((item) => {
446+
const data = DynamoDBClient.unmarshal(item)
447+
return {
448+
packageName: data.packageName,
449+
version: data.version,
450+
requestCount: data.requestCount || 0,
451+
lastRequestedAt: data.lastRequestedAt || '',
452+
}
453+
})
454+
455+
// Sort by request count descending
456+
requests.sort((a, b) => b.requestCount - a.requestCount)
457+
return requests.slice(0, limit)
458+
}
459+
434460
async getInstallAnalytics(days: 30 | 90 | 365): Promise<InstallAnalyticsResult> {
435461
return this.getCategoryAnalytics('install', days)
436462
}
@@ -582,6 +608,12 @@ export class InMemoryAnalytics implements AnalyticsStorage {
582608
return results.slice(0, limit)
583609
}
584610

611+
async getAllMissingVersionRequests(limit = 100): Promise<MissingVersionRequest[]> {
612+
const results = Array.from(this.missingVersionRequests.values())
613+
results.sort((a, b) => b.requestCount - a.requestCount)
614+
return results.slice(0, limit)
615+
}
616+
585617
async getInstallAnalytics(days: 30 | 90 | 365): Promise<InstallAnalyticsResult> {
586618
return this.getCategoryAnalytics('install', days)
587619
}

packages/registry/src/server.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,7 @@ export function createServer(
574574
console.log(' GET /binaries/{domain}/{ver}/{plat}/* - Tarball/checksum')
575575
console.log('Dashboard:')
576576
console.log(' GET /dashboard - Analytics overview')
577+
console.log(' GET /dashboard/requested-versions - Missing version requests')
577578
console.log(' GET /dashboard/package/{name} - Package detail')
578579
console.log(' GET /dashboard/login - Login')
579580
}
@@ -1126,6 +1127,11 @@ async function handleDashboard(
11261127
return Response.json({ packages: topPackages }, { headers: noCacheHeaders })
11271128
}
11281129

1130+
if (path === '/dashboard/api/requested-versions') {
1131+
const allRequests = await analytics.getAllMissingVersionRequests(200)
1132+
return Response.json({ requests: allRequests }, { headers: noCacheHeaders })
1133+
}
1134+
11291135
if (path.startsWith('/dashboard/api/package/')) {
11301136
const packageName = decodeURIComponent(path.replace('/dashboard/api/package/', ''))
11311137
const [stats, timeline] = await Promise.all([
@@ -1146,6 +1152,14 @@ async function handleDashboard(
11461152
return new Response(html, { headers: htmlHeaders })
11471153
}
11481154

1155+
// Requested versions page
1156+
if (path === '/dashboard/requested-versions') {
1157+
const allRequests = await analytics.getAllMissingVersionRequests(200)
1158+
const page = Math.max(1, Number.parseInt(url.searchParams.get('page') || '1', 10))
1159+
const html = await stxRender(`${DASHBOARD_DIR}/requested-versions.stx`, { requests: allRequests, page, perPage: 25 })
1160+
return new Response(html, { headers: htmlHeaders })
1161+
}
1162+
11491163
// Overview page (default)
11501164
if (path === '/dashboard' || path === '/dashboard/') {
11511165
const topPackages = await analytics.getTopPackages(100)

0 commit comments

Comments
 (0)