@littlebearapps/platform-admin-sdk 1.4.2 → 1.5.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 (111) hide show
  1. package/dist/templates.d.ts +1 -1
  2. package/dist/templates.js +121 -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/PatternStats.tsx +60 -0
  9. package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
  10. package/templates/full/dashboard/src/components/patterns/index.ts +2 -0
  11. package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
  12. package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
  13. package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
  14. package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
  15. package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
  16. package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
  17. package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
  18. package/templates/full/dashboard/src/pages/notifications.astro +11 -0
  19. package/templates/full/migrations/008_auditor.sql +99 -0
  20. package/templates/full/migrations/010_pricing_versions.sql +110 -0
  21. package/templates/full/migrations/011_multi_account.sql +51 -0
  22. package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
  23. package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
  24. package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
  25. package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
  26. package/templates/full/workers/lib/auditor/index.ts +9 -0
  27. package/templates/full/workers/lib/auditor/types.ts +167 -0
  28. package/templates/full/workers/platform-auditor.ts +1071 -0
  29. package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
  30. package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
  31. package/templates/shared/config/observability.yaml.hbs +276 -0
  32. package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
  33. package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
  34. package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
  35. package/templates/shared/dashboard/astro.config.mjs +21 -0
  36. package/templates/shared/dashboard/package.json.hbs +29 -0
  37. package/templates/shared/dashboard/src/components/Header.astro +29 -0
  38. package/templates/shared/dashboard/src/components/Nav.astro.hbs +57 -0
  39. package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
  40. package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
  41. package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
  42. package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
  43. package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
  44. package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
  45. package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
  46. package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
  47. package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
  48. package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
  49. package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
  50. package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
  51. package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
  52. package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
  53. package/templates/shared/dashboard/src/components/ui/index.ts +3 -0
  54. package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
  55. package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
  56. package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
  57. package/templates/shared/dashboard/src/lib/types.ts +72 -0
  58. package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
  59. package/templates/shared/dashboard/src/middleware/index.ts +1 -0
  60. package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
  61. package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
  62. package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
  63. package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
  64. package/templates/shared/dashboard/src/pages/index.astro +3 -0
  65. package/templates/shared/dashboard/src/pages/resources.astro +11 -0
  66. package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
  67. package/templates/shared/dashboard/src/styles/global.css +29 -0
  68. package/templates/shared/dashboard/tailwind.config.mjs +9 -0
  69. package/templates/shared/dashboard/tsconfig.json +9 -0
  70. package/templates/shared/dashboard/wrangler.json.hbs +47 -0
  71. package/templates/shared/package.json.hbs +12 -1
  72. package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
  73. package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
  74. package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
  75. package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
  76. package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
  77. package/templates/shared/scripts/validate-schemas.js +61 -0
  78. package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
  79. package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
  80. package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
  81. package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
  82. package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
  83. package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
  84. package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
  85. package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
  86. package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
  87. package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
  88. package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
  89. package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
  90. package/templates/shared/workers/platform-usage.ts +98 -8
  91. package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
  92. package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
  93. package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
  94. package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
  95. package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
  96. package/templates/standard/dashboard/src/components/health/index.ts +2 -0
  97. package/templates/standard/dashboard/src/lib/errors.ts +28 -0
  98. package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
  99. package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
  100. package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
  101. package/templates/standard/dashboard/src/pages/errors.astro +13 -0
  102. package/templates/standard/dashboard/src/pages/health.astro +11 -0
  103. package/templates/standard/migrations/009_topology_mapper.sql +65 -0
  104. package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
  105. package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
  106. package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
  107. package/templates/standard/workers/lib/mapper/index.ts +7 -0
  108. package/templates/standard/workers/platform-mapper.ts +482 -0
  109. package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
  110. package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
  111. package/templates/standard/wrangler.sdk-test-client.jsonc.hbs +62 -0
