@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.
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +121 -2
- package/package.json +1 -1
- package/templates/full/config/audit-targets.yaml +72 -0
- package/templates/full/dashboard/src/components/notifications/NotificationBell.tsx +30 -0
- package/templates/full/dashboard/src/components/notifications/NotificationList.tsx +116 -0
- package/templates/full/dashboard/src/components/notifications/index.ts +2 -0
- package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
- package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
- package/templates/full/dashboard/src/components/patterns/index.ts +2 -0
- package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
- package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
- package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
- package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
- package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
- package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
- package/templates/full/dashboard/src/pages/notifications.astro +11 -0
- package/templates/full/migrations/008_auditor.sql +99 -0
- package/templates/full/migrations/010_pricing_versions.sql +110 -0
- package/templates/full/migrations/011_multi_account.sql +51 -0
- package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
- package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
- package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
- package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
- package/templates/full/workers/lib/auditor/index.ts +9 -0
- package/templates/full/workers/lib/auditor/types.ts +167 -0
- package/templates/full/workers/platform-auditor.ts +1071 -0
- package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
- package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
- package/templates/shared/config/observability.yaml.hbs +276 -0
- package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
- package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
- package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
- package/templates/shared/dashboard/astro.config.mjs +21 -0
- package/templates/shared/dashboard/package.json.hbs +29 -0
- package/templates/shared/dashboard/src/components/Header.astro +29 -0
- package/templates/shared/dashboard/src/components/Nav.astro.hbs +57 -0
- package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
- package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
- package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
- package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
- package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
- package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
- package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
- package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
- package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
- package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
- package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
- package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
- package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
- package/templates/shared/dashboard/src/components/ui/index.ts +3 -0
- package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
- package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
- package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
- package/templates/shared/dashboard/src/lib/types.ts +72 -0
- package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
- package/templates/shared/dashboard/src/middleware/index.ts +1 -0
- package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
- package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
- package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
- package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
- package/templates/shared/dashboard/src/pages/index.astro +3 -0
- package/templates/shared/dashboard/src/pages/resources.astro +11 -0
- package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
- package/templates/shared/dashboard/src/styles/global.css +29 -0
- package/templates/shared/dashboard/tailwind.config.mjs +9 -0
- package/templates/shared/dashboard/tsconfig.json +9 -0
- package/templates/shared/dashboard/wrangler.json.hbs +47 -0
- package/templates/shared/package.json.hbs +12 -1
- package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
- package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
- package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
- package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
- package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
- package/templates/shared/scripts/validate-schemas.js +61 -0
- package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
- package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
- package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
- package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
- package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
- package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
- package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
- package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
- package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
- package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
- package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
- package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
- package/templates/shared/workers/platform-usage.ts +98 -8
- package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
- package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
- package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
- package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
- package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
- package/templates/standard/dashboard/src/components/health/index.ts +2 -0
- package/templates/standard/dashboard/src/lib/errors.ts +28 -0
- package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
- package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
- package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
- package/templates/standard/dashboard/src/pages/errors.astro +13 -0
- package/templates/standard/dashboard/src/pages/health.astro +11 -0
- package/templates/standard/migrations/009_topology_mapper.sql +65 -0
- package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
- package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
- package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
- package/templates/standard/workers/lib/mapper/index.ts +7 -0
- package/templates/standard/workers/platform-mapper.ts +482 -0
- package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
- package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
- 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
|
+
);
|