@littlebearapps/platform-admin-sdk 1.5.0 → 2.1.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 (157) hide show
  1. package/README.md +2 -2
  2. package/dist/templates.d.ts +1 -1
  3. package/dist/templates.js +197 -2
  4. package/package.json +1 -1
  5. package/templates/full/dashboard/src/components/patterns/ActivePatterns.tsx +62 -0
  6. package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
  7. package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
  8. package/templates/full/dashboard/src/components/patterns/index.ts +3 -0
  9. package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
  10. package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
  11. package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
  12. package/templates/full/dashboard/src/components/reports/SdkAuditReport.tsx +72 -0
  13. package/templates/full/dashboard/src/components/reports/index.ts +2 -0
  14. package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
  15. package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
  16. package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
  17. package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
  18. package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
  19. package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
  20. package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
  21. package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
  22. package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
  23. package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
  24. package/templates/full/dashboard/src/lib/search/api.ts +258 -0
  25. package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
  26. package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
  27. package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
  28. package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
  29. package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
  30. package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
  31. package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -0
  32. package/templates/full/dashboard/src/pages/api/patterns/cache-refresh.ts +38 -0
  33. package/templates/full/dashboard/src/pages/api/patterns/discover.ts +36 -0
  34. package/templates/full/dashboard/src/pages/api/patterns/ready-for-review.ts +39 -0
  35. package/templates/full/dashboard/src/pages/api/patterns/stats.ts +39 -0
  36. package/templates/full/dashboard/src/pages/api/patterns/suggestions.ts +43 -0
  37. package/templates/full/dashboard/src/pages/api/reports/audit.ts +45 -0
  38. package/templates/full/dashboard/src/pages/api/reports/usage.ts +52 -0
  39. package/templates/full/dashboard/src/pages/api/search/reindex.ts +28 -0
  40. package/templates/full/dashboard/src/pages/api/search/stats.ts +27 -0
  41. package/templates/full/dashboard/src/pages/api/settings/index.ts +37 -0
  42. package/templates/full/dashboard/src/pages/api/settings/update.ts +41 -0
  43. package/templates/full/dashboard/src/pages/api/topology/index.ts +56 -0
  44. package/templates/full/scripts/ops/universal-backfill.ts +147 -0
  45. package/templates/shared/.github/workflows/contract-check.yml.hbs +42 -0
  46. package/templates/shared/.github/workflows/dashboard-deploy.yml.hbs +39 -0
  47. package/templates/shared/.github/workflows/security.yml +33 -0
  48. package/templates/shared/dashboard/src/components/Nav.astro.hbs +2 -0
  49. package/templates/shared/dashboard/src/components/infrastructure/AlertHistory.tsx +57 -0
  50. package/templates/shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx +73 -0
  51. package/templates/shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx +55 -0
  52. package/templates/shared/dashboard/src/components/infrastructure/UptimeStatus.tsx +56 -0
  53. package/templates/shared/dashboard/src/components/infrastructure/index.ts +4 -0
  54. package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
  55. package/templates/shared/dashboard/src/components/ui/Breadcrumbs.tsx +27 -0
  56. package/templates/shared/dashboard/src/components/ui/EmptyState.tsx +26 -0
  57. package/templates/shared/dashboard/src/components/ui/ErrorBoundary.tsx +42 -0
  58. package/templates/shared/dashboard/src/components/ui/LoadingSkeleton.tsx +18 -0
  59. package/templates/shared/dashboard/src/components/ui/PageShell.tsx +26 -0
  60. package/templates/shared/dashboard/src/components/ui/Toast.tsx +44 -0
  61. package/templates/shared/dashboard/src/components/ui/index.ts +6 -0
  62. package/templates/shared/dashboard/src/components/usage/AnomaliesWidget.tsx +68 -0
  63. package/templates/shared/dashboard/src/components/usage/HourlyUsageChart.tsx +55 -0
  64. package/templates/shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx +67 -0
  65. package/templates/shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx +55 -0
  66. package/templates/shared/dashboard/src/components/usage/index.ts +4 -0
  67. package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
  68. package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
  69. package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
  70. package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
  71. package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
  72. package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
  73. package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
  74. package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
  75. package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
  76. package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
  77. package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
  78. package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
  79. package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
  80. package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
  81. package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
  82. package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
  83. package/templates/shared/dashboard/src/lib/cloudflare/costs.ts +21 -0
  84. package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
  85. package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
  86. package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
  87. package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
  88. package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
  89. package/templates/shared/dashboard/src/pages/api/costs/overview.ts +65 -0
  90. package/templates/shared/dashboard/src/pages/api/costs/providers.ts +47 -0
  91. package/templates/shared/dashboard/src/pages/api/infrastructure/services.ts +55 -0
  92. package/templates/shared/dashboard/src/pages/api/infrastructure/stats.ts +99 -0
  93. package/templates/shared/dashboard/src/pages/api/usage/allowances.ts +56 -0
  94. package/templates/shared/dashboard/src/pages/api/usage/anomalies.ts +45 -0
  95. package/templates/shared/dashboard/src/pages/api/usage/billing.ts +53 -0
  96. package/templates/shared/dashboard/src/pages/api/usage/granular.ts +50 -0
  97. package/templates/shared/dashboard/src/pages/api/usage/hourly.ts +45 -0
  98. package/templates/shared/dashboard/src/pages/api/usage/projects.ts +51 -0
  99. package/templates/shared/dashboard/src/pages/api/user/identity.ts +11 -0
  100. package/templates/shared/dashboard/src/pages/settings/notifications.astro +34 -0
  101. package/templates/shared/dashboard/src/pages/settings/thresholds.astro +39 -0
  102. package/templates/shared/dashboard/src/pages/settings/usage.astro +28 -0
  103. package/templates/shared/docs/architecture.md +89 -0
  104. package/templates/shared/docs/post-deploy-runbook.md +126 -0
  105. package/templates/shared/docs/troubleshooting.md +91 -0
  106. package/templates/shared/package.json.hbs +5 -0
  107. package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
  108. package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -0
  109. package/templates/shared/scripts/ops/validate-controls.js +141 -0
  110. package/templates/shared/tests/contract/validate-schemas.test.ts +130 -0
  111. package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
  112. package/templates/shared/tests/fixtures/telemetry-envelope-invalid.json +9 -0
  113. package/templates/shared/tests/fixtures/telemetry-envelope-valid.json +27 -0
  114. package/templates/shared/tests/helpers/mock-d1.ts +61 -0
  115. package/templates/shared/tests/helpers/mock-kv.ts +37 -0
  116. package/templates/shared/tests/helpers/mock-storage.ts +166 -0
  117. package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
  118. package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
  119. package/templates/shared/tests/unit/billing.test.ts +331 -0
  120. package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
  121. package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
  122. package/templates/shared/tests/unit/control.test.ts +226 -0
  123. package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
  124. package/templates/shared/tests/unit/economics.test.ts +365 -0
  125. package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
  126. package/templates/shared/tests/unit/workers/batch-persistence.test.ts +133 -0
  127. package/templates/shared/tests/unit/workers/budget-enforcement.test.ts +214 -0
  128. package/templates/shared/vitest.config.ts +18 -0
  129. package/templates/standard/dashboard/src/components/health/CircuitBreakerEvents.tsx +69 -0
  130. package/templates/standard/dashboard/src/components/health/CircuitBreakerPanel.tsx +97 -0
  131. package/templates/standard/dashboard/src/components/health/index.ts +2 -0
  132. package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
  133. package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
  134. package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
  135. package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
  136. package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
  137. package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
  138. package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
  139. package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
  140. package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
  141. package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
  142. package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
  143. package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
  144. package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
  145. package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
  146. package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
  147. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts +49 -0
  148. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts +36 -0
  149. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint].ts +55 -0
  150. package/templates/standard/dashboard/src/pages/api/health/audit-history.ts +37 -0
  151. package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -0
  152. package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
  153. package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
  154. package/templates/standard/tests/unit/error-collector/capture.test.ts +106 -0
  155. package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
  156. package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -0
  157. package/templates/standard/tests/unit/error-collector/github.test.ts +187 -0