@@ -0,0 +1,108 @@
1
+ import { useState, useEffect, useRef, useCallback } from 'react';
2
+
3
+ interface SearchResult {
4
+ type: string;
5
+ id: string;
6
+ title: string;
7
+ subtitle: string;
8
+ }
9
+
10
+ export function SearchModal() {
11
+ const [open, setOpen] = useState(false);
12
+ const [query, setQuery] = useState('');
13
+ const [results, setResults] = useState<SearchResult[]>([]);
14
+ const [loading, setLoading] = useState(false);
15
+ const inputRef = useRef<HTMLInputElement>(null);
16
+ const debounceRef = useRef<ReturnType<typeof setTimeout>>();
17
+
18
+ // Cmd+K / Ctrl+K listener
19
+ useEffect(() => {
20
+ function handleKeyDown(e: KeyboardEvent) {
21
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
22
+ e.preventDefault();
23
+ setOpen(prev => !prev);
24
+ }
25
+ if (e.key === 'Escape') {
26
+ setOpen(false);
27
+ }
28
+ }
29
+ document.addEventListener('keydown', handleKeyDown);
30
+ return () => document.removeEventListener('keydown', handleKeyDown);
31
+ }, []);
32
+
33
+ useEffect(() => {
34
+ if (open && inputRef.current) {
35
+ inputRef.current.focus();
36
+ }
37
+ }, [open]);
38
+
39
+ const search = useCallback((q: string) => {
40
+ if (q.length < 2) {
41
+ setResults([]);
42
+ return;
43
+ }
44
+ setLoading(true);
45
+ fetch(`/api/search?q=${encodeURIComponent(q)}`)
46
+ .then(res => res.json())
47
+ .then((data: { results: SearchResult[] }) => {
48
+ setResults(data.results ?? []);
49
+ setLoading(false);
50
+ })
51
+ .catch(() => setLoading(false));
52
+ }, []);
53
+
54
+ function handleInput(value: string) {
55
+ setQuery(value);
56
+ clearTimeout(debounceRef.current);
57
+ debounceRef.current = setTimeout(() => search(value), 300);
58
+ }
59
+
60
+ if (!open) return null;
61
+
62
+ return (
63
+ <div className="fixed inset-0 z-50 flex items-start justify-center pt-[20vh]" onClick={() => setOpen(false)}>
64
+ <div className="fixed inset-0 bg-black/50" />
65
+ <div
66
+ className="relative w-full max-w-lg bg-white dark:bg-gray-800 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700"
67
+ onClick={(e) => e.stopPropagation()}
68
+ >
69
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-gray-200 dark:border-gray-700">
70
+ <span className="text-gray-400 text-sm">Search</span>
71
+ <input
72
+ ref={inputRef}
73
+ type="text"
74
+ value={query}
75
+ onChange={(e) => handleInput(e.target.value)}
76
+ placeholder="Search errors, notifications..."
77
+ className="flex-1 bg-transparent text-gray-900 dark:text-white placeholder-gray-400 outline-none text-sm"
78
+ />
79
+ <kbd className="hidden md:inline text-xs text-gray-400 border border-gray-300 dark:border-gray-600 rounded px-1.5 py-0.5">
80
+ ESC
81
+ </kbd>
82
+ </div>
83
+
84
+ <div className="max-h-80 overflow-y-auto p-2">
85
+ {loading && (
86
+ <div className="text-center py-4 text-sm text-gray-500 dark:text-gray-400">Searching...</div>
87
+ )}
88
+ {!loading && results.length === 0 && query.length >= 2 && (
89
+ <div className="text-center py-4 text-sm text-gray-500 dark:text-gray-400">No results found.</div>
90
+ )}
91
+ {!loading && results.map((r) => (
92
+ <a
93
+ key={`${r.type}-${r.id}`}
94
+ href={r.type === 'error' ? `/errors` : `/notifications`}
95
+ className="flex items-center gap-3 px-3 py-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
96
+ >
97
+ <span className="text-xs font-mono text-gray-400 dark:text-gray-500 uppercase">{r.type}</span>
98
+ <div className="flex-1 min-w-0">
99
+ <div className="text-sm text-gray-900 dark:text-white truncate">{r.title}</div>
100
+ <div className="text-xs text-gray-500 dark:text-gray-400 truncate">{r.subtitle}</div>
101
+ </div>
102
+ </a>
103
+ ))}
104
+ </div>
105
+ </div>
106
+ </div>
107
+ );
108
+ }
@@ -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,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,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,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,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,11 @@
1
+ ---
2
+ import DashboardLayout from '../../layouts/DashboardLayout.astro';
3
+ import { NotificationList } from '../../components/notifications/NotificationList';
4
+ ---
5
+
6
+ <DashboardLayout title="Notifications">
7
+ <div class="max-w-4xl mx-auto">
8
+ <h2 class="text-xl font-bold text-gray-900 dark:text-white mb-4">Notifications</h2>
9
+ <NotificationList client:load />
10
+ </div>
11
+ </DashboardLayout>
@@ -0,0 +1,99 @@
1
+ -- =============================================================================
2
+ -- 008_auditor.sql — Auditor Reports & Feature Coverage
3
+ -- =============================================================================
4
+ -- Additional tables for FULL tier SDK integration auditor.
5
+ -- Base audit_results table is in shared migration 004_settings_alerts.sql.
6
+ --
7
+ -- Tables:
8
+ -- attribution_reports — Resource-to-project mapping status
9
+ -- feature_coverage_audit — Feature active/dormant/undefined tracking
10
+ -- comprehensive_audit_reports — Weekly aggregated audit findings
11
+ -- =============================================================================
12
+
13
+
14
+ -- =============================================================================
15
+ -- ATTRIBUTION REPORTS (from platform-mapper discovery runs)
16
+ -- =============================================================================
17
+ -- Stores resource-to-project attribution status from topology discovery.
18
+
19
+ CREATE TABLE IF NOT EXISTS attribution_reports (
20
+ id TEXT PRIMARY KEY,
21
+ discovery_time TEXT NOT NULL,
22
+ total_resources INTEGER NOT NULL DEFAULT 0,
23
+ attributed_count INTEGER NOT NULL DEFAULT 0,
24
+ unattributed_count INTEGER NOT NULL DEFAULT 0,
25
+ report_json TEXT NOT NULL,
26
+ created_at TEXT DEFAULT (datetime('now'))
27
+ );
28
+
29
+ CREATE INDEX IF NOT EXISTS idx_attribution_discovery_time
30
+ ON attribution_reports(discovery_time);
31
+
32
+
33
+ -- =============================================================================
34
+ -- FEATURE COVERAGE AUDIT
35
+ -- =============================================================================
36
+ -- Tracks which features from budgets.yaml are active vs dormant.
37
+ -- Populated by platform-auditor feature coverage checks.
38
+
39
+ CREATE TABLE IF NOT EXISTS feature_coverage_audit (
40
+ id TEXT PRIMARY KEY,
41
+ audit_time TEXT NOT NULL,
42
+ project TEXT NOT NULL,
43
+ feature TEXT NOT NULL,
44
+ -- Status: active (heartbeats in last 7d), dormant (defined but no heartbeats),
45
+ -- undefined (reporting telemetry but not in config)
46
+ status TEXT NOT NULL CHECK (status IN ('active', 'dormant', 'undefined')),
47
+ last_heartbeat TEXT,
48
+ events_last_7d INTEGER DEFAULT 0,
49
+ defined_budget INTEGER,
50
+ budget_unit TEXT,
51
+ created_at TEXT DEFAULT (datetime('now'))
52
+ );
53
+
54
+ CREATE INDEX IF NOT EXISTS idx_feature_coverage_project
55
+ ON feature_coverage_audit(project);
56
+ CREATE INDEX IF NOT EXISTS idx_feature_coverage_status
57
+ ON feature_coverage_audit(status);
58
+ CREATE INDEX IF NOT EXISTS idx_feature_coverage_audit_time
59
+ ON feature_coverage_audit(audit_time);
60
+
61
+
62
+ -- =============================================================================
63
+ -- COMPREHENSIVE AUDIT REPORTS
64
+ -- =============================================================================
65
+ -- Weekly aggregated reports combining all audit dimensions:
66
+ -- gap analysis, resource attribution, feature coverage, and AI Judge scores.
67
+
68
+ CREATE TABLE IF NOT EXISTS comprehensive_audit_reports (
69
+ id TEXT PRIMARY KEY,
70
+ generated_at TEXT NOT NULL,
71
+ -- Gap summary (from sentinel gap_detection_log)
72
+ gap_events_count INTEGER NOT NULL DEFAULT 0,
73
+ total_missing_hours INTEGER NOT NULL DEFAULT 0,
74
+ worst_gap_day TEXT,
75
+ average_gap_severity TEXT,
76
+ -- Attribution summary (from mapper attribution_reports)
77
+ total_resources INTEGER NOT NULL DEFAULT 0,
78
+ attributed_count INTEGER NOT NULL DEFAULT 0,
79
+ unattributed_count INTEGER NOT NULL DEFAULT 0,
80
+ unattributed_resources TEXT, -- JSON array of {type, name}
81
+ -- Feature coverage summary
82
+ defined_features_count INTEGER NOT NULL DEFAULT 0,
83
+ active_features_count INTEGER NOT NULL DEFAULT 0,
84
+ dormant_features_count INTEGER NOT NULL DEFAULT 0,
85
+ undefined_features_count INTEGER NOT NULL DEFAULT 0,
86
+ -- AI Judge summary
87
+ ai_judge_avg_score REAL,
88
+ ai_judge_recommendations TEXT, -- JSON array
89
+ -- Action items
90
+ action_items_count INTEGER NOT NULL DEFAULT 0,
91
+ critical_items_count INTEGER NOT NULL DEFAULT 0,
92
+ action_items TEXT, -- JSON array
93
+ -- Full report
94
+ report_json TEXT NOT NULL,
95
+ created_at TEXT DEFAULT (datetime('now'))
96
+ );
97
+
98
+ CREATE INDEX IF NOT EXISTS idx_comprehensive_audit_generated
99
+ ON comprehensive_audit_reports(generated_at);
@@ -0,0 +1,110 @@
1
+ -- Migration 010: Pricing Version Tracking
2
+ --
3
+ -- Purpose: Audit trail for Cloudflare pricing changes with historical recomputation support.
4
+ --
5
+ -- Benefits:
6
+ -- - Track when pricing changes occur (Cloudflare updates rates periodically)
7
+ -- - Recompute historical costs when pricing is corrected
8
+ -- - Audit trail for billing reconciliation
9
+ -- - Support for pricing A/B comparisons
10
+
11
+ -- Create pricing_versions table
12
+ CREATE TABLE IF NOT EXISTS pricing_versions (
13
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
14
+ version_name TEXT NOT NULL, -- Human-readable name (e.g., "2026-01", "2025-workers-update")
15
+ effective_from TEXT NOT NULL, -- ISO date when pricing became effective
16
+ effective_to TEXT, -- NULL = current, ISO date when superseded
17
+ source_url TEXT, -- CF pricing page reference for audit
18
+ pricing_json TEXT NOT NULL, -- Full pricing config as JSON
19
+ allowances_json TEXT NOT NULL, -- Paid plan allowances as JSON
20
+ notes TEXT, -- Optional notes about changes
21
+ created_at TEXT DEFAULT (datetime('now')),
22
+ created_by TEXT DEFAULT 'system' -- 'system' for auto-detected, 'manual' for user overrides
23
+ );
24
+
25
+ -- Index for querying current/historical pricing
26
+ CREATE INDEX IF NOT EXISTS idx_pricing_versions_effective
27
+ ON pricing_versions (effective_from, effective_to);
28
+
29
+ -- Add pricing_version_id to daily rollups for cost recomputation
30
+ -- Note: SQLite doesn't support adding constraints in ALTER, so we just add the column
31
+ ALTER TABLE daily_usage_rollups ADD COLUMN pricing_version_id INTEGER;
32
+
33
+ -- Add pricing_version_id to monthly rollups
34
+ ALTER TABLE monthly_usage_rollups ADD COLUMN pricing_version_id INTEGER;
35
+
36
+ -- Seed with current pricing (January 2026)
37
+ INSERT INTO pricing_versions (
38
+ version_name,
39
+ effective_from,
40
+ effective_to,
41
+ source_url,
42
+ pricing_json,
43
+ allowances_json,
44
+ notes,
45
+ created_by
46
+ ) VALUES (
47
+ '2026-01-initial',
48
+ '2026-01-01',
49
+ NULL,
50
+ 'https://developers.cloudflare.com/workers/platform/pricing/',
51
+ '{
52
+ "workers": {
53
+ "baseCostMonthly": 5.0,
54
+ "includedRequests": 10000000,
55
+ "requestsPerMillion": 0.3,
56
+ "cpuMsPerMillion": 0.02
57
+ },
58
+ "d1": {
59
+ "rowsReadPerBillion": 0.001,
60
+ "rowsWrittenPerMillion": 1.0,
61
+ "storagePerGb": 0.75
62
+ },
63
+ "kv": {
64
+ "readsPerMillion": 0.5,
65
+ "writesPerMillion": 5.0,
66
+ "deletesPerMillion": 5.0,
67
+ "listsPerMillion": 5.0,
68
+ "storagePerGb": 0.5
69
+ },
70
+ "r2": {
71
+ "storagePerGbMonth": 0.015,
72
+ "classAPerMillion": 4.5,
73
+ "classBPerMillion": 0.36
74
+ },
75
+ "durableObjects": {
76
+ "requestsPerMillion": 0.15,
77
+ "gbSecondsPerMillion": 12.5,
78
+ "storagePerGbMonth": 0.2,
79
+ "readsPerMillion": 0.2,
80
+ "writesPerMillion": 1.0,
81
+ "deletesPerMillion": 1.0
82
+ },
83
+ "vectorize": {
84
+ "storedDimensionsPerMillion": 0.01,
85
+ "queriedDimensionsPerMillion": 0.01
86
+ },
87
+ "aiGateway": { "free": true },
88
+ "workersAI": {
89
+ "neuronsPerThousand": 0.011
90
+ },
91
+ "pages": {
92
+ "buildCost": 0.15,
93
+ "bandwidthPerGb": 0.02
94
+ },
95
+ "queues": {
96
+ "messagesPerMillion": 0.4,
97
+ "operationsPerMillion": 0.4
98
+ },
99
+ "workflows": { "free": true }
100
+ }',
101
+ '{
102
+ "d1": { "rowsRead": 25000000000, "rowsWritten": 50000000 },
103
+ "kv": { "reads": 10000000, "writes": 1000000, "deletes": 1000000, "lists": 1000000 },
104
+ "r2": { "storage": 10000000000, "classA": 1000000, "classB": 10000000 },
105
+ "durableObjects": { "requests": 3000000, "gbSeconds": 400000 },
106
+ "vectorize": { "storedDimensions": 30000000, "queriedDimensions": 30000000 }
107
+ }',
108
+ 'Initial pricing version seeded from Cloudflare pricing pages (January 2026)',
109
+ 'migration'
110
+ );
@@ -0,0 +1,51 @@
1
+ -- Migration 011: Multi-Account Cloudflare Support
2
+ --
3
+ -- Purpose: Registry for managing multiple Cloudflare accounts from a single platform.
4
+ -- Enables federated infrastructure monitoring across accounts.
5
+ --
6
+ -- Use cases:
7
+ -- - Separate accounts for production, staging, client projects
8
+ -- - Consolidated billing and usage visibility across accounts
9
+ -- - Per-account API token management
10
+ --
11
+ -- Architecture:
12
+ -- - Each account has its own CLOUDFLARE_ACCOUNT_ID and API token
13
+ -- - The api_token_env_key column stores the ENV VAR name (not the token itself)
14
+ -- - Workers read the appropriate token from env at runtime
15
+ -- - Primary account is the default for backwards compatibility
16
+
17
+ CREATE TABLE IF NOT EXISTS account_registry (
18
+ account_id TEXT PRIMARY KEY, -- Internal identifier (e.g., "primary", "staging", "client-acme")
19
+ account_name TEXT NOT NULL, -- Human-readable name
20
+ cloudflare_account_id TEXT NOT NULL, -- Cloudflare account ID
21
+ api_token_env_key TEXT NOT NULL, -- ENV var name for the API token (e.g., "CLOUDFLARE_API_TOKEN")
22
+ is_primary INTEGER NOT NULL DEFAULT 0, -- 1 = default account, 0 = secondary
23
+ status TEXT NOT NULL DEFAULT 'active', -- active, disabled
24
+ notes TEXT, -- Optional notes
25
+ created_at TEXT DEFAULT (datetime('now')),
26
+ updated_at TEXT DEFAULT (datetime('now'))
27
+ );
28
+
29
+ -- Ensure only one primary account
30
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_account_registry_primary
31
+ ON account_registry (is_primary) WHERE is_primary = 1;
32
+
33
+ -- Seed with primary account placeholder
34
+ -- TODO: Update cloudflare_account_id with your actual account ID
35
+ INSERT INTO account_registry (
36
+ account_id,
37
+ account_name,
38
+ cloudflare_account_id,
39
+ api_token_env_key,
40
+ is_primary,
41
+ status,
42
+ notes
43
+ ) VALUES (
44
+ 'primary',
45
+ 'Primary Account',
46
+ 'YOUR_CLOUDFLARE_ACCOUNT_ID',
47
+ 'CLOUDFLARE_API_TOKEN',
48
+ 1,
49
+ 'active',
50
+ 'Default account seeded by migration. Update cloudflare_account_id after deployment.'
51
+ );