@littlebearapps/platform-admin-sdk 2.1.0 → 2.3.0

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.
Files changed (122) hide show
  1. package/README.md +2 -5
  2. package/dist/check-upgrade.d.ts +29 -0
  3. package/dist/check-upgrade.js +97 -0
  4. package/dist/index.js +59 -4
  5. package/dist/manifest.d.ts +2 -0
  6. package/dist/scaffold.js +5 -1
  7. package/dist/templates.d.ts +6 -1
  8. package/dist/templates.js +141 -3
  9. package/dist/upgrade.d.ts +1 -0
  10. package/dist/upgrade.js +21 -2
  11. package/package.json +1 -1
  12. package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
  13. package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
  14. package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
  15. package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
  16. package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
  17. package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
  18. package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
  19. package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
  20. package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
  21. package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
  22. package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
  23. package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
  24. package/templates/full/dashboard/src/pages/feedback.astro +365 -0
  25. package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
  26. package/templates/full/dashboard/src/pages/map.astro +561 -0
  27. package/templates/full/dashboard/src/pages/revenue.astro +72 -0
  28. package/templates/full/dashboard/src/pages/tests.astro +431 -0
  29. package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
  30. package/templates/full/scripts/ops/verify-account-total.ts +256 -0
  31. package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
  32. package/templates/full/tests/integration/r2-archive.test.ts +108 -0
  33. package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
  34. package/templates/shared/.github/workflows/validate-controls.yml +27 -0
  35. package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
  36. package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
  37. package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
  38. package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
  39. package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
  40. package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
  41. package/templates/shared/dashboard/src/components/Toast.astro +170 -0
  42. package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
  43. package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
  44. package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
  45. package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
  46. package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
  47. package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
  48. package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
  49. package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
  50. package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
  51. package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
  52. package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
  53. package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
  54. package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
  55. package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
  56. package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
  57. package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
  58. package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
  59. package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
  60. package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
  61. package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
  62. package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
  63. package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
  64. package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
  65. package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
  66. package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
  67. package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
  68. package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
  69. package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
  70. package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
  71. package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
  72. package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
  73. package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
  74. package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
  75. package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
  76. package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
  77. package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
  78. package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
  79. package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
  80. package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
  81. package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
  82. package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
  83. package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
  84. package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
  85. package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
  86. package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
  87. package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
  88. package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
  89. package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
  90. package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
  91. package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
  92. package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
  93. package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
  94. package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
  95. package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
  96. package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
  97. package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
  98. package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
  99. package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
  100. package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
  101. package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
  102. package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
  103. package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
  104. package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
  105. package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
  106. package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
  107. package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
  108. package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
  109. package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
  110. package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
  111. package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
  112. package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
  113. package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
  114. package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
  115. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
  116. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
  117. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
  118. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
  119. package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
  120. package/templates/standard/tests/integration/connectors.test.ts +241 -0
  121. package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
  122. package/templates/standard/tests/integration/ingestion.test.ts +211 -0