@@ -0,0 +1,28 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const POST: APIRoute = async ({ locals }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ error: 'Database not available' }), {
8
+ status: 503,
9
+ headers: { 'Content-Type': 'application/json' },
10
+ });
11
+ }
12
+
13
+ try {
14
+ await db
15
+ .prepare(`INSERT INTO search_index(search_index) VALUES('rebuild')`)
16
+ .run();
17
+
18
+ return new Response(
19
+ JSON.stringify({ ok: true, message: 'FTS5 reindex started' }),
20
+ { headers: { 'Content-Type': 'application/json' } }
21
+ );
22
+ } catch {
23
+ return new Response(JSON.stringify({ error: 'Reindex failed' }), {
24
+ status: 500,
25
+ headers: { 'Content-Type': 'application/json' },
26
+ });
27
+ }
28
+ };
@@ -0,0 +1,27 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const GET: APIRoute = async ({ locals }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ indexed: 0, lastUpdated: null }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ try {
13
+ const count = await db
14
+ .prepare(`SELECT COUNT(*) as count FROM search_index LIMIT 1`)
15
+ .first<{ count: number }>();
16
+
17
+ return new Response(
18
+ JSON.stringify({ indexed: count?.count ?? 0 }),
19
+ { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=60' } }
20
+ );
21
+ } catch {
22
+ return new Response(JSON.stringify({ indexed: 0 }), {
23
+ status: 500,
24
+ headers: { 'Content-Type': 'application/json' },
25
+ });
26
+ }
27
+ };
@@ -0,0 +1,37 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const GET: APIRoute = async ({ locals }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ settings: {} }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ try {
13
+ const rows = await db
14
+ .prepare(
15
+ `SELECT key, value, updated_at
16
+ FROM platform_settings
17
+ ORDER BY key ASC
18
+ LIMIT 200`
19
+ )
20
+ .all<{ key: string; value: string; updated_at: string }>();
21
+
22
+ const settings: Record<string, { value: string; updatedAt: string }> = {};
23
+ for (const row of rows.results ?? []) {
24
+ settings[row.key] = { value: row.value, updatedAt: row.updated_at };
25
+ }
26
+
27
+ return new Response(
28
+ JSON.stringify({ settings }),
29
+ { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=30' } }
30
+ );
31
+ } catch {
32
+ return new Response(JSON.stringify({ settings: {} }), {
33
+ status: 500,
34
+ headers: { 'Content-Type': 'application/json' },
35
+ });
36
+ }
37
+ };
@@ -0,0 +1,41 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const POST: APIRoute = async ({ request, locals }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ error: 'Database not available' }), {
8
+ status: 503,
9
+ headers: { 'Content-Type': 'application/json' },
10
+ });
11
+ }
12
+
13
+ try {
14
+ const body = (await request.json()) as { key?: string; value?: string };
15
+ if (!body.key || body.value === undefined) {
16
+ return new Response(JSON.stringify({ error: 'key and value are required' }), {
17
+ status: 400,
18
+ headers: { 'Content-Type': 'application/json' },
19
+ });
20
+ }
21
+
22
+ await db
23
+ .prepare(
24
+ `INSERT INTO platform_settings (key, value, updated_at)
25
+ VALUES (?, ?, datetime('now'))
26
+ ON CONFLICT (key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`
27
+ )
28
+ .bind(body.key, body.value)
29
+ .run();
30
+
31
+ return new Response(
32
+ JSON.stringify({ ok: true, key: body.key }),
33
+ { headers: { 'Content-Type': 'application/json' } }
34
+ );
35
+ } catch {
36
+ return new Response(JSON.stringify({ error: 'Update failed' }), {
37
+ status: 500,
38
+ headers: { 'Content-Type': 'application/json' },
39
+ });
40
+ }
41
+ };
@@ -0,0 +1,56 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const GET: APIRoute = async ({ locals }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ nodes: [], edges: [] }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ try {
13
+ const [workers, bindings] = await Promise.all([
14
+ db
15
+ .prepare(
16
+ `SELECT script_name, project, worker_type, last_seen_at
17
+ FROM resource_project_mapping
18
+ WHERE resource_type = 'worker'
19
+ ORDER BY script_name ASC
20
+ LIMIT 100`
21
+ )
22
+ .all(),
23
+ db
24
+ .prepare(
25
+ `SELECT source_worker, target_worker, binding_type
26
+ FROM service_bindings
27
+ ORDER BY source_worker ASC
28
+ LIMIT 200`
29
+ )
30
+ .all(),
31
+ ]);
32
+
33
+ const nodes = (workers.results ?? []).map((w: Record<string, unknown>) => ({
34
+ id: w.script_name as string,
35
+ project: w.project as string,
36
+ type: w.worker_type as string,
37
+ lastSeen: w.last_seen_at as string,
38
+ }));
39
+
40
+ const edges = (bindings.results ?? []).map((b: Record<string, unknown>) => ({
41
+ from: b.source_worker as string,
42
+ to: b.target_worker as string,
43
+ type: b.binding_type as string,
44
+ }));
45
+
46
+ return new Response(
47
+ JSON.stringify({ nodes, edges }),
48
+ { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=300' } }
49
+ );
50
+ } catch {
51
+ return new Response(JSON.stringify({ nodes: [], edges: [] }), {
52
+ status: 500,
53
+ headers: { 'Content-Type': 'application/json' },
54
+ });
55
+ }
56
+ };
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Universal Backfill Script (Full Tier)
4
+ *
5
+ * Orchestrates the hourly → daily → monthly cascade with progress reporting.
6
+ * Runs each step in sequence, passing date ranges through the pipeline.
7
+ *
8
+ * Prerequisites:
9
+ * CLOUDFLARE_API_TOKEN — API token with Analytics:Read + D1:Write permissions
10
+ * CLOUDFLARE_ACCOUNT_ID — Your Cloudflare account ID
11
+ * D1_DATABASE_ID — Your platform-metrics D1 database ID
12
+ *
13
+ * Usage:
14
+ * npx tsx scripts/ops/universal-backfill.ts
15
+ * npx tsx scripts/ops/universal-backfill.ts --dry-run
16
+ * npx tsx scripts/ops/universal-backfill.ts --start 2026-02-01 --end 2026-02-28
17
+ * npx tsx scripts/ops/universal-backfill.ts --skip-hourly
18
+ */
19
+
20
+ import { execSync } from 'node:child_process';
21
+ import { resolve } from 'node:path';
22
+
23
+ const SCRIPTS_DIR = resolve(import.meta.dirname);
24
+
25
+ interface Args {
26
+ start?: string;
27
+ end?: string;
28
+ dryRun: boolean;
29
+ skipHourly: boolean;
30
+ }
31
+
32
+ function parseArgs(): Args {
33
+ const args = process.argv.slice(2);
34
+ const result: Args = { dryRun: false, skipHourly: false };
35
+
36
+ for (let i = 0; i < args.length; i++) {
37
+ if (args[i] === '--start' && args[i + 1]) result.start = args[++i];
38
+ else if (args[i] === '--end' && args[i + 1]) result.end = args[++i];
39
+ else if (args[i] === '--dry-run') result.dryRun = true;
40
+ else if (args[i] === '--skip-hourly') result.skipHourly = true;
41
+ }
42
+
43
+ if (!result.start) {
44
+ const d = new Date();
45
+ d.setDate(d.getDate() - 7);
46
+ result.start = d.toISOString().slice(0, 10);
47
+ }
48
+ if (!result.end) {
49
+ result.end = new Date().toISOString().slice(0, 10);
50
+ }
51
+
52
+ return result;
53
+ }
54
+
55
+ function runStep(name: string, script: string, extraArgs: string[]) {
56
+ console.log(`\n${'='.repeat(60)}`);
57
+ console.log(`Step: ${name}`);
58
+ console.log('='.repeat(60));
59
+
60
+ const cmd = `npx tsx ${script} ${extraArgs.join(' ')}`;
61
+ console.log(`Running: ${cmd}\n`);
62
+
63
+ try {
64
+ execSync(cmd, { stdio: 'inherit', env: process.env });
65
+ console.log(`\n${name}: COMPLETE`);
66
+ return true;
67
+ } catch (err) {
68
+ console.error(`\n${name}: FAILED`);
69
+ return false;
70
+ }
71
+ }
72
+
73
+ async function main() {
74
+ const args = parseArgs();
75
+ const dateArgs = [];
76
+ if (args.start) dateArgs.push('--start', args.start);
77
+ if (args.end) dateArgs.push('--end', args.end);
78
+ if (args.dryRun) dateArgs.push('--dry-run');
79
+
80
+ console.log('Universal Backfill Pipeline');
81
+ console.log(`Range: ${args.start} → ${args.end}`);
82
+ console.log(`Dry run: ${args.dryRun}`);
83
+ console.log(`Skip hourly: ${args.skipHourly}`);
84
+
85
+ const startTime = Date.now();
86
+ const results: Array<{ step: string; success: boolean }> = [];
87
+
88
+ // Step 1: Hourly backfill (optional — requires GraphQL API, rate-limited)
89
+ if (!args.skipHourly) {
90
+ const success = runStep(
91
+ '1/3: Hourly Backfill',
92
+ resolve(SCRIPTS_DIR, 'backfill-cloudflare-hourly.ts'),
93
+ dateArgs
94
+ );
95
+ results.push({ step: 'Hourly', success });
96
+ if (!success && !args.dryRun) {
97
+ console.warn('\nHourly backfill failed — continuing with daily rollups using existing hourly data.');
98
+ }
99
+ } else {
100
+ console.log('\nSkipping hourly backfill (--skip-hourly)');
101
+ results.push({ step: 'Hourly', success: true });
102
+ }
103
+
104
+ // Step 2: Daily rollups
105
+ const dailySuccess = runStep(
106
+ '2/3: Daily Rollups',
107
+ resolve(SCRIPTS_DIR, 'backfill-cloudflare-daily.ts'),
108
+ dateArgs
109
+ );
110
+ results.push({ step: 'Daily', success: dailySuccess });
111
+
112
+ // Step 3: Monthly rollups
113
+ const monthStart = args.start?.slice(0, 7) ?? '';
114
+ const monthEnd = args.end?.slice(0, 7) ?? '';
115
+ const monthlyArgs = [];
116
+ if (monthStart) monthlyArgs.push('--start', monthStart);
117
+ if (monthEnd) monthlyArgs.push('--end', monthEnd);
118
+ if (args.dryRun) monthlyArgs.push('--dry-run');
119
+
120
+ const monthlySuccess = runStep(
121
+ '3/3: Monthly Rollups',
122
+ resolve(SCRIPTS_DIR, 'backfill-monthly-rollups.ts'),
123
+ monthlyArgs
124
+ );
125
+ results.push({ step: 'Monthly', success: monthlySuccess });
126
+
127
+ // Summary
128
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
129
+ console.log(`\n${'='.repeat(60)}`);
130
+ console.log('Pipeline Summary');
131
+ console.log('='.repeat(60));
132
+ for (const r of results) {
133
+ console.log(` ${r.success ? 'PASS' : 'FAIL'} ${r.step}`);
134
+ }
135
+ console.log(`\nTotal time: ${elapsed}s`);
136
+
137
+ const failed = results.filter((r) => !r.success);
138
+ if (failed.length > 0) {
139
+ console.error(`\n${failed.length} step(s) failed.`);
140
+ process.exit(1);
141
+ }
142
+ }
143
+
144
+ main().catch((err) => {
145
+ console.error('Fatal error:', err);
146
+ process.exit(1);
147
+ });
@@ -0,0 +1,42 @@
1
+ # Contract Schema Validation
2
+ #
3
+ # Validates JSON schemas and TypeScript types match expected contracts.
4
+ # Runs schema validation against fixture data.
5
+ #
6
+ # Checks performed:
7
+ # - JSON schema validation (envelope, error_report)
8
+ # - TypeScript type compilation
9
+ # - Contract fixture validation
10
+
11
+ name: Contract Check
12
+
13
+ on:
14
+ push:
15
+ branches: [main]
16
+ paths:
17
+ - 'contracts/**'
18
+ - 'tests/contract/**'
19
+ pull_request:
20
+ branches: [main]
21
+ paths:
22
+ - 'contracts/**'
23
+ - 'tests/contract/**'
24
+
25
+ jobs:
26
+ validate:
27
+ runs-on: ubuntu-latest
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+
31
+ - uses: actions/setup-node@v4
32
+ with:
33
+ node-version: '22'
34
+ cache: 'npm'
35
+
36
+ - run: npm ci
37
+
38
+ - name: Validate schemas
39
+ run: npm run validate:schemas
40
+
41
+ - name: Run contract tests
42
+ run: npx vitest run tests/contract/
@@ -0,0 +1,39 @@
1
+ name: Deploy Dashboard
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ paths:
7
+ - 'dashboard/**'
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ deploy:
12
+ runs-on: ubuntu-latest
13
+ permissions:
14
+ contents: read
15
+ deployments: write
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: actions/setup-node@v4
20
+ with:
21
+ node-version: 20
22
+ cache: npm
23
+ cache-dependency-path: dashboard/package-lock.json
24
+
25
+ - name: Install dependencies
26
+ working-directory: dashboard
27
+ run: npm ci
28
+
29
+ - name: Build
30
+ working-directory: dashboard
31
+ run: npm run build
32
+
33
+ - name: Deploy to Cloudflare Pages
34
+ uses: cloudflare/wrangler-action@v3
35
+ with:
36
+ apiToken: $\{{ secrets.CLOUDFLARE_API_TOKEN }}
37
+ accountId: $\{{ secrets.CLOUDFLARE_ACCOUNT_ID }}
38
+ command: pages deploy dist --project-name={{projectSlug}}-dashboard
39
+ workingDirectory: dashboard
@@ -0,0 +1,33 @@
1
+ # Security Scanning
2
+ #
3
+ # Runs dependency audit and secret detection on pushes and PRs.
4
+
5
+ name: Security
6
+
7
+ on:
8
+ push:
9
+ branches: [main]
10
+ pull_request:
11
+ branches: [main]
12
+
13
+ jobs:
14
+ audit:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: actions/setup-node@v4
20
+ with:
21
+ node-version: '22'
22
+ cache: 'npm'
23
+
24
+ - run: npm ci
25
+
26
+ - name: Audit dependencies
27
+ run: npm audit --audit-level=high
28
+ continue-on-error: true
29
+
30
+ - name: Check for secrets
31
+ uses: trufflesecurity/trufflehog@main
32
+ with:
33
+ extra_args: --only-verified
@@ -13,6 +13,7 @@ const navItems: NavItem[] = [
13
13
  {{#if isStandard}}
14
14
  { href: '/health', label: 'Health', icon: 'activity' },
15
15
  { href: '/errors', label: 'Errors', icon: 'alert-triangle' },
16
+ { href: '/circuit-breakers', label: 'Breakers', icon: 'zap' },
16
17
  {{/if}}
17
18
  {{#if isFull}}
18
19
  { href: '/notifications', label: 'Notifications', icon: 'bell' },
@@ -47,6 +48,7 @@ function isActive(href: string): boolean {
47
48
  {item.icon === 'server' && '\u229E'}
48
49
  {item.icon === 'activity' && '\u2661'}
49
50
  {item.icon === 'alert-triangle' && '\u25B3'}
51
+ {item.icon === 'zap' && '\u26A1'}
50
52
  {item.icon === 'bell' && '\uD83D\uDD14'}
51
53
  {item.icon === 'settings' && '\u2699'}
52
54
  </span>
@@ -0,0 +1,57 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { LoadingSkeleton } from '../ui/LoadingSkeleton';
3
+ import { EmptyState } from '../ui/EmptyState';
4
+
5
+ interface Alert {
6
+ id: number;
7
+ title: string;
8
+ severity: string;
9
+ source: string;
10
+ created_at: string;
11
+ resolved_at?: string;
12
+ }
13
+
14
+ export function AlertHistory() {
15
+ const [alerts, setAlerts] = useState<Alert[]>([]);
16
+ const [loading, setLoading] = useState(true);
17
+
18
+ useEffect(() => {
19
+ fetch('/api/usage/anomalies?limit=20')
20
+ .then((r) => r.json())
21
+ .then((data: { anomalies: Alert[] }) => {
22
+ setAlerts(data.anomalies ?? []);
23
+ setLoading(false);
24
+ })
25
+ .catch(() => setLoading(false));
26
+ }, []);
27
+
28
+ if (loading) return <LoadingSkeleton lines={3} />;
29
+ if (alerts.length === 0) return <EmptyState title="No alerts" description="No recent alerts or anomalies detected." />;
30
+
31
+ const severityColour: Record<string, string> = {
32
+ critical: 'text-red-600 dark:text-red-400',
33
+ warning: 'text-yellow-600 dark:text-yellow-400',
34
+ info: 'text-blue-600 dark:text-blue-400',
35
+ };
36
+
37
+ return (
38
+ <div className="space-y-2">
39
+ {alerts.map((a) => (
40
+ <div
41
+ key={a.id}
42
+ className="flex items-start justify-between gap-3 py-2 px-3 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700"
43
+ >
44
+ <div className="min-w-0">
45
+ <p className={`text-sm font-medium ${severityColour[a.severity] ?? 'text-gray-900 dark:text-white'}`}>
46
+ {a.title}
47
+ </p>
48
+ <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">{a.source}</p>
49
+ </div>
50
+ <span className="text-xs text-gray-400 dark:text-gray-500 shrink-0">
51
+ {new Date(a.created_at).toLocaleDateString()}
52
+ </span>
53
+ </div>
54
+ ))}
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,73 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { LoadingSkeleton } from '../ui/LoadingSkeleton';
3
+
4
+ interface Stats {
5
+ services: {
6
+ total: number;
7
+ byStatus: Record<string, number>;
8
+ byType: Record<string, number>;
9
+ byProject: Record<string, number>;
10
+ };
11
+ alerts: {
12
+ total: number;
13
+ unacknowledged: number;
14
+ critical: number;
15
+ };
16
+ }
17
+
18
+ export function InfrastructureStats() {
19
+ const [data, setData] = useState<Stats | null>(null);
20
+ const [loading, setLoading] = useState(true);
21
+
22
+ useEffect(() => {
23
+ fetch('/api/infrastructure/stats')
24
+ .then(res => res.json())
25
+ .then((stats: Stats) => { setData(stats); setLoading(false); })
26
+ .catch(() => setLoading(false));
27
+ }, []);
28
+
29
+ if (loading) return <LoadingSkeleton lines={4} />;
30
+
31
+ if (!data) {
32
+ return <p className="text-sm text-gray-500 dark:text-gray-400">No infrastructure data available.</p>;
33
+ }
34
+
35
+ const cards = [
36
+ { label: 'Total Services', value: data.services.total, icon: '\u229E' },
37
+ { label: 'Deployed', value: data.services.byStatus.deployed ?? 0, icon: '\u2713' },
38
+ { label: 'Alerts', value: data.alerts.total, icon: '\u25B3' },
39
+ { label: 'Critical', value: data.alerts.critical, icon: '\u26A0' },
40
+ ];
41
+
42
+ return (
43
+ <div className="space-y-4">
44
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
45
+ {cards.map(card => (
46
+ <div key={card.label} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
47
+ <div className="flex items-center gap-2 mb-1">
48
+ <span className="opacity-50">{card.icon}</span>
49
+ <span className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">{card.label}</span>
50
+ </div>
51
+ <p className="text-2xl font-bold text-gray-900 dark:text-white">{card.value}</p>
52
+ </div>
53
+ ))}
54
+ </div>
55
+
56
+ {Object.keys(data.services.byType).length > 0 && (
57
+ <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
58
+ <h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
59
+ By Resource Type
60
+ </h3>
61
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-2">
62
+ {Object.entries(data.services.byType).map(([type, count]) => (
63
+ <div key={type} className="flex items-center justify-between py-1">
64
+ <span className="text-sm text-gray-600 dark:text-gray-300">{type}</span>
65
+ <span className="text-sm font-medium text-gray-900 dark:text-white">{count}</span>
66
+ </div>
67
+ ))}
68
+ </div>
69
+ </div>
70
+ )}
71
+ </div>
72
+ );
73
+ }
@@ -0,0 +1,55 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { EmptyState } from '../ui/EmptyState';
3
+ import { LoadingSkeleton } from '../ui/LoadingSkeleton';
4
+
5
+ interface Service {
6
+ script_name: string;
7
+ project: string;
8
+ resource_type: string;
9
+ last_seen_at?: string;
10
+ }
11
+
12
+ export function ServiceRegistry() {
13
+ const [services, setServices] = useState<Service[]>([]);
14
+ const [loading, setLoading] = useState(true);
15
+
16
+ useEffect(() => {
17
+ fetch('/api/infrastructure/services')
18
+ .then((r) => r.json())
19
+ .then((data: { services: Service[] }) => {
20
+ setServices(data.services ?? []);
21
+ setLoading(false);
22
+ })
23
+ .catch(() => setLoading(false));
24
+ }, []);
25
+
26
+ if (loading) return <LoadingSkeleton lines={5} />;
27
+ if (services.length === 0) return <EmptyState title="No services" description="Register services via services.yaml." />;
28
+
29
+ const grouped = services.reduce<Record<string, Service[]>>((acc, s) => {
30
+ const key = s.project || 'unassigned';
31
+ (acc[key] ??= []).push(s);
32
+ return acc;
33
+ }, {});
34
+
35
+ return (
36
+ <div className="space-y-4">
37
+ {Object.entries(grouped).map(([project, svcList]) => (
38
+ <div key={project}>
39
+ <h4 className="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400 mb-1">{project}</h4>
40
+ <div className="space-y-1">
41
+ {svcList.map((s) => (
42
+ <div
43
+ key={s.script_name}
44
+ className="flex items-center justify-between py-1 px-2 text-sm bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700"
45
+ >
46
+ <span className="font-mono text-gray-900 dark:text-white">{s.script_name}</span>
47
+ <span className="text-xs text-gray-400 dark:text-gray-500">{s.resource_type}</span>
48
+ </div>
49
+ ))}
50
+ </div>
51
+ </div>
52
+ ))}
53
+ </div>
54
+ );
55
+ }