x12i-downloads 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @x12i Total Downloads
2
2
 
3
+ This is the `x12i-downloads` package in the x12i-stats monorepo (`packages/x12i-stats`). Run the commands below from this directory, or from the repo root with `npm run <script> -w x12i-downloads`.
4
+
3
5
  A React dashboard that counts cumulative npm downloads of every package the `@x12i` org has ever published, since March 20, 2026.
4
6
 
5
7
  Live: https://stats.x12i.com (also https://x12i-downloads.ami-3ad.workers.dev)
@@ -10,7 +12,7 @@ Live: https://stats.x12i.com (also https://x12i-downloads.ami-3ad.workers.dev)
10
12
  - `packages` remembers every package ever discovered. Each run counts the union of known packages and npm's current org listing, so a package that leaves the listing keeps counting.
11
13
  - `package_daily` stores per-package daily downloads. A stored day is only ever raised, never lowered or deleted, so the total cannot go down when npm revises or drops data.
12
14
  - A new total is refused (the published one is held) if it would be lower; `stats_history` keeps one row per published day.
13
- - npm publishes day D on D+1 at the earliest, sometimes stalls for days, and sometimes skips days for every package. Days npm reports as 0 for every package are estimated, through yesterday: each package gets the average of its last 7 days with downloads (within the 30 days before npm's last counted day), as an integer. "Skipped" days are ones npm moved past; "pending" days are after npm's latest counted day. Estimates are computed at publish time and never stored in `package_daily`, so npm's real counts replace them as soon as npm publishes or backfills those days. Because estimates skip zero days they run high, so when real counts arrive the total may be held for a while until real downloads catch up.
15
+ - npm publishes day D on D+1 at the earliest, sometimes stalls for days, and sometimes skips days for every package. Days npm reports as 0 for every package are estimated, through yesterday: each package gets the average of its last 7 days with downloads (within the 30 days before npm's last counted day), as an integer, and the day's sum is capped at the org's real average over the 7 days npm counted before it (so sparse packages and org-wide sweep days don't inflate it). "Skipped" days are ones npm moved past; "pending" days are after npm's latest counted day. Estimates are computed at publish time and never stored in `package_daily`, so npm's real counts replace them as soon as npm publishes or backfills those days. If real counts arrive lower than the estimates, the total is held until real downloads catch up.
14
16
 
15
17
  ## Refresh schedule
16
18
 
@@ -92,7 +94,7 @@ npm test
92
94
  ## Main source files
93
95
 
94
96
  - `lib/npm-stats-live.ts` — refresh state machine and SQL (shared by Worker and local runner)
95
- - `lib/npm-client.ts` — npm registry / downloads API client with fetch budget and retries
97
+ - `lib/npm-client.ts` — download-range client with a fetch budget. Org discovery goes through `@x12i/npm-core`
96
98
  - `migrations/` — D1 / SQLite schema
97
99
  - `lib/insights.mjs` — analytics SDK (leaders, movers, concentration, anomalies, lifecycle, report rendering)
98
100
  - `lib/sqlite-d1.mjs` — D1-compatible adapter over `node:sqlite`
@@ -279,7 +279,8 @@ export default function AdminPage() {
279
279
  <h2>Estimated days ({estimatedDays.length})</h2>
280
280
  <p className="muted small">
281
281
  Days npm reports as 0 for every package. Each package is estimated as the average of its last 7 days
282
- with downloads (within 30 days); npm&apos;s real counts replace these whenever npm publishes them.
282
+ with downloads (within 30 days), and each day is capped at the org&apos;s real average over the 7 days npm
283
+ counted before it. npm&apos;s real counts replace these whenever npm publishes them.
283
284
  </p>
284
285
  <div className="admin-table-wrap">
285
286
  <table className="admin-table">
package/app/globals.css CHANGED
@@ -1,17 +1,41 @@
1
1
  @import "tailwindcss";
2
2
 
3
3
  :root {
4
- --background: #0f172a;
4
+ color-scheme: dark;
5
+ --background: #0b1222;
5
6
  --surface: #111c30;
6
- --surface-border: #253149;
7
- --foreground: #f8fafc;
8
- --muted: #94a3b8;
9
- --muted-dark: #64748b;
7
+ --card: #0f1a2e;
8
+ --surface-border: #22304a;
9
+ --foreground: #f1f5f9;
10
+ --muted: #a3b2c7;
11
+ --muted-dark: #8293ab;
10
12
  --accent: #38bdf8;
13
+ --accent-strong: #7dd3fc;
11
14
  --accent-soft: rgba(56, 189, 248, 0.12);
12
- --grid: #334155;
15
+ --grid: #26344d;
16
+ --command-bg: rgba(8, 15, 30, 0.6);
13
17
  --danger: #f87171;
14
- --anim-duration: 2s;
18
+ --shadow: 0 28px 80px rgba(2, 6, 23, 0.45);
19
+ }
20
+
21
+ /* Light theme for the downloads card only; the stats admin page stays dark. */
22
+ @media (prefers-color-scheme: light) {
23
+ :root:has(.dashboard-shell) {
24
+ color-scheme: light;
25
+ --background: #eef2f7;
26
+ --surface: #ffffff;
27
+ --card: #ffffff;
28
+ --surface-border: #dde3ec;
29
+ --foreground: #0f172a;
30
+ --muted: #475569;
31
+ --muted-dark: #5b6b82;
32
+ --accent: #0b7ea8;
33
+ --accent-strong: #075c7c;
34
+ --accent-soft: rgba(11, 126, 168, 0.1);
35
+ --grid: #e3e8f0;
36
+ --command-bg: #f5f7fb;
37
+ --shadow: 0 24px 60px rgba(15, 23, 42, 0.08);
38
+ }
15
39
  }
16
40
 
17
41
  * {
@@ -26,9 +50,7 @@ html {
26
50
  body {
27
51
  min-height: 100vh;
28
52
  margin: 0;
29
- background:
30
- radial-gradient(circle at 50% 0%, rgba(56, 189, 248, 0.08), transparent 32rem),
31
- var(--background);
53
+ background: var(--background);
32
54
  color: var(--foreground);
33
55
  font-family: var(--font-geist-sans), Inter, ui-sans-serif, system-ui, sans-serif;
34
56
  }
@@ -40,144 +62,159 @@ select {
40
62
  font: inherit;
41
63
  }
42
64
 
65
+ .sr {
66
+ position: absolute;
67
+ width: 1px;
68
+ height: 1px;
69
+ overflow: hidden;
70
+ clip: rect(0 0 0 0);
71
+ white-space: nowrap;
72
+ }
73
+
74
+ /* ── Card ──────────────────────────────────────────────── */
75
+
43
76
  .dashboard-shell {
44
77
  display: grid;
45
78
  min-height: 100vh;
46
79
  place-items: center;
47
- padding: 2rem;
80
+ padding: 2rem 1rem;
48
81
  }
49
82
 
50
83
  .dashboard-card {
51
- width: min(100%, 44rem);
52
- overflow: hidden;
84
+ width: min(100%, 41rem);
85
+ padding: 2.25rem 2.25rem 2rem;
53
86
  border: 1px solid var(--surface-border);
54
- border-radius: 1.75rem;
55
- background: rgba(15, 23, 42, 0.82);
56
- box-shadow: 0 32px 90px rgba(2, 6, 23, 0.48);
57
- backdrop-filter: blur(16px);
58
- }
59
-
60
- .dashboard-content {
61
- padding: 2.25rem 2.5rem 2.5rem;
87
+ border-radius: 1.5rem;
88
+ background: var(--card);
89
+ box-shadow: var(--shadow);
62
90
  text-align: center;
63
91
  }
64
92
 
65
- .eyebrow {
66
- margin: 0.5rem 0 0;
67
- color: var(--muted);
68
- font-size: 0.72rem;
69
- font-weight: 700;
70
- letter-spacing: 0.18em;
71
- text-transform: uppercase;
93
+ .intro {
94
+ display: grid;
95
+ justify-items: center;
96
+ gap: 0.75rem;
72
97
  }
73
98
 
74
99
  .title {
75
100
  margin: 0;
76
- font-size: clamp(2rem, 7vw, 2.75rem);
77
- font-weight: 850;
78
- letter-spacing: -0.045em;
101
+ max-width: 20ch;
102
+ font-size: clamp(1.6rem, 5.4vw, 2.2rem);
103
+ font-weight: 800;
104
+ letter-spacing: -0.03em;
105
+ line-height: 1.12;
106
+ text-wrap: balance;
107
+ }
108
+
109
+ .title-break {
110
+ display: block;
111
+ }
112
+
113
+ .description {
114
+ max-width: 30rem;
115
+ margin: 0;
116
+ color: var(--muted);
117
+ font-size: 1rem;
118
+ line-height: 1.55;
119
+ text-wrap: balance;
79
120
  }
80
121
 
81
122
  .metric-block {
82
- margin: 2.5rem 0 2rem;
123
+ margin: 2.25rem 0 0.5rem;
83
124
  }
84
125
 
85
126
  .total {
86
- min-height: 1.05em;
127
+ margin: 0;
87
128
  color: var(--accent);
88
- font-size: clamp(3.5rem, 12vw, 5.25rem);
129
+ font-size: clamp(3.25rem, 15vw, 5.75rem);
89
130
  font-variant-numeric: tabular-nums;
90
- font-weight: 900;
91
- letter-spacing: -0.065em;
131
+ font-weight: 850;
132
+ letter-spacing: -0.045em;
92
133
  line-height: 1;
134
+ white-space: nowrap;
93
135
  }
94
136
 
95
- .total-pending {
96
- color: var(--muted);
97
- font-size: clamp(2.25rem, 8vw, 3.5rem);
98
- letter-spacing: -0.045em;
137
+ .total-placeholder {
138
+ color: var(--grid);
99
139
  }
100
140
 
101
- .metric-description {
102
- margin: 0.65rem 0 0;
141
+ .total-empty {
103
142
  color: var(--muted-dark);
104
- font-size: 0.875rem;
105
143
  }
106
144
 
107
- .metric-description strong {
145
+ .metric-label {
146
+ margin: 0.6rem 0 0;
108
147
  color: var(--foreground);
148
+ font-size: 1rem;
109
149
  font-weight: 700;
110
150
  }
111
151
 
112
- .metric-tagline {
113
- margin: 0.4rem 0 0;
114
- color: var(--muted-dark);
115
- font-size: 0.8rem;
152
+ .metric-scope {
153
+ margin: 0.25rem 0 0;
154
+ color: var(--muted);
155
+ font-size: 0.875rem;
116
156
  }
117
157
 
118
- .status-row {
119
- display: flex;
120
- min-height: 1.5rem;
121
- align-items: center;
122
- justify-content: center;
123
- gap: 0.5rem;
124
- margin-top: 0.4rem;
125
- color: var(--muted-dark);
126
- font-size: 0.75rem;
158
+ .metric-status {
159
+ margin: 0.5rem 0 0;
160
+ color: var(--muted);
161
+ font-size: 0.8rem;
127
162
  }
128
163
 
129
- .status-row.error {
130
- color: var(--danger);
164
+ /* ── Chart ─────────────────────────────────────────────── */
165
+
166
+ .chart-frame {
167
+ margin: 2rem 0 0;
168
+ text-align: left;
131
169
  }
132
170
 
133
- .spinner {
134
- width: 0.75rem;
135
- height: 0.75rem;
136
- flex: none;
137
- border: 2px solid rgba(56, 189, 248, 0.2);
138
- border-top-color: var(--accent);
139
- border-radius: 999px;
140
- animation: spin 0.85s linear infinite;
171
+ .chart-head {
172
+ display: flex;
173
+ flex-wrap: wrap;
174
+ align-items: baseline;
175
+ justify-content: space-between;
176
+ gap: 0.25rem 1rem;
177
+ margin-bottom: 0.6rem;
141
178
  }
142
179
 
143
- .retry-button {
144
- margin-left: 0.25rem;
145
- border: 0;
146
- border-bottom: 1px solid currentColor;
147
- padding: 0;
148
- background: transparent;
149
- color: inherit;
150
- cursor: pointer;
151
- font-weight: 700;
180
+ .chart-title {
181
+ margin: 0;
182
+ color: var(--muted);
183
+ font-size: 0.78rem;
184
+ font-weight: 400;
152
185
  }
153
186
 
154
- .retry-button:focus-visible {
155
- outline: 2px solid var(--accent);
156
- outline-offset: 4px;
187
+ .chart-readout {
188
+ margin: 0;
189
+ color: var(--muted);
190
+ font-size: 0.78rem;
191
+ font-variant-numeric: tabular-nums;
157
192
  }
158
193
 
159
- /* ── Chart ─────────────────────────────────────────────── */
194
+ .chart-readout strong {
195
+ color: var(--foreground);
196
+ font-weight: 400;
197
+ }
160
198
 
161
- .chart-frame {
199
+ .chart-plot {
162
200
  position: relative;
163
- width: 100%;
164
- height: 16rem;
165
- margin-top: 1rem;
201
+ height: 13.5rem;
166
202
  }
167
203
 
168
- .chart-frame svg {
204
+ .chart-plot svg {
169
205
  display: block;
170
206
  width: 100%;
171
207
  height: 100%;
172
208
  overflow: visible;
209
+ cursor: crosshair;
210
+ outline: none;
211
+ touch-action: pan-y;
173
212
  }
174
213
 
175
- .chart-empty {
176
- display: grid;
177
- height: 100%;
178
- place-items: center;
179
- color: var(--muted-dark);
180
- font-size: 0.8rem;
214
+ .chart-plot svg:focus-visible {
215
+ outline: 2px solid var(--accent);
216
+ outline-offset: 4px;
217
+ border-radius: 4px;
181
218
  }
182
219
 
183
220
  .chart-grid {
@@ -186,10 +223,8 @@ select {
186
223
  vector-effect: non-scaling-stroke;
187
224
  }
188
225
 
189
- .chart-y-label {
190
- fill: var(--muted);
191
- font-size: 12px;
192
- font-variant-numeric: tabular-nums;
226
+ .chart-area {
227
+ fill: url(#area-gradient);
193
228
  }
194
229
 
195
230
  .chart-line {
@@ -197,18 +232,192 @@ select {
197
232
  stroke: var(--accent);
198
233
  stroke-linecap: round;
199
234
  stroke-linejoin: round;
200
- stroke-width: 3;
235
+ stroke-width: 2.5;
201
236
  vector-effect: non-scaling-stroke;
202
- /* Draw animation — uses a very large dasharray; the offset animates to 0 */
203
- stroke-dasharray: 2000;
204
- stroke-dashoffset: 2000;
205
- animation: draw-line var(--anim-duration) ease-out forwards;
206
237
  }
207
238
 
208
- .chart-area {
209
- fill: url(#area-gradient);
210
- opacity: 0;
211
- animation: fade-area var(--anim-duration) ease-out forwards;
239
+ .chart-guide {
240
+ stroke: var(--muted-dark);
241
+ stroke-width: 1;
242
+ stroke-dasharray: 3 4;
243
+ vector-effect: non-scaling-stroke;
244
+ }
245
+
246
+ .chart-dot {
247
+ position: absolute;
248
+ width: 11px;
249
+ height: 11px;
250
+ border: 2px solid var(--card);
251
+ border-radius: 50%;
252
+ background: var(--accent);
253
+ box-shadow: 0 0 0 4px var(--accent-soft);
254
+ transform: translate(-50%, -50%);
255
+ pointer-events: none;
256
+ }
257
+
258
+ .chart-y-axis {
259
+ position: absolute;
260
+ inset: 0 auto 0 0;
261
+ width: calc(64 / 760 * 100%);
262
+ pointer-events: none;
263
+ }
264
+
265
+ .chart-y-axis span {
266
+ position: absolute;
267
+ right: 0.6rem;
268
+ transform: translateY(-50%);
269
+ color: var(--muted-dark);
270
+ font-size: 0.72rem;
271
+ font-variant-numeric: tabular-nums;
272
+ }
273
+
274
+ .chart-x-axis {
275
+ position: absolute;
276
+ right: 0;
277
+ bottom: 0;
278
+ left: 0;
279
+ height: 1.1rem;
280
+ pointer-events: none;
281
+ }
282
+
283
+ .chart-x-axis span {
284
+ position: absolute;
285
+ bottom: 0;
286
+ transform: translateX(-50%);
287
+ color: var(--muted-dark);
288
+ font-size: 0.72rem;
289
+ white-space: nowrap;
290
+ }
291
+
292
+ /* ── Agent setup ───────────────────────────────────────── */
293
+
294
+ .setup {
295
+ display: grid;
296
+ justify-items: center;
297
+ gap: 0.55rem;
298
+ margin-top: 1.75rem;
299
+ padding-top: 1.5rem;
300
+ border-top: 1px solid var(--surface-border);
301
+ }
302
+
303
+ .setup-title {
304
+ margin: 0;
305
+ font-size: 1.05rem;
306
+ font-weight: 750;
307
+ }
308
+
309
+ .setup-description {
310
+ margin: 0;
311
+ color: var(--muted);
312
+ font-size: 0.92rem;
313
+ }
314
+
315
+ .setup-row {
316
+ display: flex;
317
+ flex-wrap: wrap;
318
+ align-items: center;
319
+ justify-content: center;
320
+ gap: 0.6rem 1rem;
321
+ width: 100%;
322
+ margin-top: 0.35rem;
323
+ }
324
+
325
+ .setup-command {
326
+ display: flex;
327
+ flex: 0 1 25rem;
328
+ align-items: center;
329
+ gap: 0.6rem;
330
+ min-width: 0;
331
+ min-height: 2.75rem;
332
+ padding: 0.3rem 0.3rem 0.3rem 0.9rem;
333
+ border: 1px solid var(--surface-border);
334
+ border-radius: 0.7rem;
335
+ background: var(--command-bg);
336
+ text-align: left;
337
+ }
338
+
339
+ .setup-prompt {
340
+ color: var(--muted-dark);
341
+ font-family: var(--font-geist-mono), ui-monospace, monospace;
342
+ font-size: 0.9rem;
343
+ user-select: none;
344
+ }
345
+
346
+ .setup-command code {
347
+ flex: 1;
348
+ min-width: 0;
349
+ color: var(--foreground);
350
+ font-family: var(--font-geist-mono), ui-monospace, monospace;
351
+ font-size: 0.9rem;
352
+ overflow-wrap: anywhere;
353
+ user-select: all;
354
+ }
355
+
356
+ .setup-copy {
357
+ display: inline-flex;
358
+ min-width: 2.5rem;
359
+ height: 2.25rem;
360
+ flex: none;
361
+ align-items: center;
362
+ justify-content: center;
363
+ gap: 0.35rem;
364
+ padding: 0 0.55rem;
365
+ border: 0;
366
+ border-radius: 0.5rem;
367
+ background: transparent;
368
+ color: var(--muted);
369
+ cursor: pointer;
370
+ }
371
+
372
+ .setup-copy:hover {
373
+ background: var(--accent-soft);
374
+ color: var(--foreground);
375
+ }
376
+
377
+ .copy-feedback {
378
+ color: var(--accent);
379
+ font-size: 0.8rem;
380
+ font-weight: 700;
381
+ }
382
+
383
+ .setup-copy:focus-visible,
384
+ .catalog-link:focus-visible {
385
+ outline: 2px solid var(--accent);
386
+ outline-offset: 3px;
387
+ }
388
+
389
+ .catalog-link {
390
+ display: inline-flex;
391
+ min-height: 2.75rem;
392
+ align-items: center;
393
+ padding: 0 0.4rem;
394
+ border-radius: 0.5rem;
395
+ color: var(--accent);
396
+ font-size: 0.92rem;
397
+ font-weight: 700;
398
+ text-underline-offset: 4px;
399
+ }
400
+
401
+ .catalog-link:hover {
402
+ color: var(--accent-strong);
403
+ }
404
+
405
+ .setup-help {
406
+ margin: 0;
407
+ color: var(--muted);
408
+ font-size: 0.82rem;
409
+ }
410
+
411
+ .setup-prompt-hint {
412
+ max-width: 30rem;
413
+ margin: 0.35rem 0 0;
414
+ color: var(--muted-dark);
415
+ font-size: 0.8rem;
416
+ line-height: 1.5;
417
+ }
418
+
419
+ .setup-prompt-hint q {
420
+ color: var(--muted);
212
421
  }
213
422
 
214
423
  /* ── Admin ─────────────────────────────────────────────── */
@@ -414,63 +623,48 @@ select {
414
623
  color: var(--danger);
415
624
  }
416
625
 
417
- /* ── Keyframes ────────────────────────────────────────── */
418
-
419
- @keyframes spin {
420
- to { transform: rotate(360deg); }
421
- }
422
-
423
- @keyframes draw-line {
424
- to { stroke-dashoffset: 0; }
425
- }
426
-
427
- @keyframes fade-area {
428
- 0% { opacity: 0; }
429
- 40% { opacity: 0; }
430
- 100% { opacity: 1; }
431
- }
432
-
433
626
  /* ── Responsive ───────────────────────────────────────── */
434
627
 
435
628
  @media (max-width: 640px) {
436
629
  .dashboard-shell {
437
- align-items: stretch;
438
- padding: 0;
630
+ padding: 0.75rem;
439
631
  }
440
632
 
441
633
  .dashboard-card {
442
- min-height: 100vh;
443
- border: 0;
444
- border-radius: 0;
634
+ padding: 1.6rem 1.15rem 1.25rem;
635
+ border-radius: 1.25rem;
445
636
  }
446
637
 
447
- .dashboard-content {
448
- padding: 3rem 1.25rem 2rem;
638
+ .title {
639
+ font-size: clamp(1.55rem, 7.4vw, 1.75rem);
640
+ }
641
+
642
+ .description {
643
+ font-size: 0.95rem;
449
644
  }
450
645
 
451
646
  .metric-block {
452
- margin-top: 3rem;
647
+ margin-top: 1.9rem;
453
648
  }
454
649
 
455
- .chart-frame {
456
- height: 14rem;
457
- margin-top: 2rem;
650
+ .chart-plot {
651
+ height: 11.5rem;
458
652
  }
459
- }
460
653
 
461
- @media (prefers-reduced-motion: reduce) {
462
- .spinner {
463
- animation-duration: 2s;
654
+ .chart-x-axis .tick-wide-only {
655
+ display: none;
656
+ }
657
+
658
+ .setup-row {
659
+ flex-direction: column;
660
+ align-items: stretch;
464
661
  }
465
662
 
466
- .chart-line {
467
- stroke-dasharray: none;
468
- stroke-dashoffset: 0;
469
- animation: none;
663
+ .setup-command {
664
+ flex-basis: auto;
470
665
  }
471
666
 
472
- .chart-area {
473
- opacity: 1;
474
- animation: none;
667
+ .catalog-link {
668
+ justify-content: center;
475
669
  }
476
670
  }
package/app/layout.tsx CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { Metadata } from "next";
2
2
  import { Geist, Geist_Mono } from "next/font/google";
3
- import { PACKAGE_SCOPE } from "@/lib/config";
4
3
  import "./globals.css";
5
4
 
6
5
  const geistSans = Geist({
@@ -13,11 +12,9 @@ const geistMono = Geist_Mono({
13
12
  subsets: ["latin"],
14
13
  });
15
14
 
16
- const scopeDisplay = PACKAGE_SCOPE.replace(/\/$/, "");
17
-
18
15
  export const metadata: Metadata = {
19
- title: `${scopeDisplay} Total Downloads`,
20
- description: `Live cumulative downloads across every public ${scopeDisplay} npm package.`,
16
+ title: "x12i · SDK ecosystem",
17
+ description: "The SDK ecosystem built for coding agents: reusable packages for data, intelligence, logic, presentation and tools. npm downloads across the x12i package catalog.",
21
18
  other: {
22
19
  "codex-preview": "development",
23
20
  },