@littlebearapps/platform-admin-sdk 1.4.2 → 2.0.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 (189) hide show
  1. package/dist/templates.d.ts +1 -1
  2. package/dist/templates.js +232 -2
  3. package/package.json +1 -1
  4. package/templates/full/config/audit-targets.yaml +72 -0
  5. package/templates/full/dashboard/src/components/notifications/NotificationBell.tsx +30 -0
  6. package/templates/full/dashboard/src/components/notifications/NotificationList.tsx +116 -0
  7. package/templates/full/dashboard/src/components/notifications/index.ts +2 -0
  8. package/templates/full/dashboard/src/components/patterns/ActivePatterns.tsx +62 -0
  9. package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
  10. package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
  11. package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
  12. package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
  13. package/templates/full/dashboard/src/components/patterns/index.ts +5 -0
  14. package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
  15. package/templates/full/dashboard/src/components/reports/SdkAuditReport.tsx +72 -0
  16. package/templates/full/dashboard/src/components/reports/index.ts +2 -0
  17. package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
  18. package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
  19. package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
  20. package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -0
  21. package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
  22. package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
  23. package/templates/full/dashboard/src/pages/api/patterns/cache-refresh.ts +38 -0
  24. package/templates/full/dashboard/src/pages/api/patterns/discover.ts +36 -0
  25. package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
  26. package/templates/full/dashboard/src/pages/api/patterns/ready-for-review.ts +39 -0
  27. package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
  28. package/templates/full/dashboard/src/pages/api/patterns/stats.ts +39 -0
  29. package/templates/full/dashboard/src/pages/api/patterns/suggestions.ts +43 -0
  30. package/templates/full/dashboard/src/pages/api/reports/audit.ts +45 -0
  31. package/templates/full/dashboard/src/pages/api/reports/usage.ts +52 -0
  32. package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
  33. package/templates/full/dashboard/src/pages/api/search/reindex.ts +28 -0
  34. package/templates/full/dashboard/src/pages/api/search/stats.ts +27 -0
  35. package/templates/full/dashboard/src/pages/api/settings/index.ts +37 -0
  36. package/templates/full/dashboard/src/pages/api/settings/update.ts +41 -0
  37. package/templates/full/dashboard/src/pages/api/topology/index.ts +56 -0
  38. package/templates/full/dashboard/src/pages/notifications.astro +11 -0
  39. package/templates/full/migrations/008_auditor.sql +99 -0
  40. package/templates/full/migrations/010_pricing_versions.sql +110 -0
  41. package/templates/full/migrations/011_multi_account.sql +51 -0
  42. package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
  43. package/templates/full/scripts/ops/universal-backfill.ts +147 -0
  44. package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
  45. package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
  46. package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
  47. package/templates/full/workers/lib/auditor/index.ts +9 -0
  48. package/templates/full/workers/lib/auditor/types.ts +167 -0
  49. package/templates/full/workers/platform-auditor.ts +1071 -0
  50. package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
  51. package/templates/shared/.github/workflows/contract-check.yml.hbs +42 -0
  52. package/templates/shared/.github/workflows/dashboard-deploy.yml.hbs +39 -0
  53. package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
  54. package/templates/shared/.github/workflows/security.yml +33 -0
  55. package/templates/shared/config/observability.yaml.hbs +276 -0
  56. package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
  57. package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
  58. package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
  59. package/templates/shared/dashboard/astro.config.mjs +21 -0
  60. package/templates/shared/dashboard/package.json.hbs +29 -0
  61. package/templates/shared/dashboard/src/components/Header.astro +29 -0
  62. package/templates/shared/dashboard/src/components/Nav.astro.hbs +59 -0
  63. package/templates/shared/dashboard/src/components/infrastructure/AlertHistory.tsx +57 -0
  64. package/templates/shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx +73 -0
  65. package/templates/shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx +55 -0
  66. package/templates/shared/dashboard/src/components/infrastructure/UptimeStatus.tsx +56 -0
  67. package/templates/shared/dashboard/src/components/infrastructure/index.ts +4 -0
  68. package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
  69. package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
  70. package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
  71. package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
  72. package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
  73. package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
  74. package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
  75. package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
  76. package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
  77. package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
  78. package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
  79. package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
  80. package/templates/shared/dashboard/src/components/ui/Breadcrumbs.tsx +27 -0
  81. package/templates/shared/dashboard/src/components/ui/EmptyState.tsx +26 -0
  82. package/templates/shared/dashboard/src/components/ui/ErrorBoundary.tsx +42 -0
  83. package/templates/shared/dashboard/src/components/ui/LoadingSkeleton.tsx +18 -0
  84. package/templates/shared/dashboard/src/components/ui/PageShell.tsx +26 -0
  85. package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
  86. package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
  87. package/templates/shared/dashboard/src/components/ui/Toast.tsx +44 -0
  88. package/templates/shared/dashboard/src/components/ui/index.ts +9 -0
  89. package/templates/shared/dashboard/src/components/usage/AnomaliesWidget.tsx +68 -0
  90. package/templates/shared/dashboard/src/components/usage/HourlyUsageChart.tsx +55 -0
  91. package/templates/shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx +67 -0
  92. package/templates/shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx +55 -0
  93. package/templates/shared/dashboard/src/components/usage/index.ts +4 -0
  94. package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
  95. package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
  96. package/templates/shared/dashboard/src/lib/cloudflare/costs.ts +21 -0
  97. package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
  98. package/templates/shared/dashboard/src/lib/types.ts +72 -0
  99. package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
  100. package/templates/shared/dashboard/src/middleware/index.ts +1 -0
  101. package/templates/shared/dashboard/src/pages/api/costs/overview.ts +65 -0
  102. package/templates/shared/dashboard/src/pages/api/costs/providers.ts +47 -0
  103. package/templates/shared/dashboard/src/pages/api/infrastructure/services.ts +55 -0
  104. package/templates/shared/dashboard/src/pages/api/infrastructure/stats.ts +99 -0
  105. package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
  106. package/templates/shared/dashboard/src/pages/api/usage/allowances.ts +56 -0
  107. package/templates/shared/dashboard/src/pages/api/usage/anomalies.ts +45 -0
  108. package/templates/shared/dashboard/src/pages/api/usage/billing.ts +53 -0
  109. package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
  110. package/templates/shared/dashboard/src/pages/api/usage/granular.ts +50 -0
  111. package/templates/shared/dashboard/src/pages/api/usage/hourly.ts +45 -0
  112. package/templates/shared/dashboard/src/pages/api/usage/projects.ts +51 -0
  113. package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
  114. package/templates/shared/dashboard/src/pages/api/user/identity.ts +11 -0
  115. package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
  116. package/templates/shared/dashboard/src/pages/index.astro +3 -0
  117. package/templates/shared/dashboard/src/pages/resources.astro +11 -0
  118. package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
  119. package/templates/shared/dashboard/src/pages/settings/notifications.astro +34 -0
  120. package/templates/shared/dashboard/src/pages/settings/thresholds.astro +39 -0
  121. package/templates/shared/dashboard/src/pages/settings/usage.astro +28 -0
  122. package/templates/shared/dashboard/src/styles/global.css +29 -0
  123. package/templates/shared/dashboard/tailwind.config.mjs +9 -0
  124. package/templates/shared/dashboard/tsconfig.json +9 -0
  125. package/templates/shared/dashboard/wrangler.json.hbs +47 -0
  126. package/templates/shared/docs/architecture.md +89 -0
  127. package/templates/shared/docs/post-deploy-runbook.md +126 -0
  128. package/templates/shared/docs/troubleshooting.md +91 -0
  129. package/templates/shared/package.json.hbs +17 -1
  130. package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
  131. package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
  132. package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -0
  133. package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
  134. package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
  135. package/templates/shared/scripts/ops/validate-controls.js +141 -0
  136. package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
  137. package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
  138. package/templates/shared/scripts/validate-schemas.js +61 -0
  139. package/templates/shared/tests/contract/validate-schemas.test.ts +130 -0
  140. package/templates/shared/tests/fixtures/telemetry-envelope-invalid.json +9 -0
  141. package/templates/shared/tests/fixtures/telemetry-envelope-valid.json +27 -0
  142. package/templates/shared/tests/helpers/mock-d1.ts +61 -0
  143. package/templates/shared/tests/helpers/mock-kv.ts +37 -0
  144. package/templates/shared/tests/unit/workers/batch-persistence.test.ts +133 -0
  145. package/templates/shared/tests/unit/workers/budget-enforcement.test.ts +214 -0
  146. package/templates/shared/vitest.config.ts +18 -0
  147. package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
  148. package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
  149. package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
  150. package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
  151. package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
  152. package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
  153. package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
  154. package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
  155. package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
  156. package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
  157. package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
  158. package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
  159. package/templates/shared/workers/platform-usage.ts +98 -8
  160. package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
  161. package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
  162. package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
  163. package/templates/standard/dashboard/src/components/health/CircuitBreakerEvents.tsx +69 -0
  164. package/templates/standard/dashboard/src/components/health/CircuitBreakerPanel.tsx +97 -0
  165. package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
  166. package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
  167. package/templates/standard/dashboard/src/components/health/index.ts +4 -0
  168. package/templates/standard/dashboard/src/lib/errors.ts +28 -0
  169. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts +49 -0
  170. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts +36 -0
  171. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint].ts +55 -0
  172. package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
  173. package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
  174. package/templates/standard/dashboard/src/pages/api/health/audit-history.ts +37 -0
  175. package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
  176. package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -0
  177. package/templates/standard/dashboard/src/pages/errors.astro +13 -0
  178. package/templates/standard/dashboard/src/pages/health.astro +11 -0
  179. package/templates/standard/migrations/009_topology_mapper.sql +65 -0
  180. package/templates/standard/tests/unit/error-collector/capture.test.ts +106 -0
  181. package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -0
  182. package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
  183. package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
  184. package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
  185. package/templates/standard/workers/lib/mapper/index.ts +7 -0
  186. package/templates/standard/workers/platform-mapper.ts +482 -0
  187. package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
  188. package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
  189. package/templates/standard/wrangler.sdk-test-client.jsonc.hbs +62 -0
