@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.
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +232 -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/ActivePatterns.tsx +62 -0
- package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
- package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
- package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
- package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
- package/templates/full/dashboard/src/components/patterns/index.ts +5 -0
- package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
- package/templates/full/dashboard/src/components/reports/SdkAuditReport.tsx +72 -0
- package/templates/full/dashboard/src/components/reports/index.ts +2 -0
- package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
- package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
- package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
- package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -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/cache-refresh.ts +38 -0
- package/templates/full/dashboard/src/pages/api/patterns/discover.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
- package/templates/full/dashboard/src/pages/api/patterns/ready-for-review.ts +39 -0
- package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
- package/templates/full/dashboard/src/pages/api/patterns/stats.ts +39 -0
- package/templates/full/dashboard/src/pages/api/patterns/suggestions.ts +43 -0
- package/templates/full/dashboard/src/pages/api/reports/audit.ts +45 -0
- package/templates/full/dashboard/src/pages/api/reports/usage.ts +52 -0
- package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
- package/templates/full/dashboard/src/pages/api/search/reindex.ts +28 -0
- package/templates/full/dashboard/src/pages/api/search/stats.ts +27 -0
- package/templates/full/dashboard/src/pages/api/settings/index.ts +37 -0
- package/templates/full/dashboard/src/pages/api/settings/update.ts +41 -0
- package/templates/full/dashboard/src/pages/api/topology/index.ts +56 -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/scripts/ops/universal-backfill.ts +147 -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/contract-check.yml.hbs +42 -0
- package/templates/shared/.github/workflows/dashboard-deploy.yml.hbs +39 -0
- package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
- package/templates/shared/.github/workflows/security.yml +33 -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 +59 -0
- package/templates/shared/dashboard/src/components/infrastructure/AlertHistory.tsx +57 -0
- package/templates/shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx +73 -0
- package/templates/shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx +55 -0
- package/templates/shared/dashboard/src/components/infrastructure/UptimeStatus.tsx +56 -0
- package/templates/shared/dashboard/src/components/infrastructure/index.ts +4 -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/Breadcrumbs.tsx +27 -0
- package/templates/shared/dashboard/src/components/ui/EmptyState.tsx +26 -0
- package/templates/shared/dashboard/src/components/ui/ErrorBoundary.tsx +42 -0
- package/templates/shared/dashboard/src/components/ui/LoadingSkeleton.tsx +18 -0
- package/templates/shared/dashboard/src/components/ui/PageShell.tsx +26 -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/Toast.tsx +44 -0
- package/templates/shared/dashboard/src/components/ui/index.ts +9 -0
- package/templates/shared/dashboard/src/components/usage/AnomaliesWidget.tsx +68 -0
- package/templates/shared/dashboard/src/components/usage/HourlyUsageChart.tsx +55 -0
- package/templates/shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx +67 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx +55 -0
- package/templates/shared/dashboard/src/components/usage/index.ts +4 -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/cloudflare/costs.ts +21 -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/costs/overview.ts +65 -0
- package/templates/shared/dashboard/src/pages/api/costs/providers.ts +47 -0
- package/templates/shared/dashboard/src/pages/api/infrastructure/services.ts +55 -0
- package/templates/shared/dashboard/src/pages/api/infrastructure/stats.ts +99 -0
- package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
- package/templates/shared/dashboard/src/pages/api/usage/allowances.ts +56 -0
- package/templates/shared/dashboard/src/pages/api/usage/anomalies.ts +45 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing.ts +53 -0
- package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
- package/templates/shared/dashboard/src/pages/api/usage/granular.ts +50 -0
- package/templates/shared/dashboard/src/pages/api/usage/hourly.ts +45 -0
- package/templates/shared/dashboard/src/pages/api/usage/projects.ts +51 -0
- package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
- package/templates/shared/dashboard/src/pages/api/user/identity.ts +11 -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/pages/settings/notifications.astro +34 -0
- package/templates/shared/dashboard/src/pages/settings/thresholds.astro +39 -0
- package/templates/shared/dashboard/src/pages/settings/usage.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/docs/architecture.md +89 -0
- package/templates/shared/docs/post-deploy-runbook.md +126 -0
- package/templates/shared/docs/troubleshooting.md +91 -0
- package/templates/shared/package.json.hbs +17 -1
- package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
- package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
- package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -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-controls.js +141 -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/tests/contract/validate-schemas.test.ts +130 -0
- package/templates/shared/tests/fixtures/telemetry-envelope-invalid.json +9 -0
- package/templates/shared/tests/fixtures/telemetry-envelope-valid.json +27 -0
- package/templates/shared/tests/helpers/mock-d1.ts +61 -0
- package/templates/shared/tests/helpers/mock-kv.ts +37 -0
- package/templates/shared/tests/unit/workers/batch-persistence.test.ts +133 -0
- package/templates/shared/tests/unit/workers/budget-enforcement.test.ts +214 -0
- package/templates/shared/vitest.config.ts +18 -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/CircuitBreakerEvents.tsx +69 -0
- package/templates/standard/dashboard/src/components/health/CircuitBreakerPanel.tsx +97 -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 +4 -0
- package/templates/standard/dashboard/src/lib/errors.ts +28 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts +49 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts +36 -0
- package/templates/standard/dashboard/src/pages/api/errors/[fingerprint].ts +55 -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/audit-history.ts +37 -0
- package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
- package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -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/tests/unit/error-collector/capture.test.ts +106 -0
- package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -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,133 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
interface ErrorRow {
|
|
4
|
+
fingerprint: string;
|
|
5
|
+
message: string;
|
|
6
|
+
script_name: string;
|
|
7
|
+
priority: string;
|
|
8
|
+
occurrence_count: number;
|
|
9
|
+
status: string;
|
|
10
|
+
first_seen_at: string;
|
|
11
|
+
last_seen_at: string;
|
|
12
|
+
github_issue_url: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const priorityColours: Record<string, string> = {
|
|
16
|
+
P0: 'bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300',
|
|
17
|
+
P1: 'bg-orange-100 text-orange-800 dark:bg-orange-900/50 dark:text-orange-300',
|
|
18
|
+
P2: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300',
|
|
19
|
+
P3: 'bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-300',
|
|
20
|
+
P4: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function ErrorsTable() {
|
|
24
|
+
const [errors, setErrors] = useState<ErrorRow[]>([]);
|
|
25
|
+
const [total, setTotal] = useState(0);
|
|
26
|
+
const [page, setPage] = useState(1);
|
|
27
|
+
const [filterPriority, setFilterPriority] = useState<string>('');
|
|
28
|
+
const [sortField, setSortField] = useState<'priority' | 'occurrence_count' | 'last_seen_at'>('priority');
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
const params = new URLSearchParams({ page: String(page), limit: '20' });
|
|
32
|
+
if (filterPriority) params.set('priority', filterPriority);
|
|
33
|
+
|
|
34
|
+
fetch(`/api/errors?${params}`)
|
|
35
|
+
.then(res => res.json())
|
|
36
|
+
.then((data: { errors: ErrorRow[]; total: number }) => {
|
|
37
|
+
setErrors(data.errors ?? []);
|
|
38
|
+
setTotal(data.total ?? 0);
|
|
39
|
+
})
|
|
40
|
+
.catch(() => {});
|
|
41
|
+
}, [page, filterPriority]);
|
|
42
|
+
|
|
43
|
+
const sorted = [...errors].sort((a, b) => {
|
|
44
|
+
if (sortField === 'occurrence_count') return b.occurrence_count - a.occurrence_count;
|
|
45
|
+
if (sortField === 'last_seen_at') return new Date(b.last_seen_at).getTime() - new Date(a.last_seen_at).getTime();
|
|
46
|
+
return 0; // Default server sort by priority
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
|
51
|
+
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
52
|
+
<h3 className="text-sm font-medium text-gray-900 dark:text-white">
|
|
53
|
+
Errors ({total})
|
|
54
|
+
</h3>
|
|
55
|
+
<select
|
|
56
|
+
value={filterPriority}
|
|
57
|
+
onChange={(e) => { setFilterPriority(e.target.value); setPage(1); }}
|
|
58
|
+
className="text-sm border border-gray-300 dark:border-gray-600 rounded px-2 py-1 bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
|
|
59
|
+
>
|
|
60
|
+
<option value="">All Priorities</option>
|
|
61
|
+
<option value="P0">P0</option>
|
|
62
|
+
<option value="P1">P1</option>
|
|
63
|
+
<option value="P2">P2</option>
|
|
64
|
+
<option value="P3">P3</option>
|
|
65
|
+
<option value="P4">P4</option>
|
|
66
|
+
</select>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div className="overflow-x-auto">
|
|
70
|
+
<table className="w-full text-sm">
|
|
71
|
+
<thead>
|
|
72
|
+
<tr className="border-b border-gray-200 dark:border-gray-700">
|
|
73
|
+
<th className="text-left py-2 px-4 text-gray-500 dark:text-gray-400 font-medium">Priority</th>
|
|
74
|
+
<th className="text-left py-2 px-4 text-gray-500 dark:text-gray-400 font-medium">Message</th>
|
|
75
|
+
<th className="text-left py-2 px-4 text-gray-500 dark:text-gray-400 font-medium">Worker</th>
|
|
76
|
+
<th
|
|
77
|
+
className="text-right py-2 px-4 text-gray-500 dark:text-gray-400 font-medium cursor-pointer hover:text-gray-700 dark:hover:text-gray-200"
|
|
78
|
+
onClick={() => setSortField('occurrence_count')}
|
|
79
|
+
>
|
|
80
|
+
Count {sortField === 'occurrence_count' ? '↓' : ''}
|
|
81
|
+
</th>
|
|
82
|
+
<th
|
|
83
|
+
className="text-right py-2 px-4 text-gray-500 dark:text-gray-400 font-medium cursor-pointer hover:text-gray-700 dark:hover:text-gray-200"
|
|
84
|
+
onClick={() => setSortField('last_seen_at')}
|
|
85
|
+
>
|
|
86
|
+
Last Seen {sortField === 'last_seen_at' ? '↓' : ''}
|
|
87
|
+
</th>
|
|
88
|
+
</tr>
|
|
89
|
+
</thead>
|
|
90
|
+
<tbody>
|
|
91
|
+
{sorted.map((err) => (
|
|
92
|
+
<tr key={err.fingerprint} className="border-b border-gray-100 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700/50">
|
|
93
|
+
<td className="py-2 px-4">
|
|
94
|
+
<span className={`text-xs px-1.5 py-0.5 rounded font-medium ${priorityColours[err.priority] ?? priorityColours.P4}`}>
|
|
95
|
+
{err.priority}
|
|
96
|
+
</span>
|
|
97
|
+
</td>
|
|
98
|
+
<td className="py-2 px-4 text-gray-900 dark:text-white max-w-md truncate">{err.message}</td>
|
|
99
|
+
<td className="py-2 px-4 text-gray-500 dark:text-gray-400 font-mono text-xs">{err.script_name}</td>
|
|
100
|
+
<td className="py-2 px-4 text-right text-gray-900 dark:text-white">{err.occurrence_count}</td>
|
|
101
|
+
<td className="py-2 px-4 text-right text-gray-500 dark:text-gray-400 text-xs">
|
|
102
|
+
{new Date(err.last_seen_at).toLocaleDateString('en-AU', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })}
|
|
103
|
+
</td>
|
|
104
|
+
</tr>
|
|
105
|
+
))}
|
|
106
|
+
</tbody>
|
|
107
|
+
</table>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{total > 20 && (
|
|
111
|
+
<div className="p-4 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
112
|
+
<button
|
|
113
|
+
onClick={() => setPage(p => Math.max(1, p - 1))}
|
|
114
|
+
disabled={page === 1}
|
|
115
|
+
className="text-sm text-blue-600 dark:text-blue-400 disabled:opacity-50"
|
|
116
|
+
>
|
|
117
|
+
Previous
|
|
118
|
+
</button>
|
|
119
|
+
<span className="text-sm text-gray-500 dark:text-gray-400">
|
|
120
|
+
Page {page} of {Math.ceil(total / 20)}
|
|
121
|
+
</span>
|
|
122
|
+
<button
|
|
123
|
+
onClick={() => setPage(p => p + 1)}
|
|
124
|
+
disabled={page >= Math.ceil(total / 20)}
|
|
125
|
+
className="text-sm text-blue-600 dark:text-blue-400 disabled:opacity-50"
|
|
126
|
+
>
|
|
127
|
+
Next
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { EmptyState } from '../../components/ui/EmptyState';
|
|
3
|
+
import { LoadingSkeleton } from '../../components/ui/LoadingSkeleton';
|
|
4
|
+
|
|
5
|
+
interface CBEvent {
|
|
6
|
+
id: number;
|
|
7
|
+
feature_key: string;
|
|
8
|
+
event_type: string;
|
|
9
|
+
old_status: string;
|
|
10
|
+
new_status: string;
|
|
11
|
+
reason: string | null;
|
|
12
|
+
timestamp: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function CircuitBreakerEvents() {
|
|
16
|
+
const [events, setEvents] = useState<CBEvent[]>([]);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
fetch('/api/usage/circuit-breakers')
|
|
21
|
+
.then(res => res.json())
|
|
22
|
+
.then((data: { events?: CBEvent[] }) => { setEvents(data.events ?? []); setLoading(false); })
|
|
23
|
+
.catch(() => setLoading(false));
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
if (loading) return <LoadingSkeleton lines={3} />;
|
|
27
|
+
|
|
28
|
+
if (events.length === 0) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
31
|
+
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
|
|
32
|
+
CB Event History
|
|
33
|
+
</h3>
|
|
34
|
+
<EmptyState title="No events" description="No circuit breaker state changes recorded." />
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
41
|
+
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
|
|
42
|
+
CB Event History
|
|
43
|
+
</h3>
|
|
44
|
+
<div className="space-y-2">
|
|
45
|
+
{events.map(ev => (
|
|
46
|
+
<div key={ev.id} className="flex items-start gap-3 py-1.5 border-b border-gray-100 dark:border-gray-700 last:border-0">
|
|
47
|
+
<div className={`mt-1 w-2 h-2 rounded-full shrink-0 ${
|
|
48
|
+
ev.new_status === 'active' ? 'bg-green-500'
|
|
49
|
+
: ev.new_status === 'warning' ? 'bg-yellow-500'
|
|
50
|
+
: 'bg-red-500'
|
|
51
|
+
}`} />
|
|
52
|
+
<div className="min-w-0">
|
|
53
|
+
<p className="text-sm text-gray-900 dark:text-white">
|
|
54
|
+
<span className="font-medium">{ev.feature_key}</span>
|
|
55
|
+
{' '}{ev.old_status} → {ev.new_status}
|
|
56
|
+
</p>
|
|
57
|
+
{ev.reason && (
|
|
58
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">{ev.reason}</p>
|
|
59
|
+
)}
|
|
60
|
+
<p className="text-xs text-gray-400 dark:text-gray-500">
|
|
61
|
+
{new Date(ev.timestamp).toLocaleString()}
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
))}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { LoadingSkeleton } from '../../components/ui/LoadingSkeleton';
|
|
3
|
+
|
|
4
|
+
interface CircuitBreaker {
|
|
5
|
+
key: string;
|
|
6
|
+
feature: string;
|
|
7
|
+
status: string;
|
|
8
|
+
reason: string | null;
|
|
9
|
+
level: 'global' | 'project' | 'feature';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function CircuitBreakerPanel() {
|
|
13
|
+
const [breakers, setBreakers] = useState<CircuitBreaker[]>([]);
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
|
|
16
|
+
const loadBreakers = useCallback(() => {
|
|
17
|
+
fetch('/api/usage/circuit-breakers')
|
|
18
|
+
.then(res => res.json())
|
|
19
|
+
.then((data: { breakers: CircuitBreaker[] }) => {
|
|
20
|
+
const enriched = (data.breakers ?? []).map(cb => ({
|
|
21
|
+
...cb,
|
|
22
|
+
level: (cb.key.includes(':global:') ? 'global'
|
|
23
|
+
: cb.key.includes(':project:') ? 'project'
|
|
24
|
+
: 'feature') as CircuitBreaker['level'],
|
|
25
|
+
}));
|
|
26
|
+
setBreakers(enriched);
|
|
27
|
+
setLoading(false);
|
|
28
|
+
})
|
|
29
|
+
.catch(() => setLoading(false));
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
useEffect(() => { loadBreakers(); }, [loadBreakers]);
|
|
33
|
+
|
|
34
|
+
if (loading) return <LoadingSkeleton lines={4} />;
|
|
35
|
+
|
|
36
|
+
const grouped = {
|
|
37
|
+
global: breakers.filter(b => b.level === 'global'),
|
|
38
|
+
project: breakers.filter(b => b.level === 'project'),
|
|
39
|
+
feature: breakers.filter(b => b.level === 'feature'),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const statusBadge = (status: string) => {
|
|
43
|
+
const styles = status === 'active'
|
|
44
|
+
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
|
45
|
+
: status === 'warning'
|
|
46
|
+
? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
|
|
47
|
+
: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400';
|
|
48
|
+
return <span className={`text-xs px-2 py-0.5 rounded-full font-medium ${styles}`}>{status}</span>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const renderGroup = (title: string, items: CircuitBreaker[]) => {
|
|
52
|
+
if (items.length === 0) return null;
|
|
53
|
+
return (
|
|
54
|
+
<div>
|
|
55
|
+
<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">{title}</h4>
|
|
56
|
+
<div className="space-y-1.5">
|
|
57
|
+
{items.map(cb => (
|
|
58
|
+
<div key={cb.key} className="flex items-center justify-between py-1.5">
|
|
59
|
+
<div>
|
|
60
|
+
<span className="text-sm text-gray-900 dark:text-white">{cb.feature}</span>
|
|
61
|
+
{cb.reason && (
|
|
62
|
+
<p className="text-xs text-gray-500 dark:text-gray-400">{cb.reason}</p>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
{statusBadge(cb.status)}
|
|
66
|
+
</div>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
75
|
+
<div className="flex items-center justify-between mb-4">
|
|
76
|
+
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
|
77
|
+
Circuit Breakers
|
|
78
|
+
</h3>
|
|
79
|
+
<button
|
|
80
|
+
onClick={loadBreakers}
|
|
81
|
+
className="text-xs text-blue-600 dark:text-blue-400 hover:underline"
|
|
82
|
+
>
|
|
83
|
+
Refresh
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
{breakers.length === 0 ? (
|
|
87
|
+
<p className="text-sm text-gray-500 dark:text-gray-400">No circuit breakers configured.</p>
|
|
88
|
+
) : (
|
|
89
|
+
<div className="space-y-4">
|
|
90
|
+
{renderGroup('Global', grouped.global)}
|
|
91
|
+
{renderGroup('Project', grouped.project)}
|
|
92
|
+
{renderGroup('Feature', grouped.feature)}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
interface DlqStatus {
|
|
4
|
+
depth: number;
|
|
5
|
+
oldestAgeMinutes: number | null;
|
|
6
|
+
status: 'healthy' | 'warning' | 'critical' | 'unknown';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function DlqStatusCard() {
|
|
10
|
+
const [data, setData] = useState<DlqStatus | null>(null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
fetch('/api/health/dlq')
|
|
14
|
+
.then(res => res.json())
|
|
15
|
+
.then((dlq: DlqStatus) => setData(dlq))
|
|
16
|
+
.catch(() => {});
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
const statusColour = data?.status === 'healthy'
|
|
20
|
+
? 'text-green-600 dark:text-green-400'
|
|
21
|
+
: data?.status === 'warning'
|
|
22
|
+
? 'text-yellow-600 dark:text-yellow-400'
|
|
23
|
+
: data?.status === 'critical'
|
|
24
|
+
? 'text-red-600 dark:text-red-400'
|
|
25
|
+
: 'text-gray-500 dark:text-gray-400';
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
29
|
+
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
|
|
30
|
+
Dead Letter Queue
|
|
31
|
+
</h3>
|
|
32
|
+
<div className="grid grid-cols-3 gap-4">
|
|
33
|
+
<div>
|
|
34
|
+
<div className="text-2xl font-bold text-gray-900 dark:text-white">{data?.depth ?? 0}</div>
|
|
35
|
+
<div className="text-xs text-gray-500 dark:text-gray-400">Pending</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div>
|
|
38
|
+
<div className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
39
|
+
{data?.oldestAgeMinutes != null ? `${data.oldestAgeMinutes}m` : '\u2014'}
|
|
40
|
+
</div>
|
|
41
|
+
<div className="text-xs text-gray-500 dark:text-gray-400">Oldest</div>
|
|
42
|
+
</div>
|
|
43
|
+
<div>
|
|
44
|
+
<div className={`text-2xl font-bold ${statusColour}`}>
|
|
45
|
+
{data?.status ?? 'Unknown'}
|
|
46
|
+
</div>
|
|
47
|
+
<div className="text-xs text-gray-500 dark:text-gray-400">Status</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { DlqStatusCard } from './DlqStatusCard';
|
|
3
|
+
|
|
4
|
+
type Tab = 'overview' | 'workers' | 'queues' | 'storage';
|
|
5
|
+
|
|
6
|
+
export function HealthTabs() {
|
|
7
|
+
const [activeTab, setActiveTab] = useState<Tab>('overview');
|
|
8
|
+
const [cbData, setCbData] = useState<Array<{
|
|
9
|
+
key: string;
|
|
10
|
+
feature: string;
|
|
11
|
+
status: string;
|
|
12
|
+
reason: string | null;
|
|
13
|
+
}>>([]);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
fetch('/api/usage/circuit-breakers')
|
|
17
|
+
.then(res => res.json())
|
|
18
|
+
.then((data: { breakers: typeof cbData }) => setCbData(data.breakers ?? []))
|
|
19
|
+
.catch(() => {});
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
const tabs: { id: Tab; label: string }[] = [
|
|
23
|
+
{ id: 'overview', label: 'Overview' },
|
|
24
|
+
{ id: 'workers', label: 'Workers' },
|
|
25
|
+
{ id: 'queues', label: 'Queues' },
|
|
26
|
+
{ id: 'storage', label: 'Storage' },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div>
|
|
31
|
+
<div className="border-b border-gray-200 dark:border-gray-700 mb-4">
|
|
32
|
+
<nav className="flex gap-4">
|
|
33
|
+
{tabs.map(tab => (
|
|
34
|
+
<button
|
|
35
|
+
key={tab.id}
|
|
36
|
+
onClick={() => setActiveTab(tab.id)}
|
|
37
|
+
className={`py-2 px-1 text-sm font-medium border-b-2 transition-colors ${
|
|
38
|
+
activeTab === tab.id
|
|
39
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
|
40
|
+
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
|
41
|
+
}`}
|
|
42
|
+
>
|
|
43
|
+
{tab.label}
|
|
44
|
+
</button>
|
|
45
|
+
))}
|
|
46
|
+
</nav>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{activeTab === 'overview' && (
|
|
50
|
+
<div className="space-y-4">
|
|
51
|
+
<DlqStatusCard />
|
|
52
|
+
{/* Circuit Breakers */}
|
|
53
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
54
|
+
<h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
|
|
55
|
+
Circuit Breakers
|
|
56
|
+
</h3>
|
|
57
|
+
{cbData.length === 0 ? (
|
|
58
|
+
<p className="text-sm text-gray-500 dark:text-gray-400">No circuit breakers configured.</p>
|
|
59
|
+
) : (
|
|
60
|
+
<div className="space-y-2">
|
|
61
|
+
{cbData.map(cb => (
|
|
62
|
+
<div key={cb.key} className="flex items-center justify-between py-1">
|
|
63
|
+
<span className="text-sm text-gray-900 dark:text-white">{cb.feature}</span>
|
|
64
|
+
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
|
|
65
|
+
cb.status === 'active' ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
|
66
|
+
: cb.status === 'warning' ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
|
|
67
|
+
: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
|
|
68
|
+
}`}>
|
|
69
|
+
{cb.status}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{activeTab !== 'overview' && (
|
|
80
|
+
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
81
|
+
<p>{activeTab} health details will appear here once monitoring data is collected.</p>
|
|
82
|
+
</div>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface ErrorOccurrence {
|
|
2
|
+
fingerprint: string;
|
|
3
|
+
message: string;
|
|
4
|
+
script_name: string;
|
|
5
|
+
priority: string;
|
|
6
|
+
occurrence_count: number;
|
|
7
|
+
status: string;
|
|
8
|
+
first_seen_at: string;
|
|
9
|
+
last_seen_at: string;
|
|
10
|
+
github_issue_url: string | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ErrorStats {
|
|
14
|
+
byPriority: Record<string, number>;
|
|
15
|
+
byWorker: Array<{ script_name: string; count: number }>;
|
|
16
|
+
total: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getPriorityColour(priority: string): string {
|
|
20
|
+
const colours: Record<string, string> = {
|
|
21
|
+
P0: 'bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300',
|
|
22
|
+
P1: 'bg-orange-100 text-orange-800 dark:bg-orange-900/50 dark:text-orange-300',
|
|
23
|
+
P2: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300',
|
|
24
|
+
P3: 'bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-300',
|
|
25
|
+
P4: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300',
|
|
26
|
+
};
|
|
27
|
+
return colours[priority] ?? colours.P4;
|
|
28
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
interface Env {
|
|
4
|
+
PLATFORM_DB?: D1Database;
|
|
5
|
+
PLATFORM_CACHE?: KVNamespace;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const POST: APIRoute = async ({ locals, params }) => {
|
|
9
|
+
const env = locals.runtime?.env as Env | undefined;
|
|
10
|
+
const db = env?.PLATFORM_DB;
|
|
11
|
+
const kv = env?.PLATFORM_CACHE;
|
|
12
|
+
const fingerprint = params.fingerprint;
|
|
13
|
+
|
|
14
|
+
if (!fingerprint || !db) {
|
|
15
|
+
return new Response(JSON.stringify({ error: 'Missing fingerprint or database' }), {
|
|
16
|
+
status: 400,
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await db
|
|
23
|
+
.prepare(
|
|
24
|
+
`UPDATE error_occurrences
|
|
25
|
+
SET labels = CASE
|
|
26
|
+
WHEN labels IS NULL THEN 'cf:muted'
|
|
27
|
+
WHEN labels NOT LIKE '%cf:muted%' THEN labels || ',cf:muted'
|
|
28
|
+
ELSE labels
|
|
29
|
+
END
|
|
30
|
+
WHERE fingerprint = ?`
|
|
31
|
+
)
|
|
32
|
+
.bind(fingerprint)
|
|
33
|
+
.run();
|
|
34
|
+
|
|
35
|
+
// Also set KV flag for error-collector to skip
|
|
36
|
+
if (kv) {
|
|
37
|
+
await kv.put(`ERROR_FINGERPRINT:${fingerprint}:MUTED`, '1', { expirationTtl: 86400 * 30 });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
});
|
|
43
|
+
} catch {
|
|
44
|
+
return new Response(JSON.stringify({ error: 'Failed to mute error' }), {
|
|
45
|
+
status: 500,
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
interface Env {
|
|
4
|
+
PLATFORM_DB?: D1Database;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const POST: APIRoute = async ({ locals, params }) => {
|
|
8
|
+
const env = locals.runtime?.env as Env | undefined;
|
|
9
|
+
const db = env?.PLATFORM_DB;
|
|
10
|
+
const fingerprint = params.fingerprint;
|
|
11
|
+
|
|
12
|
+
if (!fingerprint || !db) {
|
|
13
|
+
return new Response(JSON.stringify({ error: 'Missing fingerprint or database' }), {
|
|
14
|
+
status: 400,
|
|
15
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await db
|
|
21
|
+
.prepare(
|
|
22
|
+
`UPDATE error_occurrences SET status = 'resolved' WHERE fingerprint = ?`
|
|
23
|
+
)
|
|
24
|
+
.bind(fingerprint)
|
|
25
|
+
.run();
|
|
26
|
+
|
|
27
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
28
|
+
headers: { 'Content-Type': 'application/json' },
|
|
29
|
+
});
|
|
30
|
+
} catch {
|
|
31
|
+
return new Response(JSON.stringify({ error: 'Failed to resolve error' }), {
|
|
32
|
+
status: 500,
|
|
33
|
+
headers: { 'Content-Type': 'application/json' },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
interface Env {
|
|
4
|
+
PLATFORM_DB?: D1Database;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const GET: APIRoute = async ({ locals, params }) => {
|
|
8
|
+
const env = locals.runtime?.env as Env | undefined;
|
|
9
|
+
const db = env?.PLATFORM_DB;
|
|
10
|
+
const fingerprint = params.fingerprint;
|
|
11
|
+
|
|
12
|
+
if (!fingerprint) {
|
|
13
|
+
return new Response(JSON.stringify({ error: 'Missing fingerprint' }), {
|
|
14
|
+
status: 400,
|
|
15
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!db) {
|
|
20
|
+
return new Response(JSON.stringify({ error: 'Database not available' }), {
|
|
21
|
+
status: 503,
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const row = await db
|
|
28
|
+
.prepare(
|
|
29
|
+
`SELECT fingerprint, normalized_message, script_name, priority, status,
|
|
30
|
+
occurrence_count, first_seen_at, last_seen_at, github_issue_url,
|
|
31
|
+
labels, error_category
|
|
32
|
+
FROM error_occurrences
|
|
33
|
+
WHERE fingerprint = ?
|
|
34
|
+
LIMIT 1`
|
|
35
|
+
)
|
|
36
|
+
.bind(fingerprint)
|
|
37
|
+
.first();
|
|
38
|
+
|
|
39
|
+
if (!row) {
|
|
40
|
+
return new Response(JSON.stringify({ error: 'Not found' }), {
|
|
41
|
+
status: 404,
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return new Response(JSON.stringify(row), {
|
|
47
|
+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=30' },
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return new Response(JSON.stringify({ error: 'Failed to fetch error detail' }), {
|
|
51
|
+
status: 500,
|
|
52
|
+
headers: { 'Content-Type': 'application/json' },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
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({ errors: [], 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
|
+
const priority = url.searchParams.get('priority');
|
|
16
|
+
const status = url.searchParams.get('status') ?? 'open';
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
let query = `SELECT fingerprint, normalized_message as message, script_name, priority,
|
|
20
|
+
occurrence_count, status, first_seen_at, last_seen_at, github_issue_url
|
|
21
|
+
FROM error_occurrences WHERE status = ?`;
|
|
22
|
+
const params: unknown[] = [status];
|
|
23
|
+
|
|
24
|
+
if (priority) {
|
|
25
|
+
query += ` AND priority = ?`;
|
|
26
|
+
params.push(priority);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
query += ` ORDER BY
|
|
30
|
+
CASE priority WHEN 'P0' THEN 1 WHEN 'P1' THEN 2 WHEN 'P2' THEN 3 WHEN 'P3' THEN 4 ELSE 5 END,
|
|
31
|
+
last_seen_at DESC
|
|
32
|
+
LIMIT ? OFFSET ?`;
|
|
33
|
+
params.push(limit, offset);
|
|
34
|
+
|
|
35
|
+
const errors = await db.prepare(query).bind(...params).all();
|
|
36
|
+
|
|
37
|
+
const countQuery = priority
|
|
38
|
+
? `SELECT COUNT(*) as total FROM error_occurrences WHERE status = ? AND priority = ? LIMIT 1`
|
|
39
|
+
: `SELECT COUNT(*) as total FROM error_occurrences WHERE status = ? LIMIT 1`;
|
|
40
|
+
const countParams = priority ? [status, priority] : [status];
|
|
41
|
+
const countResult = await db.prepare(countQuery).bind(...countParams).first<{ total: number }>();
|
|
42
|
+
|
|
43
|
+
return new Response(
|
|
44
|
+
JSON.stringify({
|
|
45
|
+
errors: errors.results ?? [],
|
|
46
|
+
total: countResult?.total ?? 0,
|
|
47
|
+
page,
|
|
48
|
+
limit,
|
|
49
|
+
}),
|
|
50
|
+
{ headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=30' } }
|
|
51
|
+
);
|
|
52
|
+
} catch {
|
|
53
|
+
return new Response(JSON.stringify({ errors: [], total: 0, error: 'Query failed' }), {
|
|
54
|
+
status: 500,
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
};
|