@littlebearapps/platform-admin-sdk 1.5.0 → 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 +112 -1
- package/package.json +1 -1
- package/templates/full/dashboard/src/components/patterns/ActivePatterns.tsx +62 -0
- package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
- package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
- package/templates/full/dashboard/src/components/patterns/index.ts +3 -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/pages/api/notifications/[id]/read.ts +37 -0
- package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -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/ready-for-review.ts +39 -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/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/scripts/ops/universal-backfill.ts +147 -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/security.yml +33 -0
- package/templates/shared/dashboard/src/components/Nav.astro.hbs +2 -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/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/Toast.tsx +44 -0
- package/templates/shared/dashboard/src/components/ui/index.ts +6 -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/lib/cloudflare/costs.ts +21 -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/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/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/user/identity.ts +11 -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/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 +5 -0
- package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
- package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -0
- package/templates/shared/scripts/ops/validate-controls.js +141 -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/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/index.ts +2 -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/health/audit-history.ts +37 -0
- package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -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/dist/templates.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { Tier } from './prompts.js';
|
|
8
8
|
/** Single source of truth for the SDK version. */
|
|
9
|
-
export declare const SDK_VERSION = "
|
|
9
|
+
export declare const SDK_VERSION = "2.0.0";
|
|
10
10
|
/** Returns true if `to` is the same or higher tier than `from`. */
|
|
11
11
|
export declare function isTierUpgradeOrSame(from: Tier, to: Tier): boolean;
|
|
12
12
|
/** Check if a template file is a numbered migration (not seed.sql). */
|
package/dist/templates.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* All other files are copied verbatim.
|
|
6
6
|
*/
|
|
7
7
|
/** Single source of truth for the SDK version. */
|
|
8
|
-
export const SDK_VERSION = '
|
|
8
|
+
export const SDK_VERSION = '2.0.0';
|
|
9
9
|
/** Tier ordering for upgrade validation. */
|
|
10
10
|
const TIER_ORDER = { minimal: 0, standard: 1, full: 2 };
|
|
11
11
|
/** Returns true if `to` is the same or higher tier than `from`. */
|
|
@@ -147,6 +147,72 @@ const SHARED_FILES = [
|
|
|
147
147
|
{ src: 'shared/dashboard/src/components/settings/index.ts', dest: 'dashboard/src/components/settings/index.ts', template: false },
|
|
148
148
|
{ src: 'shared/dashboard/src/lib/types.ts', dest: 'dashboard/src/lib/types.ts', template: false },
|
|
149
149
|
{ src: 'shared/dashboard/src/lib/fetch.ts', dest: 'dashboard/src/lib/fetch.ts', template: false },
|
|
150
|
+
// Dashboard — shared tier: UI primitives (v1.6.0)
|
|
151
|
+
{ src: 'shared/dashboard/src/components/ui/EmptyState.tsx', dest: 'dashboard/src/components/ui/EmptyState.tsx', template: false },
|
|
152
|
+
{ src: 'shared/dashboard/src/components/ui/LoadingSkeleton.tsx', dest: 'dashboard/src/components/ui/LoadingSkeleton.tsx', template: false },
|
|
153
|
+
{ src: 'shared/dashboard/src/components/ui/Breadcrumbs.tsx', dest: 'dashboard/src/components/ui/Breadcrumbs.tsx', template: false },
|
|
154
|
+
{ src: 'shared/dashboard/src/components/ui/Toast.tsx', dest: 'dashboard/src/components/ui/Toast.tsx', template: false },
|
|
155
|
+
// Dashboard — shared tier: usage API routes (v1.6.0)
|
|
156
|
+
{ src: 'shared/dashboard/src/pages/api/usage/billing.ts', dest: 'dashboard/src/pages/api/usage/billing.ts', template: false },
|
|
157
|
+
{ src: 'shared/dashboard/src/pages/api/usage/hourly.ts', dest: 'dashboard/src/pages/api/usage/hourly.ts', template: false },
|
|
158
|
+
{ src: 'shared/dashboard/src/pages/api/usage/granular.ts', dest: 'dashboard/src/pages/api/usage/granular.ts', template: false },
|
|
159
|
+
{ src: 'shared/dashboard/src/pages/api/usage/projects.ts', dest: 'dashboard/src/pages/api/usage/projects.ts', template: false },
|
|
160
|
+
{ src: 'shared/dashboard/src/pages/api/usage/allowances.ts', dest: 'dashboard/src/pages/api/usage/allowances.ts', template: false },
|
|
161
|
+
{ src: 'shared/dashboard/src/pages/api/usage/anomalies.ts', dest: 'dashboard/src/pages/api/usage/anomalies.ts', template: false },
|
|
162
|
+
// Dashboard — shared tier: costs API routes (v1.6.0)
|
|
163
|
+
{ src: 'shared/dashboard/src/pages/api/costs/overview.ts', dest: 'dashboard/src/pages/api/costs/overview.ts', template: false },
|
|
164
|
+
{ src: 'shared/dashboard/src/pages/api/costs/providers.ts', dest: 'dashboard/src/pages/api/costs/providers.ts', template: false },
|
|
165
|
+
// Dashboard — shared tier: infrastructure API routes (v1.6.0)
|
|
166
|
+
{ src: 'shared/dashboard/src/pages/api/infrastructure/stats.ts', dest: 'dashboard/src/pages/api/infrastructure/stats.ts', template: false },
|
|
167
|
+
{ src: 'shared/dashboard/src/pages/api/infrastructure/services.ts', dest: 'dashboard/src/pages/api/infrastructure/services.ts', template: false },
|
|
168
|
+
// Dashboard — shared tier: user API route (v1.6.0)
|
|
169
|
+
{ src: 'shared/dashboard/src/pages/api/user/identity.ts', dest: 'dashboard/src/pages/api/user/identity.ts', template: false },
|
|
170
|
+
// Dashboard — shared tier: cloudflare lib (v1.6.0)
|
|
171
|
+
{ src: 'shared/dashboard/src/lib/cloudflare/costs.ts', dest: 'dashboard/src/lib/cloudflare/costs.ts', template: false },
|
|
172
|
+
// Dashboard — shared tier: infrastructure components (v1.6.0)
|
|
173
|
+
{ src: 'shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx', dest: 'dashboard/src/components/infrastructure/InfrastructureStats.tsx', template: false },
|
|
174
|
+
{ src: 'shared/dashboard/src/components/infrastructure/index.ts', dest: 'dashboard/src/components/infrastructure/index.ts', template: false },
|
|
175
|
+
// Dashboard — shared tier: usage components (v1.6.0)
|
|
176
|
+
{ src: 'shared/dashboard/src/components/usage/HourlyUsageChart.tsx', dest: 'dashboard/src/components/usage/HourlyUsageChart.tsx', template: false },
|
|
177
|
+
{ src: 'shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx', dest: 'dashboard/src/components/usage/PlanAllowanceDashboard.tsx', template: false },
|
|
178
|
+
{ src: 'shared/dashboard/src/components/usage/AnomaliesWidget.tsx', dest: 'dashboard/src/components/usage/AnomaliesWidget.tsx', template: false },
|
|
179
|
+
{ src: 'shared/dashboard/src/components/usage/index.ts', dest: 'dashboard/src/components/usage/index.ts', template: false },
|
|
180
|
+
// Dashboard — shared tier: settings pages (v1.6.0)
|
|
181
|
+
{ src: 'shared/dashboard/src/pages/settings/notifications.astro', dest: 'dashboard/src/pages/settings/notifications.astro', template: false },
|
|
182
|
+
{ src: 'shared/dashboard/src/pages/settings/thresholds.astro', dest: 'dashboard/src/pages/settings/thresholds.astro', template: false },
|
|
183
|
+
{ src: 'shared/dashboard/src/pages/settings/usage.astro', dest: 'dashboard/src/pages/settings/usage.astro', template: false },
|
|
184
|
+
// CI — dashboard deploy workflow (v1.6.0)
|
|
185
|
+
{ src: 'shared/.github/workflows/dashboard-deploy.yml.hbs', dest: '.github/workflows/dashboard-deploy.yml', template: true },
|
|
186
|
+
// Test infrastructure (v1.7.0)
|
|
187
|
+
{ src: 'shared/vitest.config.ts', dest: 'vitest.config.ts', template: false },
|
|
188
|
+
{ src: 'shared/tests/helpers/mock-kv.ts', dest: 'tests/helpers/mock-kv.ts', template: false },
|
|
189
|
+
{ src: 'shared/tests/helpers/mock-d1.ts', dest: 'tests/helpers/mock-d1.ts', template: false },
|
|
190
|
+
{ src: 'shared/tests/fixtures/telemetry-envelope-valid.json', dest: 'tests/fixtures/telemetry-envelope-valid.json', template: false },
|
|
191
|
+
{ src: 'shared/tests/fixtures/telemetry-envelope-invalid.json', dest: 'tests/fixtures/telemetry-envelope-invalid.json', template: false },
|
|
192
|
+
// Shared tests (v1.7.0)
|
|
193
|
+
{ src: 'shared/tests/unit/workers/budget-enforcement.test.ts', dest: 'tests/unit/workers/budget-enforcement.test.ts', template: false },
|
|
194
|
+
{ src: 'shared/tests/unit/workers/batch-persistence.test.ts', dest: 'tests/unit/workers/batch-persistence.test.ts', template: false },
|
|
195
|
+
{ src: 'shared/tests/contract/validate-schemas.test.ts', dest: 'tests/contract/validate-schemas.test.ts', template: false },
|
|
196
|
+
// Backfill scripts (v1.8.0)
|
|
197
|
+
{ src: 'shared/scripts/ops/backfill-cloudflare-daily.ts', dest: 'scripts/ops/backfill-cloudflare-daily.ts', template: false },
|
|
198
|
+
{ src: 'shared/scripts/ops/backfill-monthly-rollups.ts', dest: 'scripts/ops/backfill-monthly-rollups.ts', template: false },
|
|
199
|
+
{ src: 'shared/scripts/ops/validate-controls.js', dest: 'scripts/ops/validate-controls.js', template: false },
|
|
200
|
+
// Dashboard — shared tier: additional infrastructure components (v2.0.0)
|
|
201
|
+
{ src: 'shared/dashboard/src/components/infrastructure/UptimeStatus.tsx', dest: 'dashboard/src/components/infrastructure/UptimeStatus.tsx', template: false },
|
|
202
|
+
{ src: 'shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx', dest: 'dashboard/src/components/infrastructure/ServiceRegistry.tsx', template: false },
|
|
203
|
+
{ src: 'shared/dashboard/src/components/infrastructure/AlertHistory.tsx', dest: 'dashboard/src/components/infrastructure/AlertHistory.tsx', template: false },
|
|
204
|
+
// Dashboard — shared tier: additional usage components (v2.0.0)
|
|
205
|
+
{ src: 'shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx', dest: 'dashboard/src/components/usage/ProjectCostBreakdown.tsx', template: false },
|
|
206
|
+
// Dashboard — shared tier: additional UI components (v2.0.0)
|
|
207
|
+
{ src: 'shared/dashboard/src/components/ui/ErrorBoundary.tsx', dest: 'dashboard/src/components/ui/ErrorBoundary.tsx', template: false },
|
|
208
|
+
{ src: 'shared/dashboard/src/components/ui/PageShell.tsx', dest: 'dashboard/src/components/ui/PageShell.tsx', template: false },
|
|
209
|
+
// CI — contract check + security workflows (v2.0.0)
|
|
210
|
+
{ src: 'shared/.github/workflows/contract-check.yml.hbs', dest: '.github/workflows/contract-check.yml', template: true },
|
|
211
|
+
{ src: 'shared/.github/workflows/security.yml', dest: '.github/workflows/security.yml', template: false },
|
|
212
|
+
// Documentation (v2.0.0)
|
|
213
|
+
{ src: 'shared/docs/troubleshooting.md', dest: 'docs/troubleshooting.md', template: false },
|
|
214
|
+
{ src: 'shared/docs/architecture.md', dest: 'docs/architecture.md', template: false },
|
|
215
|
+
{ src: 'shared/docs/post-deploy-runbook.md', dest: 'docs/post-deploy-runbook.md', template: false },
|
|
150
216
|
];
|
|
151
217
|
const STANDARD_FILES = [
|
|
152
218
|
// Additional migrations
|
|
@@ -190,6 +256,20 @@ const STANDARD_FILES = [
|
|
|
190
256
|
{ src: 'standard/dashboard/src/components/errors/ErrorStats.tsx', dest: 'dashboard/src/components/errors/ErrorStats.tsx', template: false },
|
|
191
257
|
{ src: 'standard/dashboard/src/components/errors/index.ts', dest: 'dashboard/src/components/errors/index.ts', template: false },
|
|
192
258
|
{ src: 'standard/dashboard/src/lib/errors.ts', dest: 'dashboard/src/lib/errors.ts', template: false },
|
|
259
|
+
// Dashboard — standard tier: error detail API routes (v1.6.0)
|
|
260
|
+
{ src: 'standard/dashboard/src/pages/api/errors/[fingerprint].ts', dest: 'dashboard/src/pages/api/errors/[fingerprint].ts', template: false },
|
|
261
|
+
{ src: 'standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts', dest: 'dashboard/src/pages/api/errors/[fingerprint]/mute.ts', template: false },
|
|
262
|
+
{ src: 'standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts', dest: 'dashboard/src/pages/api/errors/[fingerprint]/resolve.ts', template: false },
|
|
263
|
+
// Dashboard — standard tier: circuit breaker components (v1.6.0)
|
|
264
|
+
{ src: 'standard/dashboard/src/components/health/CircuitBreakerPanel.tsx', dest: 'dashboard/src/components/health/CircuitBreakerPanel.tsx', template: false },
|
|
265
|
+
{ src: 'standard/dashboard/src/components/health/CircuitBreakerEvents.tsx', dest: 'dashboard/src/components/health/CircuitBreakerEvents.tsx', template: false },
|
|
266
|
+
// Dashboard — standard tier: circuit breakers page (v1.6.0)
|
|
267
|
+
{ src: 'standard/dashboard/src/pages/circuit-breakers.astro', dest: 'dashboard/src/pages/circuit-breakers.astro', template: false },
|
|
268
|
+
// Standard tests (v1.7.0)
|
|
269
|
+
{ src: 'standard/tests/unit/error-collector/fingerprint.test.ts', dest: 'tests/unit/error-collector/fingerprint.test.ts', template: false },
|
|
270
|
+
{ src: 'standard/tests/unit/error-collector/capture.test.ts', dest: 'tests/unit/error-collector/capture.test.ts', template: false },
|
|
271
|
+
// Dashboard — standard tier: audit history API route (v2.0.0)
|
|
272
|
+
{ src: 'standard/dashboard/src/pages/api/health/audit-history.ts', dest: 'dashboard/src/pages/api/health/audit-history.ts', template: false },
|
|
193
273
|
];
|
|
194
274
|
const FULL_FILES = [
|
|
195
275
|
// Additional migrations
|
|
@@ -237,6 +317,8 @@ const FULL_FILES = [
|
|
|
237
317
|
{ src: 'full/migrations/011_multi_account.sql', dest: 'storage/d1/migrations/011_multi_account.sql', template: false },
|
|
238
318
|
// KV pricing configuration script
|
|
239
319
|
{ src: 'full/scripts/ops/set-kv-pricing.ts', dest: 'scripts/ops/set-kv-pricing.ts', template: false },
|
|
320
|
+
// Universal backfill cascade (v1.8.0)
|
|
321
|
+
{ src: 'full/scripts/ops/universal-backfill.ts', dest: 'scripts/ops/universal-backfill.ts', template: false },
|
|
240
322
|
// Dashboard — full tier (patterns, notifications, search)
|
|
241
323
|
{ src: 'full/dashboard/src/pages/notifications.astro', dest: 'dashboard/src/pages/notifications.astro', template: false },
|
|
242
324
|
{ src: 'full/dashboard/src/pages/api/patterns/index.ts', dest: 'dashboard/src/pages/api/patterns/index.ts', template: false },
|
|
@@ -252,6 +334,35 @@ const FULL_FILES = [
|
|
|
252
334
|
{ src: 'full/dashboard/src/components/notifications/NotificationList.tsx', dest: 'dashboard/src/components/notifications/NotificationList.tsx', template: false },
|
|
253
335
|
{ src: 'full/dashboard/src/components/notifications/index.ts', dest: 'dashboard/src/components/notifications/index.ts', template: false },
|
|
254
336
|
{ src: 'full/dashboard/src/components/search/SearchModal.tsx', dest: 'dashboard/src/components/search/SearchModal.tsx', template: false },
|
|
337
|
+
// Dashboard — full tier: pattern tabs component (v1.6.0)
|
|
338
|
+
{ src: 'full/dashboard/src/components/patterns/PatternTabs.tsx', dest: 'dashboard/src/components/patterns/PatternTabs.tsx', template: false },
|
|
339
|
+
// Dashboard — full tier: additional pattern API routes (v2.0.0)
|
|
340
|
+
{ src: 'full/dashboard/src/pages/api/patterns/discover.ts', dest: 'dashboard/src/pages/api/patterns/discover.ts', template: false },
|
|
341
|
+
{ src: 'full/dashboard/src/pages/api/patterns/suggestions.ts', dest: 'dashboard/src/pages/api/patterns/suggestions.ts', template: false },
|
|
342
|
+
{ src: 'full/dashboard/src/pages/api/patterns/ready-for-review.ts', dest: 'dashboard/src/pages/api/patterns/ready-for-review.ts', template: false },
|
|
343
|
+
{ src: 'full/dashboard/src/pages/api/patterns/cache-refresh.ts', dest: 'dashboard/src/pages/api/patterns/cache-refresh.ts', template: false },
|
|
344
|
+
{ src: 'full/dashboard/src/pages/api/patterns/stats.ts', dest: 'dashboard/src/pages/api/patterns/stats.ts', template: false },
|
|
345
|
+
// Dashboard — full tier: settings API routes (v2.0.0)
|
|
346
|
+
{ src: 'full/dashboard/src/pages/api/settings/index.ts', dest: 'dashboard/src/pages/api/settings/index.ts', template: false },
|
|
347
|
+
{ src: 'full/dashboard/src/pages/api/settings/update.ts', dest: 'dashboard/src/pages/api/settings/update.ts', template: false },
|
|
348
|
+
// Dashboard — full tier: notification management (v2.0.0)
|
|
349
|
+
{ src: 'full/dashboard/src/pages/api/notifications/[id]/read.ts', dest: 'dashboard/src/pages/api/notifications/[id]/read.ts', template: false },
|
|
350
|
+
{ src: 'full/dashboard/src/pages/api/notifications/read-all.ts', dest: 'dashboard/src/pages/api/notifications/read-all.ts', template: false },
|
|
351
|
+
// Dashboard — full tier: search API routes (v2.0.0)
|
|
352
|
+
{ src: 'full/dashboard/src/pages/api/search/reindex.ts', dest: 'dashboard/src/pages/api/search/reindex.ts', template: false },
|
|
353
|
+
{ src: 'full/dashboard/src/pages/api/search/stats.ts', dest: 'dashboard/src/pages/api/search/stats.ts', template: false },
|
|
354
|
+
// Dashboard — full tier: reports API routes (v2.0.0)
|
|
355
|
+
{ src: 'full/dashboard/src/pages/api/reports/audit.ts', dest: 'dashboard/src/pages/api/reports/audit.ts', template: false },
|
|
356
|
+
{ src: 'full/dashboard/src/pages/api/reports/usage.ts', dest: 'dashboard/src/pages/api/reports/usage.ts', template: false },
|
|
357
|
+
// Dashboard — full tier: topology API route (v2.0.0)
|
|
358
|
+
{ src: 'full/dashboard/src/pages/api/topology/index.ts', dest: 'dashboard/src/pages/api/topology/index.ts', template: false },
|
|
359
|
+
// Dashboard — full tier: additional pattern components (v2.0.0)
|
|
360
|
+
{ src: 'full/dashboard/src/components/patterns/ActivePatterns.tsx', dest: 'dashboard/src/components/patterns/ActivePatterns.tsx', template: false },
|
|
361
|
+
{ src: 'full/dashboard/src/components/patterns/SystemPatterns.tsx', dest: 'dashboard/src/components/patterns/SystemPatterns.tsx', template: false },
|
|
362
|
+
// Dashboard — full tier: reports components (v2.0.0)
|
|
363
|
+
{ src: 'full/dashboard/src/components/reports/SdkAuditReport.tsx', dest: 'dashboard/src/components/reports/SdkAuditReport.tsx', template: false },
|
|
364
|
+
{ src: 'full/dashboard/src/components/reports/GapDetectionReport.tsx', dest: 'dashboard/src/components/reports/GapDetectionReport.tsx', template: false },
|
|
365
|
+
{ src: 'full/dashboard/src/components/reports/index.ts', dest: 'dashboard/src/components/reports/index.ts', template: false },
|
|
255
366
|
];
|
|
256
367
|
export function getFilesForTier(tier) {
|
|
257
368
|
const files = [...SHARED_FILES];
|
package/package.json
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { LoadingSkeleton } from '../../components/ui/LoadingSkeleton';
|
|
3
|
+
import { EmptyState } from '../../components/ui/EmptyState';
|
|
4
|
+
|
|
5
|
+
interface Pattern {
|
|
6
|
+
id: number;
|
|
7
|
+
pattern_type: string;
|
|
8
|
+
pattern_value: string;
|
|
9
|
+
error_type: string;
|
|
10
|
+
priority: string;
|
|
11
|
+
match_count: number;
|
|
12
|
+
source: string;
|
|
13
|
+
is_protected: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ActivePatterns() {
|
|
17
|
+
const [patterns, setPatterns] = useState<Pattern[]>([]);
|
|
18
|
+
const [loading, setLoading] = useState(true);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
fetch('/api/patterns/suggestions?status=approved&limit=50')
|
|
22
|
+
.then((r) => r.json())
|
|
23
|
+
.then((data: { suggestions: Pattern[] }) => {
|
|
24
|
+
setPatterns(data.suggestions ?? []);
|
|
25
|
+
setLoading(false);
|
|
26
|
+
})
|
|
27
|
+
.catch(() => setLoading(false));
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
if (loading) return <LoadingSkeleton lines={4} />;
|
|
31
|
+
if (patterns.length === 0) return <EmptyState title="No active patterns" description="Approved patterns will appear here." />;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="space-y-2">
|
|
35
|
+
{patterns.map((p) => (
|
|
36
|
+
<div
|
|
37
|
+
key={p.id}
|
|
38
|
+
className="flex items-center justify-between gap-3 py-2 px-3 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700"
|
|
39
|
+
>
|
|
40
|
+
<div className="min-w-0">
|
|
41
|
+
<div className="flex items-center gap-2">
|
|
42
|
+
<span className="text-sm font-mono text-gray-900 dark:text-white truncate">{p.pattern_value}</span>
|
|
43
|
+
{p.is_protected === 1 && (
|
|
44
|
+
<span className="text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400">
|
|
45
|
+
protected
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
<div className="flex gap-3 mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
|
50
|
+
<span>{p.pattern_type}</span>
|
|
51
|
+
<span>{p.source}</span>
|
|
52
|
+
<span>Matches: {p.match_count}</span>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<span className="text-xs px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 shrink-0">
|
|
56
|
+
{p.priority}
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
))}
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { EmptyState } from '../../components/ui/EmptyState';
|
|
3
|
+
import { LoadingSkeleton } from '../../components/ui/LoadingSkeleton';
|
|
4
|
+
|
|
5
|
+
type Tab = 'pending' | 'shadow' | 'approved' | 'rejected';
|
|
6
|
+
|
|
7
|
+
interface Pattern {
|
|
8
|
+
id: number;
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
match_rule: string;
|
|
12
|
+
status: string;
|
|
13
|
+
source: string;
|
|
14
|
+
created_at: string;
|
|
15
|
+
match_count?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function PatternTabs() {
|
|
19
|
+
const [activeTab, setActiveTab] = useState<Tab>('pending');
|
|
20
|
+
const [patterns, setPatterns] = useState<Pattern[]>([]);
|
|
21
|
+
const [loading, setLoading] = useState(true);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
setLoading(true);
|
|
25
|
+
fetch(`/api/patterns?status=${activeTab}`)
|
|
26
|
+
.then(res => res.json())
|
|
27
|
+
.then((data: { patterns: Pattern[] }) => { setPatterns(data.patterns ?? []); setLoading(false); })
|
|
28
|
+
.catch(() => setLoading(false));
|
|
29
|
+
}, [activeTab]);
|
|
30
|
+
|
|
31
|
+
const tabs: { id: Tab; label: string }[] = [
|
|
32
|
+
{ id: 'pending', label: 'Pending' },
|
|
33
|
+
{ id: 'shadow', label: 'Shadow' },
|
|
34
|
+
{ id: 'approved', label: 'Approved' },
|
|
35
|
+
{ id: 'rejected', label: 'Rejected' },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const handleAction = async (id: number, action: 'approve' | 'reject') => {
|
|
39
|
+
const endpoint = action === 'approve' ? '/api/patterns/approve' : '/api/patterns/reject';
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetch(endpoint, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({ id }),
|
|
45
|
+
});
|
|
46
|
+
if (res.ok) {
|
|
47
|
+
setPatterns(prev => prev.filter(p => p.id !== id));
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// Silent failure — user can retry
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div>
|
|
56
|
+
<div className="border-b border-gray-200 dark:border-gray-700 mb-4">
|
|
57
|
+
<nav className="flex gap-4">
|
|
58
|
+
{tabs.map(tab => (
|
|
59
|
+
<button
|
|
60
|
+
key={tab.id}
|
|
61
|
+
onClick={() => setActiveTab(tab.id)}
|
|
62
|
+
className={`py-2 px-1 text-sm font-medium border-b-2 transition-colors ${
|
|
63
|
+
activeTab === tab.id
|
|
64
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
|
65
|
+
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
|
|
66
|
+
}`}
|
|
67
|
+
>
|
|
68
|
+
{tab.label}
|
|
69
|
+
</button>
|
|
70
|
+
))}
|
|
71
|
+
</nav>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{loading ? (
|
|
75
|
+
<LoadingSkeleton lines={4} />
|
|
76
|
+
) : patterns.length === 0 ? (
|
|
77
|
+
<EmptyState title={`No ${activeTab} patterns`} description="Patterns will appear here as they are discovered." />
|
|
78
|
+
) : (
|
|
79
|
+
<div className="space-y-3">
|
|
80
|
+
{patterns.map(p => (
|
|
81
|
+
<div key={p.id} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
82
|
+
<div className="flex items-start justify-between gap-3">
|
|
83
|
+
<div className="min-w-0">
|
|
84
|
+
<p className="text-sm font-medium text-gray-900 dark:text-white">{p.name}</p>
|
|
85
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">{p.description}</p>
|
|
86
|
+
<p className="text-xs text-gray-400 dark:text-gray-500 mt-1 font-mono truncate">{p.match_rule}</p>
|
|
87
|
+
<div className="flex items-center gap-3 mt-2 text-xs text-gray-500 dark:text-gray-400">
|
|
88
|
+
<span>Source: {p.source}</span>
|
|
89
|
+
{p.match_count !== undefined && <span>Matches: {p.match_count}</span>}
|
|
90
|
+
<span>{new Date(p.created_at).toLocaleDateString()}</span>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
{(activeTab === 'pending' || activeTab === 'shadow') && (
|
|
94
|
+
<div className="flex gap-2 shrink-0">
|
|
95
|
+
<button
|
|
96
|
+
onClick={() => handleAction(p.id, 'approve')}
|
|
97
|
+
className="text-xs px-3 py-1 rounded bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 hover:bg-green-200 dark:hover:bg-green-900/50"
|
|
98
|
+
>
|
|
99
|
+
Approve
|
|
100
|
+
</button>
|
|
101
|
+
<button
|
|
102
|
+
onClick={() => handleAction(p.id, 'reject')}
|
|
103
|
+
className="text-xs px-3 py-1 rounded bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 hover:bg-red-200 dark:hover:bg-red-900/50"
|
|
104
|
+
>
|
|
105
|
+
Reject
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
)}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { LoadingSkeleton } from '../../components/ui/LoadingSkeleton';
|
|
3
|
+
|
|
4
|
+
interface PatternStats {
|
|
5
|
+
byStatus: Record<string, number>;
|
|
6
|
+
total: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function SystemPatterns() {
|
|
10
|
+
const [stats, setStats] = useState<PatternStats | null>(null);
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
fetch('/api/patterns/stats')
|
|
15
|
+
.then((r) => r.json())
|
|
16
|
+
.then((data: PatternStats) => {
|
|
17
|
+
setStats(data);
|
|
18
|
+
setLoading(false);
|
|
19
|
+
})
|
|
20
|
+
.catch(() => setLoading(false));
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
if (loading) return <LoadingSkeleton lines={2} />;
|
|
24
|
+
if (!stats) return null;
|
|
25
|
+
|
|
26
|
+
const statusLabels: Record<string, { label: string; colour: string }> = {
|
|
27
|
+
approved: { label: 'Approved', colour: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400' },
|
|
28
|
+
pending: { label: 'Pending', colour: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400' },
|
|
29
|
+
shadow: { label: 'Shadow', colour: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' },
|
|
30
|
+
rejected: { label: 'Rejected', colour: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400' },
|
|
31
|
+
stale: { label: 'Stale', colour: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400' },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div>
|
|
36
|
+
<div className="flex items-center gap-2 mb-3">
|
|
37
|
+
<span className="text-sm font-medium text-gray-900 dark:text-white">Pattern Overview</span>
|
|
38
|
+
<span className="text-xs text-gray-500 dark:text-gray-400">({stats.total} total)</span>
|
|
39
|
+
</div>
|
|
40
|
+
<div className="flex flex-wrap gap-2">
|
|
41
|
+
{Object.entries(stats.byStatus).map(([status, count]) => {
|
|
42
|
+
const meta = statusLabels[status] ?? { label: status, colour: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400' };
|
|
43
|
+
return (
|
|
44
|
+
<span key={status} className={`text-xs px-2.5 py-1 rounded-full font-medium ${meta.colour}`}>
|
|
45
|
+
{meta.label}: {count}
|
|
46
|
+
</span>
|
|
47
|
+
);
|
|
48
|
+
})}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { LoadingSkeleton } from '../../components/ui/LoadingSkeleton';
|
|
3
|
+
import { EmptyState } from '../../components/ui/EmptyState';
|
|
4
|
+
|
|
5
|
+
interface UsageReport {
|
|
6
|
+
snapshot_date: string;
|
|
7
|
+
d1_reads: number;
|
|
8
|
+
d1_writes: number;
|
|
9
|
+
kv_reads: number;
|
|
10
|
+
kv_writes: number;
|
|
11
|
+
worker_requests: number;
|
|
12
|
+
total_cost_usd: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function GapDetectionReport() {
|
|
16
|
+
const [daily, setDaily] = useState<UsageReport[]>([]);
|
|
17
|
+
const [loading, setLoading] = useState(true);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
fetch('/api/reports/usage?days=14')
|
|
21
|
+
.then((r) => r.json())
|
|
22
|
+
.then((data: { daily: UsageReport[] }) => {
|
|
23
|
+
setDaily(data.daily ?? []);
|
|
24
|
+
setLoading(false);
|
|
25
|
+
})
|
|
26
|
+
.catch(() => setLoading(false));
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
if (loading) return <LoadingSkeleton lines={5} />;
|
|
30
|
+
if (daily.length === 0) return <EmptyState title="No usage data" description="Daily rollup data will appear after collection." />;
|
|
31
|
+
|
|
32
|
+
const gapDays = daily.filter((d) => d.d1_reads === 0 && d.d1_writes === 0 && d.worker_requests === 0);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="space-y-3">
|
|
36
|
+
<div className="flex items-center gap-3">
|
|
37
|
+
<span className="text-sm font-medium text-gray-900 dark:text-white">14-Day Coverage</span>
|
|
38
|
+
<span className={`text-xs px-2 py-0.5 rounded-full ${
|
|
39
|
+
gapDays.length === 0
|
|
40
|
+
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
|
41
|
+
: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
|
|
42
|
+
}`}>
|
|
43
|
+
{daily.length - gapDays.length}/{daily.length} days
|
|
44
|
+
</span>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div className="flex gap-0.5">
|
|
48
|
+
{daily.map((d) => {
|
|
49
|
+
const hasData = d.d1_reads > 0 || d.d1_writes > 0 || d.worker_requests > 0;
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
key={d.snapshot_date}
|
|
53
|
+
title={`${d.snapshot_date}: $${d.total_cost_usd.toFixed(2)}`}
|
|
54
|
+
className={`h-6 flex-1 rounded-sm ${
|
|
55
|
+
hasData ? 'bg-green-500 dark:bg-green-600' : 'bg-red-300 dark:bg-red-700'
|
|
56
|
+
}`}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
})}
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
{gapDays.length > 0 && (
|
|
63
|
+
<p className="text-xs text-yellow-600 dark:text-yellow-400">
|
|
64
|
+
{gapDays.length} day(s) with zero data: {gapDays.map((d) => d.snapshot_date).join(', ')}
|
|
65
|
+
</p>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { LoadingSkeleton } from '../../components/ui/LoadingSkeleton';
|
|
3
|
+
import { EmptyState } from '../../components/ui/EmptyState';
|
|
4
|
+
|
|
5
|
+
interface AuditReport {
|
|
6
|
+
id: number;
|
|
7
|
+
project: string;
|
|
8
|
+
scan_type: string;
|
|
9
|
+
ai_judge_score: number;
|
|
10
|
+
sdk_score: number;
|
|
11
|
+
observability_score: number;
|
|
12
|
+
cost_protection_score: number;
|
|
13
|
+
security_score: number;
|
|
14
|
+
scan_date: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function SdkAuditReport() {
|
|
18
|
+
const [reports, setReports] = useState<AuditReport[]>([]);
|
|
19
|
+
const [loading, setLoading] = useState(true);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
fetch('/api/reports/audit?limit=20')
|
|
23
|
+
.then((r) => r.json())
|
|
24
|
+
.then((data: { reports: AuditReport[] }) => {
|
|
25
|
+
setReports(data.reports ?? []);
|
|
26
|
+
setLoading(false);
|
|
27
|
+
})
|
|
28
|
+
.catch(() => setLoading(false));
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
if (loading) return <LoadingSkeleton lines={5} />;
|
|
32
|
+
if (reports.length === 0) return <EmptyState title="No audit reports" description="Run the platform auditor to generate reports." />;
|
|
33
|
+
|
|
34
|
+
const scoreColour = (score: number): string => {
|
|
35
|
+
if (score >= 90) return 'text-green-600 dark:text-green-400';
|
|
36
|
+
if (score >= 70) return 'text-yellow-600 dark:text-yellow-400';
|
|
37
|
+
return 'text-red-600 dark:text-red-400';
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="overflow-x-auto">
|
|
42
|
+
<table className="w-full text-sm">
|
|
43
|
+
<thead>
|
|
44
|
+
<tr className="border-b border-gray-200 dark:border-gray-700">
|
|
45
|
+
<th className="text-left py-2 px-2 font-medium text-gray-500 dark:text-gray-400">Project</th>
|
|
46
|
+
<th className="text-left py-2 px-2 font-medium text-gray-500 dark:text-gray-400">Type</th>
|
|
47
|
+
<th className="text-right py-2 px-2 font-medium text-gray-500 dark:text-gray-400">Overall</th>
|
|
48
|
+
<th className="text-right py-2 px-2 font-medium text-gray-500 dark:text-gray-400">SDK</th>
|
|
49
|
+
<th className="text-right py-2 px-2 font-medium text-gray-500 dark:text-gray-400">Obs</th>
|
|
50
|
+
<th className="text-right py-2 px-2 font-medium text-gray-500 dark:text-gray-400">Cost</th>
|
|
51
|
+
<th className="text-right py-2 px-2 font-medium text-gray-500 dark:text-gray-400">Sec</th>
|
|
52
|
+
<th className="text-right py-2 px-2 font-medium text-gray-500 dark:text-gray-400">Date</th>
|
|
53
|
+
</tr>
|
|
54
|
+
</thead>
|
|
55
|
+
<tbody>
|
|
56
|
+
{reports.map((r) => (
|
|
57
|
+
<tr key={r.id} className="border-b border-gray-100 dark:border-gray-800">
|
|
58
|
+
<td className="py-1.5 px-2 text-gray-900 dark:text-white">{r.project}</td>
|
|
59
|
+
<td className="py-1.5 px-2 text-gray-500 dark:text-gray-400">{r.scan_type}</td>
|
|
60
|
+
<td className={`py-1.5 px-2 text-right font-mono ${scoreColour(r.ai_judge_score)}`}>{r.ai_judge_score}</td>
|
|
61
|
+
<td className={`py-1.5 px-2 text-right font-mono ${scoreColour(r.sdk_score)}`}>{r.sdk_score}</td>
|
|
62
|
+
<td className={`py-1.5 px-2 text-right font-mono ${scoreColour(r.observability_score)}`}>{r.observability_score}</td>
|
|
63
|
+
<td className={`py-1.5 px-2 text-right font-mono ${scoreColour(r.cost_protection_score)}`}>{r.cost_protection_score}</td>
|
|
64
|
+
<td className={`py-1.5 px-2 text-right font-mono ${scoreColour(r.security_score)}`}>{r.security_score}</td>
|
|
65
|
+
<td className="py-1.5 px-2 text-right text-gray-400 dark:text-gray-500">{new Date(r.scan_date).toLocaleDateString()}</td>
|
|
66
|
+
</tr>
|
|
67
|
+
))}
|
|
68
|
+
</tbody>
|
|
69
|
+
</table>
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
export const POST: APIRoute = async ({ params, locals }) => {
|
|
4
|
+
const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
|
|
5
|
+
|
|
6
|
+
if (!db) {
|
|
7
|
+
return new Response(JSON.stringify({ error: 'Database not available' }), {
|
|
8
|
+
status: 503,
|
|
9
|
+
headers: { 'Content-Type': 'application/json' },
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const id = params.id;
|
|
14
|
+
if (!id) {
|
|
15
|
+
return new Response(JSON.stringify({ error: 'Missing notification id' }), {
|
|
16
|
+
status: 400,
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await db
|
|
23
|
+
.prepare(`UPDATE notifications SET read_at = unixepoch() WHERE id = ? AND read_at IS NULL`)
|
|
24
|
+
.bind(id)
|
|
25
|
+
.run();
|
|
26
|
+
|
|
27
|
+
return new Response(
|
|
28
|
+
JSON.stringify({ ok: true }),
|
|
29
|
+
{ headers: { 'Content-Type': 'application/json' } }
|
|
30
|
+
);
|
|
31
|
+
} catch {
|
|
32
|
+
return new Response(JSON.stringify({ error: 'Failed to mark as read' }), {
|
|
33
|
+
status: 500,
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { APIRoute } from 'astro';
|
|
2
|
+
|
|
3
|
+
export const POST: APIRoute = async ({ locals }) => {
|
|
4
|
+
const db = (locals.runtime?.env as { PLATFORM_DB?: D1Database } | undefined)?.PLATFORM_DB;
|
|
5
|
+
|
|
6
|
+
if (!db) {
|
|
7
|
+
return new Response(JSON.stringify({ error: 'Database not available' }), {
|
|
8
|
+
status: 503,
|
|
9
|
+
headers: { 'Content-Type': 'application/json' },
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const result = await db
|
|
15
|
+
.prepare(`UPDATE notifications SET read_at = unixepoch() WHERE read_at IS NULL`)
|
|
16
|
+
.run();
|
|
17
|
+
|
|
18
|
+
return new Response(
|
|
19
|
+
JSON.stringify({ ok: true, updated: result.meta?.changes ?? 0 }),
|
|
20
|
+
{ headers: { 'Content-Type': 'application/json' } }
|
|
21
|
+
);
|
|
22
|
+
} catch {
|
|
23
|
+
return new Response(JSON.stringify({ error: 'Failed to mark all as read' }), {
|
|
24
|
+
status: 500,
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
};
|