@@ -0,0 +1,430 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Audit Cost Anomaly Script
4
+ *
5
+ * Forensic analysis of cost anomalies in daily_usage_rollups.
6
+ * Identifies days with unexpectedly high costs and breaks down by component.
7
+ *
8
+ * Usage:
9
+ * npx tsx scripts/ops/audit-cost-anomaly.ts # High-cost days
10
+ * npx tsx scripts/ops/audit-cost-anomaly.ts --threshold 50 # Custom threshold ($50)
11
+ * npx tsx scripts/ops/audit-cost-anomaly.ts --detailed # Include hourly breakdown
12
+ * npx tsx scripts/ops/audit-cost-anomaly.ts --date 2026-01-23 # Specific date deep dive
13
+ *
14
+ * Environment Variables:
15
+ * CLOUDFLARE_ACCOUNT_ID - Cloudflare account ID (optional, uses default)
16
+ * CLOUDFLARE_API_TOKEN - API token with D1:Read (required)
17
+ * D1_DATABASE_ID - Platform D1 database ID (optional, uses default)
18
+ */
19
+
20
+ import { request } from 'undici';
21
+
22
+ // =============================================================================
23
+ // CONFIGURATION
24
+ // =============================================================================
25
+
26
+ const REST_API_BASE = 'https://api.cloudflare.com/client/v4';
27
+ const DEFAULT_ACCOUNT_ID = '55a0bf6d1396d90cbf9dcbf30fceeb14';
28
+ const DEFAULT_D1_DATABASE_ID = '076b2cd5-054e-4c8c-aa42-86cedc84a4ec';
29
+
30
+ // =============================================================================
31
+ // TYPES
32
+ // =============================================================================
33
+
34
+ interface D1QueryResponse {
35
+ success: boolean;
36
+ errors?: Array<{ message: string }>;
37
+ result: Array<{
38
+ results: Record<string, unknown>[];
39
+ success: boolean;
40
+ }>;
41
+ }
42
+
43
+ interface HighCostDay {
44
+ project: string;
45
+ snapshot_date: string;
46
+ d1_writes: number;
47
+ d1_reads: number;
48
+ kv_writes: number;
49
+ kv_reads: number;
50
+ workers_requests: number;
51
+ total_cost: number;
52
+ d1_cost: number;
53
+ kv_cost: number;
54
+ workers_cost: number;
55
+ }
56
+
57
+ interface HourlySample {
58
+ snapshot_hour: string;
59
+ project: string;
60
+ d1_rows_written: number;
61
+ d1_rows_read: number;
62
+ kv_writes: number;
63
+ total_cost_usd: number;
64
+ }
65
+
66
+ // =============================================================================
67
+ // D1 QUERY FUNCTIONS
68
+ // =============================================================================
69
+
70
+ async function queryD1(
71
+ sql: string,
72
+ accountId: string,
73
+ apiToken: string,
74
+ databaseId: string
75
+ ): Promise<Record<string, unknown>[]> {
76
+ const url = `${REST_API_BASE}/accounts/${accountId}/d1/database/${databaseId}/query`;
77
+
78
+ const response = await request(url, {
79
+ method: 'POST',
80
+ headers: {
81
+ Authorization: `Bearer ${apiToken}`,
82
+ 'Content-Type': 'application/json',
83
+ },
84
+ body: JSON.stringify({ sql }),
85
+ });
86
+
87
+ const body = (await response.body.json()) as D1QueryResponse;
88
+
89
+ if (!body.success) {
90
+ const errorMsg = body.errors?.map((e) => e.message).join(', ') ?? 'Unknown error';
91
+ throw new Error(`D1 query failed: ${errorMsg}`);
92
+ }
93
+
94
+ return body.result[0]?.results ?? [];
95
+ }
96
+
97
+ // =============================================================================
98
+ // ANALYSIS QUERIES
99
+ // =============================================================================
100
+
101
+ /**
102
+ * Find days with costs exceeding threshold, broken down by resource type.
103
+ */
104
+ async function findHighCostDays(
105
+ threshold: number,
106
+ accountId: string,
107
+ apiToken: string,
108
+ databaseId: string
109
+ ): Promise<HighCostDay[]> {
110
+ const sql = `
111
+ SELECT
112
+ project,
113
+ snapshot_date,
114
+ SUM(d1_rows_written) as d1_writes,
115
+ SUM(d1_rows_read) as d1_reads,
116
+ SUM(kv_writes) as kv_writes,
117
+ SUM(kv_reads) as kv_reads,
118
+ SUM(workers_requests) as workers_requests,
119
+ SUM(total_cost_usd) as total_cost,
120
+ SUM(d1_cost_usd) as d1_cost,
121
+ SUM(kv_cost_usd) as kv_cost,
122
+ SUM(workers_cost_usd) as workers_cost
123
+ FROM daily_usage_rollups
124
+ WHERE total_cost_usd > ${threshold}
125
+ GROUP BY project, snapshot_date
126
+ ORDER BY total_cost DESC
127
+ LIMIT 50;
128
+ `;
129
+
130
+ const results = await queryD1(sql, accountId, apiToken, databaseId);
131
+ return results as unknown as HighCostDay[];
132
+ }
133
+
134
+ /**
135
+ * Get hourly breakdown for a specific date.
136
+ */
137
+ async function getHourlyBreakdown(
138
+ date: string,
139
+ accountId: string,
140
+ apiToken: string,
141
+ databaseId: string
142
+ ): Promise<HourlySample[]> {
143
+ const sql = `
144
+ SELECT
145
+ snapshot_hour,
146
+ project,
147
+ d1_rows_written,
148
+ d1_rows_read,
149
+ kv_writes,
150
+ total_cost_usd
151
+ FROM hourly_usage_snapshots
152
+ WHERE snapshot_hour LIKE '${date}%'
153
+ ORDER BY snapshot_hour, project;
154
+ `;
155
+
156
+ const results = await queryD1(sql, accountId, apiToken, databaseId);
157
+ return results as unknown as HourlySample[];
158
+ }
159
+
160
+ /**
161
+ * Get billing period summary.
162
+ */
163
+ async function getBillingPeriodSummary(
164
+ accountId: string,
165
+ apiToken: string,
166
+ databaseId: string
167
+ ): Promise<Record<string, unknown>[]> {
168
+ // First get the billing cycle day
169
+ const settingsSql = `
170
+ SELECT billing_cycle_day
171
+ FROM billing_settings
172
+ LIMIT 1;
173
+ `;
174
+ const settings = await queryD1(settingsSql, accountId, apiToken, databaseId);
175
+
176
+ // Calculate billing window dates
177
+ const now = new Date();
178
+ let billingCycleDay = 17; // Default
179
+ if (settings.length > 0 && settings[0].billing_cycle_day) {
180
+ billingCycleDay = settings[0].billing_cycle_day as number;
181
+ }
182
+
183
+ // Calculate start date of current billing period
184
+ let startDate: Date;
185
+ if (now.getUTCDate() >= billingCycleDay) {
186
+ startDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), billingCycleDay));
187
+ } else {
188
+ startDate = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, billingCycleDay));
189
+ }
190
+
191
+ const startDateStr = startDate.toISOString().split('T')[0];
192
+
193
+ const sql = `
194
+ SELECT
195
+ project,
196
+ SUM(total_cost_usd) as period_cost,
197
+ SUM(d1_rows_written) as total_d1_writes,
198
+ SUM(d1_cost_usd) as total_d1_cost,
199
+ SUM(kv_writes) as total_kv_writes,
200
+ SUM(kv_cost_usd) as total_kv_cost,
201
+ COUNT(DISTINCT snapshot_date) as days_in_period
202
+ FROM daily_usage_rollups
203
+ WHERE snapshot_date >= '${startDateStr}'
204
+ GROUP BY project
205
+ ORDER BY period_cost DESC;
206
+ `;
207
+
208
+ const results = await queryD1(sql, accountId, apiToken, databaseId);
209
+ return results.map((r) => ({ ...r, billing_start: startDateStr }));
210
+ }
211
+
212
+ /**
213
+ * Verify cost calculation logic by checking raw metrics vs computed cost.
214
+ */
215
+ async function verifyCostCalculation(
216
+ date: string,
217
+ accountId: string,
218
+ apiToken: string,
219
+ databaseId: string
220
+ ): Promise<Record<string, unknown>[]> {
221
+ const sql = `
222
+ SELECT
223
+ project,
224
+ snapshot_date,
225
+ d1_rows_written,
226
+ d1_rows_read,
227
+ d1_cost_usd,
228
+ kv_writes,
229
+ kv_reads,
230
+ kv_cost_usd,
231
+ workers_requests,
232
+ workers_cost_usd,
233
+ total_cost_usd,
234
+ -- Expected D1 cost: $1/million writes + $0.001/million reads (first 25B free)
235
+ ROUND(d1_rows_written / 1000000.0, 4) as expected_d1_write_cost,
236
+ -- Expected KV cost: $1/million writes, $0.50/million reads (first 10M free)
237
+ ROUND(kv_writes / 1000000.0, 4) as expected_kv_write_cost
238
+ FROM daily_usage_rollups
239
+ WHERE snapshot_date = '${date}'
240
+ ORDER BY total_cost_usd DESC;
241
+ `;
242
+
243
+ return await queryD1(sql, accountId, apiToken, databaseId);
244
+ }
245
+
246
+ // =============================================================================
247
+ // FORMATTING
248
+ // =============================================================================
249
+
250
+ function formatNumber(n: number): string {
251
+ if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(2)}B`;
252
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
253
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
254
+ return n.toFixed(0);
255
+ }
256
+
257
+ function formatCost(n: number): string {
258
+ return `$${n.toFixed(2)}`;
259
+ }
260
+
261
+ // =============================================================================
262
+ // CLI
263
+ // =============================================================================
264
+
265
+ async function main(): Promise<void> {
266
+ const args = process.argv.slice(2);
267
+
268
+ // Parse arguments
269
+ const thresholdIdx = args.indexOf('--threshold');
270
+ const threshold = thresholdIdx >= 0 ? parseFloat(args[thresholdIdx + 1]) : 100;
271
+
272
+ const dateIdx = args.indexOf('--date');
273
+ const specificDate = dateIdx >= 0 ? args[dateIdx + 1] : null;
274
+
275
+ const detailed = args.includes('--detailed');
276
+
277
+ // Environment variables
278
+ const accountId = process.env.CLOUDFLARE_ACCOUNT_ID ?? DEFAULT_ACCOUNT_ID;
279
+ const apiToken = process.env.CLOUDFLARE_API_TOKEN;
280
+ const databaseId = process.env.D1_DATABASE_ID ?? DEFAULT_D1_DATABASE_ID;
281
+
282
+ if (!apiToken) {
283
+ console.error('Error: CLOUDFLARE_API_TOKEN environment variable is required');
284
+ process.exit(1);
285
+ }
286
+
287
+ console.log('='.repeat(80));
288
+ console.log('COST ANOMALY FORENSIC AUDIT');
289
+ console.log('='.repeat(80));
290
+ console.log(`Threshold: ${formatCost(threshold)}`);
291
+ console.log(`Database: ${databaseId}`);
292
+ console.log('');
293
+
294
+ // Section 1: Billing Period Summary
295
+ console.log('─'.repeat(80));
296
+ console.log('SECTION 1: CURRENT BILLING PERIOD SUMMARY');
297
+ console.log('─'.repeat(80));
298
+
299
+ const billingSummary = await getBillingPeriodSummary(accountId, apiToken, databaseId);
300
+ if (billingSummary.length > 0) {
301
+ console.log(`Billing period start: ${billingSummary[0].billing_start}`);
302
+ console.log('');
303
+ console.log(
304
+ 'Project | Period Cost | D1 Writes | D1 Cost | KV Writes | KV Cost'
305
+ );
306
+ console.log(
307
+ '-------------------|-------------|---------------|-----------|-------------|--------'
308
+ );
309
+ for (const row of billingSummary) {
310
+ console.log(
311
+ `${String(row.project).padEnd(18)} | ` +
312
+ `${formatCost(row.period_cost as number).padStart(11)} | ` +
313
+ `${formatNumber(row.total_d1_writes as number).padStart(13)} | ` +
314
+ `${formatCost(row.total_d1_cost as number).padStart(9)} | ` +
315
+ `${formatNumber(row.total_kv_writes as number).padStart(11)} | ` +
316
+ `${formatCost(row.total_kv_cost as number).padStart(6)}`
317
+ );
318
+ }
319
+ } else {
320
+ console.log('No data found for current billing period.');
321
+ }
322
+ console.log('');
323
+
324
+ // Section 2: High-Cost Days
325
+ console.log('─'.repeat(80));
326
+ console.log(`SECTION 2: HIGH-COST DAYS (>${formatCost(threshold)})`);
327
+ console.log('─'.repeat(80));
328
+
329
+ const highCostDays = await findHighCostDays(threshold, accountId, apiToken, databaseId);
330
+ if (highCostDays.length === 0) {
331
+ console.log(`No days found with cost > ${formatCost(threshold)}`);
332
+ } else {
333
+ console.log(
334
+ 'Date | Project | Total Cost | D1 Writes | D1 Cost | KV Writes | KV Cost'
335
+ );
336
+ console.log(
337
+ '-----------|-------------|------------|---------------|-----------|-------------|--------'
338
+ );
339
+ for (const row of highCostDays) {
340
+ console.log(
341
+ `${row.snapshot_date} | ` +
342
+ `${String(row.project).padEnd(11)} | ` +
343
+ `${formatCost(row.total_cost).padStart(10)} | ` +
344
+ `${formatNumber(row.d1_writes).padStart(13)} | ` +
345
+ `${formatCost(row.d1_cost).padStart(9)} | ` +
346
+ `${formatNumber(row.kv_writes).padStart(11)} | ` +
347
+ `${formatCost(row.kv_cost).padStart(6)}`
348
+ );
349
+ }
350
+ }
351
+ console.log('');
352
+
353
+ // Section 3: Cost Calculation Verification (if specific date or highest cost date)
354
+ const auditDate =
355
+ specificDate ?? (highCostDays.length > 0 ? highCostDays[0].snapshot_date : null);
356
+ if (auditDate) {
357
+ console.log('─'.repeat(80));
358
+ console.log(`SECTION 3: COST CALCULATION VERIFICATION FOR ${auditDate}`);
359
+ console.log('─'.repeat(80));
360
+
361
+ const verification = await verifyCostCalculation(auditDate, accountId, apiToken, databaseId);
362
+ console.log('');
363
+ console.log('Checking if recorded costs match expected costs based on Cloudflare pricing...');
364
+ console.log('D1 pricing: $1.00/million writes, $0.001/million reads (25B free)');
365
+ console.log('KV pricing: $1.00/million writes, $0.50/million reads (10M free)');
366
+ console.log('');
367
+
368
+ for (const row of verification) {
369
+ console.log(`Project: ${row.project}`);
370
+ console.log(` D1 Writes: ${formatNumber(row.d1_rows_written as number)}`);
371
+ console.log(` D1 Recorded Cost: ${formatCost(row.d1_cost_usd as number)}`);
372
+ console.log(
373
+ ` D1 Expected Cost: ${formatCost(row.expected_d1_write_cost as number)} (writes only)`
374
+ );
375
+
376
+ const d1Diff = Math.abs((row.d1_cost_usd as number) - (row.expected_d1_write_cost as number));
377
+ if (d1Diff > 1) {
378
+ console.log(` ⚠️ D1 COST DISCREPANCY: ${formatCost(d1Diff)}`);
379
+ }
380
+
381
+ console.log(` KV Writes: ${formatNumber(row.kv_writes as number)}`);
382
+ console.log(` KV Recorded Cost: ${formatCost(row.kv_cost_usd as number)}`);
383
+ console.log(
384
+ ` KV Expected Cost: ${formatCost(row.expected_kv_write_cost as number)} (writes only)`
385
+ );
386
+
387
+ const kvDiff = Math.abs((row.kv_cost_usd as number) - (row.expected_kv_write_cost as number));
388
+ if (kvDiff > 1) {
389
+ console.log(` ⚠️ KV COST DISCREPANCY: ${formatCost(kvDiff)}`);
390
+ }
391
+
392
+ console.log(` Total Recorded Cost: ${formatCost(row.total_cost_usd as number)}`);
393
+ console.log('');
394
+ }
395
+ }
396
+
397
+ // Section 4: Hourly breakdown (if requested)
398
+ if (detailed && auditDate) {
399
+ console.log('─'.repeat(80));
400
+ console.log(`SECTION 4: HOURLY BREAKDOWN FOR ${auditDate}`);
401
+ console.log('─'.repeat(80));
402
+
403
+ const hourly = await getHourlyBreakdown(auditDate, accountId, apiToken, databaseId);
404
+ if (hourly.length === 0) {
405
+ console.log('No hourly data found (may have been rolled up and deleted).');
406
+ } else {
407
+ console.log('Hour | Project | D1 Writes | KV Writes | Cost');
408
+ console.log('---------------------|-------------|---------------|-------------|--------');
409
+ for (const row of hourly) {
410
+ console.log(
411
+ `${row.snapshot_hour} | ` +
412
+ `${String(row.project).padEnd(11)} | ` +
413
+ `${formatNumber(row.d1_rows_written).padStart(13)} | ` +
414
+ `${formatNumber(row.kv_writes).padStart(11)} | ` +
415
+ `${formatCost(row.total_cost_usd).padStart(6)}`
416
+ );
417
+ }
418
+ }
419
+ console.log('');
420
+ }
421
+
422
+ console.log('='.repeat(80));
423
+ console.log('AUDIT COMPLETE');
424
+ console.log('='.repeat(80));
425
+ }
426
+
427
+ main().catch((error) => {
428
+ console.error('Fatal error:', error);
429
+ process.exit(1);
430
+ });
@@ -0,0 +1,256 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * verify-account-total.ts
4
+ *
5
+ * Queries Cloudflare GraphQL for account-wide usage totals (not filtered by script).
6
+ * This provides the "ground truth" for what the account actually used, regardless
7
+ * of whether individual workers were deleted.
8
+ *
9
+ * Purpose: Determine if the ~$2,600 australian-history-mcp cost was REAL or PHANTOM
10
+ * by comparing account-level metrics to our D1 records.
11
+ *
12
+ * Usage:
13
+ * npx tsx scripts/ops/verify-account-total.ts
14
+ */
15
+
16
+ const GRAPHQL_ENDPOINT = 'https://api.cloudflare.com/client/v4/graphql';
17
+ const ACCOUNT_ID = '55a0bf6d1396d90cbf9dcbf30fceeb14';
18
+
19
+ // Pricing constants (Workers Paid Plan)
20
+ const WORKERS_INCLUDED_REQUESTS = 10_000_000; // 10M included
21
+ const WORKERS_REQUEST_COST_PER_MILLION = 0.3; // $0.30 per million after included
22
+ const WORKERS_CPU_COST_PER_MILLION_MS = 0.02; // $0.02 per million CPU ms (approx)
23
+
24
+ interface GraphQLResponse {
25
+ data?: {
26
+ viewer?: {
27
+ accounts?: Array<{
28
+ workersInvocationsAdaptive?: Array<{
29
+ sum: {
30
+ requests: number;
31
+ errors: number;
32
+ subrequests: number;
33
+ };
34
+ quantiles: {
35
+ cpuTimeP50: number;
36
+ cpuTimeP99: number;
37
+ };
38
+ dimensions: {
39
+ date: string;
40
+ };
41
+ }>;
42
+ }>;
43
+ };
44
+ };
45
+ errors?: Array<{ message: string }>;
46
+ }
47
+
48
+ async function queryAccountTotal(
49
+ apiToken: string,
50
+ startDate: string,
51
+ endDate: string
52
+ ): Promise<{
53
+ totalRequests: number;
54
+ totalErrors: number;
55
+ totalSubrequests: number;
56
+ avgCpuTimeP50: number;
57
+ avgCpuTimeP99: number;
58
+ dailyBreakdown: Array<{ date: string; requests: number }>;
59
+ }> {
60
+ const query = `
61
+ query AccountUsage($accountTag: String!, $startDate: Time!, $endDate: Time!) {
62
+ viewer {
63
+ accounts(filter: { accountTag: $accountTag }) {
64
+ workersInvocationsAdaptive(
65
+ filter: {
66
+ datetime_geq: $startDate
67
+ datetime_lt: $endDate
68
+ }
69
+ limit: 1000
70
+ ) {
71
+ sum {
72
+ requests
73
+ errors
74
+ subrequests
75
+ }
76
+ quantiles {
77
+ cpuTimeP50
78
+ cpuTimeP99
79
+ }
80
+ dimensions {
81
+ date
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+ `;
88
+
89
+ const response = await fetch(GRAPHQL_ENDPOINT, {
90
+ method: 'POST',
91
+ headers: {
92
+ Authorization: `Bearer ${apiToken}`,
93
+ 'Content-Type': 'application/json',
94
+ },
95
+ body: JSON.stringify({
96
+ query,
97
+ variables: {
98
+ accountTag: ACCOUNT_ID,
99
+ startDate: `${startDate}T00:00:00Z`,
100
+ endDate: `${endDate}T00:00:00Z`,
101
+ },
102
+ }),
103
+ });
104
+
105
+ const result = (await response.json()) as GraphQLResponse;
106
+
107
+ if (result.errors?.length) {
108
+ throw new Error(`GraphQL errors: ${result.errors.map((e) => e.message).join(', ')}`);
109
+ }
110
+
111
+ const data = result.data?.viewer?.accounts?.[0]?.workersInvocationsAdaptive ?? [];
112
+
113
+ let totalRequests = 0;
114
+ let totalErrors = 0;
115
+ let totalSubrequests = 0;
116
+ let cpuTimeP50Sum = 0;
117
+ let cpuTimeP99Sum = 0;
118
+ const dailyBreakdown: Array<{ date: string; requests: number }> = [];
119
+
120
+ for (const row of data) {
121
+ totalRequests += row.sum.requests;
122
+ totalErrors += row.sum.errors;
123
+ totalSubrequests += row.sum.subrequests;
124
+ cpuTimeP50Sum += row.quantiles.cpuTimeP50 * row.sum.requests;
125
+ cpuTimeP99Sum += row.quantiles.cpuTimeP99 * row.sum.requests;
126
+ dailyBreakdown.push({
127
+ date: row.dimensions.date,
128
+ requests: row.sum.requests,
129
+ });
130
+ }
131
+
132
+ return {
133
+ totalRequests,
134
+ totalErrors,
135
+ totalSubrequests,
136
+ avgCpuTimeP50: totalRequests > 0 ? cpuTimeP50Sum / totalRequests : 0,
137
+ avgCpuTimeP99: totalRequests > 0 ? cpuTimeP99Sum / totalRequests : 0,
138
+ dailyBreakdown: dailyBreakdown.sort((a, b) => a.date.localeCompare(b.date)),
139
+ };
140
+ }
141
+
142
+ function calculateWorkersCost(requests: number, avgCpuTimeMs: number): number {
143
+ // Billable requests (after 10M included)
144
+ const billableRequests = Math.max(0, requests - WORKERS_INCLUDED_REQUESTS);
145
+ const requestCost = (billableRequests / 1_000_000) * WORKERS_REQUEST_COST_PER_MILLION;
146
+
147
+ // CPU time cost (rough estimate based on total CPU ms)
148
+ // This is approximate - actual billing is more complex
149
+ const totalCpuMs = requests * avgCpuTimeMs;
150
+ const cpuCost = (totalCpuMs / 1_000_000) * WORKERS_CPU_COST_PER_MILLION_MS;
151
+
152
+ return requestCost + cpuCost;
153
+ }
154
+
155
+ async function main(): Promise<void> {
156
+ const apiToken = process.env.CLOUDFLARE_API_TOKEN;
157
+
158
+ if (!apiToken) {
159
+ console.error('Error: CLOUDFLARE_API_TOKEN environment variable is required');
160
+ process.exit(1);
161
+ }
162
+
163
+ console.log('='.repeat(70));
164
+ console.log('ACCOUNT-WIDE USAGE VERIFICATION');
165
+ console.log('='.repeat(70));
166
+ console.log('Purpose: Determine if the ~$2,600 was REAL or PHANTOM');
167
+ console.log('Method: Query account-level metrics (not filtered by script name)');
168
+ console.log('');
169
+
170
+ // Query January 2026
171
+ const startDate = '2026-01-01';
172
+ const endDate = '2026-02-01';
173
+
174
+ console.log(`Period: ${startDate} to ${endDate}`);
175
+ console.log('');
176
+ console.log('-'.repeat(70));
177
+ console.log('QUERYING CLOUDFLARE GRAPHQL...');
178
+ console.log('-'.repeat(70));
179
+
180
+ const metrics = await queryAccountTotal(apiToken, startDate, endDate);
181
+
182
+ console.log('');
183
+ console.log('ACCOUNT-WIDE TOTALS (January 2026):');
184
+ console.log('-'.repeat(70));
185
+ console.log(` Total Requests: ${metrics.totalRequests.toLocaleString()}`);
186
+ console.log(` Total Errors: ${metrics.totalErrors.toLocaleString()}`);
187
+ console.log(` Total Subrequests: ${metrics.totalSubrequests.toLocaleString()}`);
188
+ console.log(` Avg CPU Time (P50): ${metrics.avgCpuTimeP50.toFixed(2)} ms`);
189
+ console.log(` Avg CPU Time (P99): ${metrics.avgCpuTimeP99.toFixed(2)} ms`);
190
+ console.log('');
191
+
192
+ // Calculate estimated cost
193
+ const estimatedCost = calculateWorkersCost(metrics.totalRequests, metrics.avgCpuTimeP50);
194
+
195
+ console.log('ESTIMATED WORKERS COST:');
196
+ console.log('-'.repeat(70));
197
+ console.log(` Included requests: 10,000,000`);
198
+ console.log(
199
+ ` Billable requests: ${Math.max(0, metrics.totalRequests - WORKERS_INCLUDED_REQUESTS).toLocaleString()}`
200
+ );
201
+ console.log(` Estimated cost: $${estimatedCost.toFixed(2)}`);
202
+ console.log('');
203
+
204
+ // Daily breakdown (top 10 days)
205
+ console.log('TOP 10 DAYS BY REQUESTS:');
206
+ console.log('-'.repeat(70));
207
+ const topDays = [...metrics.dailyBreakdown].sort((a, b) => b.requests - a.requests).slice(0, 10);
208
+ for (const day of topDays) {
209
+ console.log(` ${day.date}: ${day.requests.toLocaleString()} requests`);
210
+ }
211
+ console.log('');
212
+
213
+ // The Verdict
214
+ console.log('='.repeat(70));
215
+ console.log('THE VERDICT');
216
+ console.log('='.repeat(70));
217
+
218
+ if (metrics.totalRequests > 500_000) {
219
+ console.log('');
220
+ console.log('⚠️ VERDICT: POTENTIALLY REAL');
221
+ console.log('');
222
+ console.log('The account had significant traffic. However, this could be from:');
223
+ console.log(' 1. Known workers (scout, brand-copilot, platform-*)');
224
+ console.log(' 2. The deleted semantic-librarian worker');
225
+ console.log('');
226
+ console.log('Next step: Compare against D1 totals to find the gap.');
227
+ } else if (estimatedCost > 2000) {
228
+ console.log('');
229
+ console.log('🔴 VERDICT: REAL');
230
+ console.log('');
231
+ console.log('Account-level costs exceed $2,000.');
232
+ console.log('The deleted worker likely had real usage.');
233
+ console.log('Recommendation: Create placeholder record for billing reconciliation.');
234
+ } else if (estimatedCost < 500) {
235
+ console.log('');
236
+ console.log('✅ VERDICT: PHANTOM');
237
+ console.log('');
238
+ console.log('Account-level costs are under $500.');
239
+ console.log('The $2,600 was likely a data artifact from triple-counting.');
240
+ console.log('Current D1 totals (~$416) are correct.');
241
+ } else {
242
+ console.log('');
243
+ console.log('⚠️ VERDICT: INCONCLUSIVE');
244
+ console.log('');
245
+ console.log('Account costs are in the middle range ($500-$2000).');
246
+ console.log('Manual investigation recommended.');
247
+ }
248
+
249
+ console.log('');
250
+ console.log('='.repeat(70));
251
+ }
252
+
253
+ main().catch((error) => {
254
+ console.error('Fatal error:', error);
255
+ process.exit(1);
256
+ });