@@ -0,0 +1,47 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const GET: APIRoute = async ({ locals, url }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ notifications: [], total: 0 }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ const page = parseInt(url.searchParams.get('page') ?? '1');
13
+ const limit = Math.min(parseInt(url.searchParams.get('limit') ?? '20'), 100);
14
+ const offset = (page - 1) * limit;
15
+
16
+ try {
17
+ const notifications = await db
18
+ .prepare(
19
+ `SELECT id, title, body, category, priority, source, created_at, read_at, action_url, expires_at
20
+ FROM notifications
21
+ WHERE expires_at IS NULL OR expires_at > unixepoch()
22
+ ORDER BY created_at DESC
23
+ LIMIT ? OFFSET ?`
24
+ )
25
+ .bind(limit, offset)
26
+ .all();
27
+
28
+ const total = await db
29
+ .prepare(`SELECT COUNT(*) as count FROM notifications WHERE expires_at IS NULL OR expires_at > unixepoch() LIMIT 1`)
30
+ .first<{ count: number }>();
31
+
32
+ return new Response(
33
+ JSON.stringify({
34
+ notifications: notifications.results ?? [],
35
+ total: total?.count ?? 0,
36
+ page,
37
+ limit,
38
+ }),
39
+ { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=15' } }
40
+ );
41
+ } catch {
42
+ return new Response(JSON.stringify({ notifications: [], total: 0 }), {
43
+ status: 500,
44
+ headers: { 'Content-Type': 'application/json' },
45
+ });
46
+ }
47
+ };
@@ -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
+ const result = await db
15
+ .prepare(`UPDATE notifications SET read_at = unixepoch() WHERE read_at IS NULL`)
16
+ .run();
17
+
18
+ return new Response(
19
+ JSON.stringify({ ok: true, updated: result.meta?.changes ?? 0 }),
20
+ { headers: { 'Content-Type': 'application/json' } }
21
+ );
22
+ } catch {
23
+ return new Response(JSON.stringify({ error: 'Failed to mark all as read' }), {
24
+ status: 500,
25
+ headers: { 'Content-Type': 'application/json' },
26
+ });
27
+ }
28
+ };
@@ -0,0 +1,31 @@
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({ count: 0 }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ try {
13
+ const result = await db
14
+ .prepare(
15
+ `SELECT COUNT(*) as count FROM notifications
16
+ WHERE read_at IS NULL
17
+ AND (expires_at IS NULL OR expires_at > unixepoch())
18
+ LIMIT 1`
19
+ )
20
+ .first<{ count: number }>();
21
+
22
+ return new Response(JSON.stringify({ count: result?.count ?? 0 }), {
23
+ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=15' },
24
+ });
25
+ } catch {
26
+ return new Response(JSON.stringify({ count: 0 }), {
27
+ status: 500,
28
+ headers: { 'Content-Type': 'application/json' },
29
+ });
30
+ }
31
+ };
@@ -0,0 +1,55 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const POST: APIRoute = async ({ locals, request }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+ const patternApi = (locals.runtime?.env as { PATTERN_DISCOVERY_API?: Fetcher } | undefined)?.PATTERN_DISCOVERY_API;
6
+
7
+ if (!db && !patternApi) {
8
+ return new Response(JSON.stringify({ error: 'No database or pattern API available' }), {
9
+ status: 500,
10
+ headers: { 'Content-Type': 'application/json' },
11
+ });
12
+ }
13
+
14
+ try {
15
+ const body = (await request.json()) as { id: number; notes?: string };
16
+
17
+ if (patternApi) {
18
+ const controller = new AbortController();
19
+ const timeout = setTimeout(() => controller.abort(), 5000);
20
+ const res = await patternApi.fetch(
21
+ `https://internal/suggestions/${body.id}?action=approve`,
22
+ {
23
+ method: 'POST',
24
+ headers: { 'Content-Type': 'application/json' },
25
+ body: JSON.stringify({ notes: body.notes }),
26
+ signal: controller.signal,
27
+ }
28
+ );
29
+ clearTimeout(timeout);
30
+ if (!res.ok) throw new Error(`Pattern API returned ${res.status}`);
31
+ return new Response(JSON.stringify({ ok: true }), {
32
+ headers: { 'Content-Type': 'application/json' },
33
+ });
34
+ }
35
+
36
+ // Fallback: update D1 directly
37
+ await db!
38
+ .prepare(
39
+ `UPDATE transient_pattern_suggestions
40
+ SET status = 'approved', reviewed_at = datetime('now'), reviewer_notes = ?
41
+ WHERE id = ?`
42
+ )
43
+ .bind(body.notes ?? null, body.id)
44
+ .run();
45
+
46
+ return new Response(JSON.stringify({ ok: true }), {
47
+ headers: { 'Content-Type': 'application/json' },
48
+ });
49
+ } catch (error) {
50
+ return new Response(
51
+ JSON.stringify({ error: error instanceof Error ? error.message : 'Failed to approve' }),
52
+ { status: 500, headers: { 'Content-Type': 'application/json' } }
53
+ );
54
+ }
55
+ };
@@ -0,0 +1,38 @@
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
+ const kv = (locals.runtime?.env as { PLATFORM_CACHE?: KVNamespace } | undefined)?.PLATFORM_CACHE;
6
+
7
+ if (!db || !kv) {
8
+ return new Response(JSON.stringify({ error: 'Bindings not available' }), {
9
+ status: 503,
10
+ headers: { 'Content-Type': 'application/json' },
11
+ });
12
+ }
13
+
14
+ try {
15
+ const approved = await db
16
+ .prepare(
17
+ `SELECT id, pattern_type, pattern_value, error_type, priority
18
+ FROM transient_pattern_suggestions
19
+ WHERE status = 'approved'
20
+ ORDER BY match_count DESC
21
+ LIMIT 500`
22
+ )
23
+ .all();
24
+
25
+ const patterns = approved.results ?? [];
26
+ await kv.put('PATTERNS:DYNAMIC:APPROVED', JSON.stringify(patterns), { expirationTtl: 86400 });
27
+
28
+ return new Response(
29
+ JSON.stringify({ refreshed: true, count: patterns.length }),
30
+ { headers: { 'Content-Type': 'application/json' } }
31
+ );
32
+ } catch {
33
+ return new Response(JSON.stringify({ error: 'Cache refresh failed' }), {
34
+ status: 500,
35
+ headers: { 'Content-Type': 'application/json' },
36
+ });
37
+ }
38
+ };
@@ -0,0 +1,36 @@
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
+ const unclassified = await db
15
+ .prepare(
16
+ `SELECT fingerprint, script_name, error_message, priority, occurrence_count
17
+ FROM error_occurrences
18
+ WHERE fingerprint NOT IN (
19
+ SELECT pattern_value FROM transient_pattern_suggestions WHERE status = 'approved'
20
+ )
21
+ ORDER BY occurrence_count DESC
22
+ LIMIT 50`
23
+ )
24
+ .all();
25
+
26
+ return new Response(
27
+ JSON.stringify({ discovered: unclassified.results?.length ?? 0, errors: unclassified.results ?? [] }),
28
+ { headers: { 'Content-Type': 'application/json' } }
29
+ );
30
+ } catch {
31
+ return new Response(JSON.stringify({ error: 'Discovery failed' }), {
32
+ status: 500,
33
+ headers: { 'Content-Type': 'application/json' },
34
+ });
35
+ }
36
+ };
@@ -0,0 +1,36 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const GET: APIRoute = async ({ locals, url }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ suggestions: [] }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ const status = url.searchParams.get('status') ?? 'pending';
13
+
14
+ try {
15
+ const suggestions = await db
16
+ .prepare(
17
+ `SELECT id, pattern_type, pattern_value, error_type, priority, status,
18
+ match_count, source, created_at, reviewed_at, reviewer_notes
19
+ FROM transient_pattern_suggestions
20
+ WHERE status = ?
21
+ ORDER BY created_at DESC
22
+ LIMIT 50`
23
+ )
24
+ .bind(status)
25
+ .all();
26
+
27
+ return new Response(JSON.stringify({ suggestions: suggestions.results ?? [] }), {
28
+ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=30' },
29
+ });
30
+ } catch {
31
+ return new Response(JSON.stringify({ suggestions: [] }), {
32
+ status: 500,
33
+ headers: { 'Content-Type': 'application/json' },
34
+ });
35
+ }
36
+ };
@@ -0,0 +1,39 @@
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({ patterns: [], count: 0 }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ try {
13
+ const patterns = await db
14
+ .prepare(
15
+ `SELECT s.id, s.pattern_type, s.pattern_value, s.error_type, s.priority,
16
+ s.match_count, s.source, s.created_at,
17
+ COUNT(e.id) as evidence_count
18
+ FROM transient_pattern_suggestions s
19
+ LEFT JOIN pattern_match_evidence e ON e.suggestion_id = s.id
20
+ WHERE s.status = 'shadow'
21
+ AND s.match_count >= 3
22
+ AND s.created_at < datetime('now', '-3 days')
23
+ GROUP BY s.id
24
+ ORDER BY s.match_count DESC
25
+ LIMIT 20`
26
+ )
27
+ .all();
28
+
29
+ return new Response(
30
+ JSON.stringify({ patterns: patterns.results ?? [], count: patterns.results?.length ?? 0 }),
31
+ { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=60' } }
32
+ );
33
+ } catch {
34
+ return new Response(JSON.stringify({ patterns: [], count: 0 }), {
35
+ status: 500,
36
+ headers: { 'Content-Type': 'application/json' },
37
+ });
38
+ }
39
+ };
@@ -0,0 +1,54 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const POST: APIRoute = async ({ locals, request }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+ const patternApi = (locals.runtime?.env as { PATTERN_DISCOVERY_API?: Fetcher } | undefined)?.PATTERN_DISCOVERY_API;
6
+
7
+ if (!db && !patternApi) {
8
+ return new Response(JSON.stringify({ error: 'No database or pattern API available' }), {
9
+ status: 500,
10
+ headers: { 'Content-Type': 'application/json' },
11
+ });
12
+ }
13
+
14
+ try {
15
+ const body = (await request.json()) as { id: number; notes?: string };
16
+
17
+ if (patternApi) {
18
+ const controller = new AbortController();
19
+ const timeout = setTimeout(() => controller.abort(), 5000);
20
+ const res = await patternApi.fetch(
21
+ `https://internal/suggestions/${body.id}?action=reject`,
22
+ {
23
+ method: 'POST',
24
+ headers: { 'Content-Type': 'application/json' },
25
+ body: JSON.stringify({ notes: body.notes }),
26
+ signal: controller.signal,
27
+ }
28
+ );
29
+ clearTimeout(timeout);
30
+ if (!res.ok) throw new Error(`Pattern API returned ${res.status}`);
31
+ return new Response(JSON.stringify({ ok: true }), {
32
+ headers: { 'Content-Type': 'application/json' },
33
+ });
34
+ }
35
+
36
+ await db!
37
+ .prepare(
38
+ `UPDATE transient_pattern_suggestions
39
+ SET status = 'rejected', reviewed_at = datetime('now'), reviewer_notes = ?
40
+ WHERE id = ?`
41
+ )
42
+ .bind(body.notes ?? null, body.id)
43
+ .run();
44
+
45
+ return new Response(JSON.stringify({ ok: true }), {
46
+ headers: { 'Content-Type': 'application/json' },
47
+ });
48
+ } catch (error) {
49
+ return new Response(
50
+ JSON.stringify({ error: error instanceof Error ? error.message : 'Failed to reject' }),
51
+ { status: 500, headers: { 'Content-Type': 'application/json' } }
52
+ );
53
+ }
54
+ };
@@ -0,0 +1,39 @@
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({ byStatus: {}, total: 0 }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ try {
13
+ const counts = await db
14
+ .prepare(
15
+ `SELECT status, COUNT(*) as count
16
+ FROM transient_pattern_suggestions
17
+ GROUP BY status
18
+ LIMIT 10`
19
+ )
20
+ .all<{ status: string; count: number }>();
21
+
22
+ const byStatus: Record<string, number> = {};
23
+ let total = 0;
24
+ for (const row of counts.results ?? []) {
25
+ byStatus[row.status] = row.count;
26
+ total += row.count;
27
+ }
28
+
29
+ return new Response(
30
+ JSON.stringify({ byStatus, total }),
31
+ { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=60' } }
32
+ );
33
+ } catch {
34
+ return new Response(JSON.stringify({ byStatus: {}, total: 0 }), {
35
+ status: 500,
36
+ headers: { 'Content-Type': 'application/json' },
37
+ });
38
+ }
39
+ };
@@ -0,0 +1,43 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const GET: APIRoute = async ({ locals, url }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ suggestions: [], total: 0 }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ const status = url.searchParams.get('status') ?? 'pending';
13
+ const limit = Math.min(parseInt(url.searchParams.get('limit') ?? '50'), 100);
14
+
15
+ try {
16
+ const suggestions = await db
17
+ .prepare(
18
+ `SELECT id, pattern_type, pattern_value, error_type, priority, status,
19
+ match_count, source, created_at, reviewed_at, reviewer_notes, is_protected
20
+ FROM transient_pattern_suggestions
21
+ WHERE status = ?
22
+ ORDER BY match_count DESC, created_at DESC
23
+ LIMIT ?`
24
+ )
25
+ .bind(status, limit)
26
+ .all();
27
+
28
+ const total = await db
29
+ .prepare(`SELECT COUNT(*) as count FROM transient_pattern_suggestions WHERE status = ? LIMIT 1`)
30
+ .bind(status)
31
+ .first<{ count: number }>();
32
+
33
+ return new Response(
34
+ JSON.stringify({ suggestions: suggestions.results ?? [], total: total?.count ?? 0 }),
35
+ { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=30' } }
36
+ );
37
+ } catch {
38
+ return new Response(JSON.stringify({ suggestions: [], total: 0 }), {
39
+ status: 500,
40
+ headers: { 'Content-Type': 'application/json' },
41
+ });
42
+ }
43
+ };
@@ -0,0 +1,45 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const GET: APIRoute = async ({ locals, url }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ reports: [] }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ const project = url.searchParams.get('project');
13
+ const limit = Math.min(parseInt(url.searchParams.get('limit') ?? '10'), 50);
14
+
15
+ try {
16
+ const query = project
17
+ ? `SELECT id, project, scan_type, ai_judge_score, sdk_score, observability_score,
18
+ cost_protection_score, security_score, scan_date, focused_dimensions
19
+ FROM audit_results
20
+ WHERE project = ?
21
+ ORDER BY scan_date DESC
22
+ LIMIT ?`
23
+ : `SELECT id, project, scan_type, ai_judge_score, sdk_score, observability_score,
24
+ cost_protection_score, security_score, scan_date, focused_dimensions
25
+ FROM audit_results
26
+ ORDER BY scan_date DESC
27
+ LIMIT ?`;
28
+
29
+ const stmt = project
30
+ ? db.prepare(query).bind(project, limit)
31
+ : db.prepare(query).bind(limit);
32
+
33
+ const reports = await stmt.all();
34
+
35
+ return new Response(
36
+ JSON.stringify({ reports: reports.results ?? [] }),
37
+ { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=120' } }
38
+ );
39
+ } catch {
40
+ return new Response(JSON.stringify({ reports: [] }), {
41
+ status: 500,
42
+ headers: { 'Content-Type': 'application/json' },
43
+ });
44
+ }
45
+ };
@@ -0,0 +1,52 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const GET: APIRoute = async ({ locals, url }) => {
4
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
5
+
6
+ if (!db) {
7
+ return new Response(JSON.stringify({ daily: [], monthly: [] }), {
8
+ headers: { 'Content-Type': 'application/json' },
9
+ });
10
+ }
11
+
12
+ const days = Math.min(parseInt(url.searchParams.get('days') ?? '30'), 90);
13
+ const startDate = new Date();
14
+ startDate.setDate(startDate.getDate() - days);
15
+ const startStr = startDate.toISOString().slice(0, 10);
16
+
17
+ try {
18
+ const [daily, monthly] = await Promise.all([
19
+ db
20
+ .prepare(
21
+ `SELECT snapshot_date, d1_reads, d1_writes, kv_reads, kv_writes,
22
+ r2_reads, r2_writes, worker_requests, total_cost_usd
23
+ FROM daily_usage_rollups
24
+ WHERE project = 'all' AND snapshot_date >= ?
25
+ ORDER BY snapshot_date ASC
26
+ LIMIT 90`
27
+ )
28
+ .bind(startStr)
29
+ .all(),
30
+ db
31
+ .prepare(
32
+ `SELECT snapshot_month, d1_reads, d1_writes, kv_reads, kv_writes,
33
+ r2_reads, r2_writes, worker_requests, total_cost_usd
34
+ FROM monthly_usage_rollups
35
+ WHERE project = 'all'
36
+ ORDER BY snapshot_month DESC
37
+ LIMIT 12`
38
+ )
39
+ .all(),
40
+ ]);
41
+
42
+ return new Response(
43
+ JSON.stringify({ daily: daily.results ?? [], monthly: monthly.results ?? [] }),
44
+ { headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=300' } }
45
+ );
46
+ } catch {
47
+ return new Response(JSON.stringify({ daily: [], monthly: [] }), {
48
+ status: 500,
49
+ headers: { 'Content-Type': 'application/json' },
50
+ });
51
+ }
52
+ };
@@ -0,0 +1,74 @@
1
+ import type { APIRoute } from 'astro';
2
+
3
+ export const GET: APIRoute = async ({ locals, url }) => {
4
+ const searchApi = (locals.runtime?.env as { SEARCH_API?: Fetcher } | undefined)?.SEARCH_API;
5
+ const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
6
+ const query = url.searchParams.get('q')?.trim();
7
+
8
+ if (!query || query.length < 2) {
9
+ return new Response(JSON.stringify({ results: [], query: '' }), {
10
+ headers: { 'Content-Type': 'application/json' },
11
+ });
12
+ }
13
+
14
+ // Prefer search API service binding
15
+ if (searchApi) {
16
+ try {
17
+ const controller = new AbortController();
18
+ const timeout = setTimeout(() => controller.abort(), 5000);
19
+ const res = await searchApi.fetch(`https://internal/search?q=${encodeURIComponent(query)}`, {
20
+ signal: controller.signal,
21
+ });
22
+ clearTimeout(timeout);
23
+ if (res.ok) {
24
+ const data = await res.json();
25
+ return new Response(JSON.stringify(data), {
26
+ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=30' },
27
+ });
28
+ }
29
+ } catch {
30
+ // Fall through to D1 fallback
31
+ }
32
+ }
33
+
34
+ // D1 fallback: basic LIKE search
35
+ if (!db) {
36
+ return new Response(JSON.stringify({ results: [], query }), {
37
+ headers: { 'Content-Type': 'application/json' },
38
+ });
39
+ }
40
+
41
+ try {
42
+ const pattern = `%${query}%`;
43
+ const errors = await db
44
+ .prepare(
45
+ `SELECT 'error' as type, fingerprint as id, normalized_message as title, script_name as subtitle
46
+ FROM error_occurrences
47
+ WHERE normalized_message LIKE ? OR script_name LIKE ?
48
+ LIMIT 5`
49
+ )
50
+ .bind(pattern, pattern)
51
+ .all();
52
+
53
+ const notifications = await db
54
+ .prepare(
55
+ `SELECT 'notification' as type, id, title, category as subtitle
56
+ FROM notifications
57
+ WHERE title LIKE ?
58
+ LIMIT 5`
59
+ )
60
+ .bind(pattern)
61
+ .all();
62
+
63
+ const results = [...(errors.results ?? []), ...(notifications.results ?? [])];
64
+
65
+ return new Response(JSON.stringify({ results, query }), {
66
+ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=30' },
67
+ });
68
+ } catch {
69
+ return new Response(JSON.stringify({ results: [], query }), {
70
+ status: 500,
71
+ headers: { 'Content-Type': 'application/json' },
72
+ });
73
+ }
74
+ };
@@ -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
+ };