@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
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 = "1.
|
|
9
|
+
export declare const SDK_VERSION = "1.5.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 = '1.
|
|
8
|
+
export const SDK_VERSION = '1.5.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`. */
|
|
@@ -22,6 +22,16 @@ const SHARED_FILES = [
|
|
|
22
22
|
{ src: 'shared/config/budgets.yaml.hbs', dest: 'platform/config/budgets.yaml', template: true },
|
|
23
23
|
// Scripts
|
|
24
24
|
{ src: 'shared/scripts/sync-config.ts', dest: 'scripts/sync-config.ts', template: false },
|
|
25
|
+
{ src: 'shared/scripts/validate-schemas.js', dest: 'scripts/validate-schemas.js', template: false },
|
|
26
|
+
// Operational scripts
|
|
27
|
+
{ src: 'shared/scripts/ops/backfill-cloudflare-hourly.ts', dest: 'scripts/ops/backfill-cloudflare-hourly.ts', template: false },
|
|
28
|
+
{ src: 'shared/scripts/ops/reset-budget-state.ts', dest: 'scripts/ops/reset-budget-state.ts', template: false },
|
|
29
|
+
{ src: 'shared/scripts/ops/verify-account-completeness.ts', dest: 'scripts/ops/verify-account-completeness.ts', template: false },
|
|
30
|
+
{ src: 'shared/scripts/ops/validate-pipeline.ts', dest: 'scripts/ops/validate-pipeline.ts', template: false },
|
|
31
|
+
// Contracts — JSON schemas + TypeScript types
|
|
32
|
+
{ src: 'shared/contracts/schemas/envelope.v1.schema.json', dest: 'contracts/schemas/envelope.v1.schema.json', template: false },
|
|
33
|
+
{ src: 'shared/contracts/schemas/error_report.v1.schema.json', dest: 'contracts/schemas/error_report.v1.schema.json', template: false },
|
|
34
|
+
{ src: 'shared/contracts/types/telemetry-envelope.types.ts', dest: 'contracts/types/telemetry-envelope.types.ts', template: false },
|
|
25
35
|
// Migrations (minimal tier)
|
|
26
36
|
{ src: 'shared/migrations/001_core_tables.sql', dest: 'storage/d1/migrations/001_core_tables.sql', template: false },
|
|
27
37
|
{ src: 'shared/migrations/002_usage_warehouse.sql', dest: 'storage/d1/migrations/002_usage_warehouse.sql', template: false },
|
|
@@ -79,11 +89,64 @@ const SHARED_FILES = [
|
|
|
79
89
|
{ src: 'shared/workers/lib/usage/scheduled/anomaly-detection.ts', dest: 'workers/lib/usage/scheduled/anomaly-detection.ts', template: false },
|
|
80
90
|
{ src: 'shared/workers/lib/usage/scheduled/rollups.ts', dest: 'workers/lib/usage/scheduled/rollups.ts', template: false },
|
|
81
91
|
{ src: 'shared/workers/lib/usage/scheduled/error-digest.ts', dest: 'workers/lib/usage/scheduled/error-digest.ts', template: false },
|
|
82
|
-
// Workers — lib/usage/collectors (
|
|
92
|
+
// Workers — lib/usage/collectors (framework + 10 provider collectors + example)
|
|
83
93
|
{ src: 'shared/workers/lib/usage/collectors/index.ts', dest: 'workers/lib/usage/collectors/index.ts', template: false },
|
|
84
94
|
{ src: 'shared/workers/lib/usage/collectors/example.ts', dest: 'workers/lib/usage/collectors/example.ts', template: false },
|
|
95
|
+
{ src: 'shared/workers/lib/usage/collectors/openai.ts', dest: 'workers/lib/usage/collectors/openai.ts', template: false },
|
|
96
|
+
{ src: 'shared/workers/lib/usage/collectors/anthropic.ts', dest: 'workers/lib/usage/collectors/anthropic.ts', template: false },
|
|
97
|
+
{ src: 'shared/workers/lib/usage/collectors/github.ts', dest: 'workers/lib/usage/collectors/github.ts', template: false },
|
|
98
|
+
{ src: 'shared/workers/lib/usage/collectors/stripe.ts', dest: 'workers/lib/usage/collectors/stripe.ts', template: false },
|
|
99
|
+
{ src: 'shared/workers/lib/usage/collectors/apify.ts', dest: 'workers/lib/usage/collectors/apify.ts', template: false },
|
|
100
|
+
{ src: 'shared/workers/lib/usage/collectors/resend.ts', dest: 'workers/lib/usage/collectors/resend.ts', template: false },
|
|
101
|
+
{ src: 'shared/workers/lib/usage/collectors/deepseek.ts', dest: 'workers/lib/usage/collectors/deepseek.ts', template: false },
|
|
102
|
+
{ src: 'shared/workers/lib/usage/collectors/minimax.ts', dest: 'workers/lib/usage/collectors/minimax.ts', template: false },
|
|
103
|
+
{ src: 'shared/workers/lib/usage/collectors/gemini.ts', dest: 'workers/lib/usage/collectors/gemini.ts', template: false },
|
|
104
|
+
{ src: 'shared/workers/lib/usage/collectors/custom-http.ts', dest: 'workers/lib/usage/collectors/custom-http.ts', template: false },
|
|
85
105
|
// Documentation
|
|
86
106
|
{ src: 'shared/docs/kv-key-patterns.md', dest: 'docs/kv-key-patterns.md', template: false },
|
|
107
|
+
// Observability config
|
|
108
|
+
{ src: 'shared/config/observability.yaml.hbs', dest: 'platform/config/observability.yaml', template: true },
|
|
109
|
+
// GraphQL dataset discovery script
|
|
110
|
+
{ src: 'shared/scripts/ops/discover-graphql-datasets.ts', dest: 'scripts/ops/discover-graphql-datasets.ts', template: false },
|
|
111
|
+
// CI workflow (caller for reusable consumer-check)
|
|
112
|
+
{ src: 'shared/.github/workflows/platform-check.yml.hbs', dest: '.github/workflows/platform-check.yml', template: true },
|
|
113
|
+
// Dashboard — shared tier (Astro SSR + Cloudflare Pages)
|
|
114
|
+
{ src: 'shared/dashboard/package.json.hbs', dest: 'dashboard/package.json', template: true },
|
|
115
|
+
{ src: 'shared/dashboard/astro.config.mjs', dest: 'dashboard/astro.config.mjs', template: false },
|
|
116
|
+
{ src: 'shared/dashboard/tailwind.config.mjs', dest: 'dashboard/tailwind.config.mjs', template: false },
|
|
117
|
+
{ src: 'shared/dashboard/tsconfig.json', dest: 'dashboard/tsconfig.json', template: false },
|
|
118
|
+
{ src: 'shared/dashboard/wrangler.json.hbs', dest: 'dashboard/wrangler.json', template: true },
|
|
119
|
+
{ src: 'shared/dashboard/src/env.d.ts.hbs', dest: 'dashboard/src/env.d.ts', template: true },
|
|
120
|
+
{ src: 'shared/dashboard/src/styles/global.css', dest: 'dashboard/src/styles/global.css', template: false },
|
|
121
|
+
{ src: 'shared/dashboard/src/middleware/auth.ts', dest: 'dashboard/src/middleware/auth.ts', template: false },
|
|
122
|
+
{ src: 'shared/dashboard/src/middleware/index.ts', dest: 'dashboard/src/middleware/index.ts', template: false },
|
|
123
|
+
{ src: 'shared/dashboard/src/layouts/DashboardLayout.astro', dest: 'dashboard/src/layouts/DashboardLayout.astro', template: false },
|
|
124
|
+
{ src: 'shared/dashboard/src/components/Nav.astro.hbs', dest: 'dashboard/src/components/Nav.astro', template: true },
|
|
125
|
+
{ src: 'shared/dashboard/src/components/Header.astro', dest: 'dashboard/src/components/Header.astro', template: false },
|
|
126
|
+
{ src: 'shared/dashboard/src/pages/index.astro', dest: 'dashboard/src/pages/index.astro', template: false },
|
|
127
|
+
{ src: 'shared/dashboard/src/pages/dashboard.astro', dest: 'dashboard/src/pages/dashboard.astro', template: false },
|
|
128
|
+
{ src: 'shared/dashboard/src/pages/resources.astro', dest: 'dashboard/src/pages/resources.astro', template: false },
|
|
129
|
+
{ src: 'shared/dashboard/src/pages/settings/index.astro', dest: 'dashboard/src/pages/settings/index.astro', template: false },
|
|
130
|
+
{ src: 'shared/dashboard/src/pages/api/overview/summary.ts', dest: 'dashboard/src/pages/api/overview/summary.ts', template: false },
|
|
131
|
+
{ src: 'shared/dashboard/src/pages/api/usage/circuit-breakers.ts', dest: 'dashboard/src/pages/api/usage/circuit-breakers.ts', template: false },
|
|
132
|
+
{ src: 'shared/dashboard/src/pages/api/usage/status.ts', dest: 'dashboard/src/pages/api/usage/status.ts', template: false },
|
|
133
|
+
{ src: 'shared/dashboard/src/components/overview/MissionControl.tsx', dest: 'dashboard/src/components/overview/MissionControl.tsx', template: false },
|
|
134
|
+
{ src: 'shared/dashboard/src/components/overview/HealthQuadrant.tsx', dest: 'dashboard/src/components/overview/HealthQuadrant.tsx', template: false },
|
|
135
|
+
{ src: 'shared/dashboard/src/components/overview/ErrorsQuadrant.tsx', dest: 'dashboard/src/components/overview/ErrorsQuadrant.tsx', template: false },
|
|
136
|
+
{ src: 'shared/dashboard/src/components/overview/CostQuadrant.tsx', dest: 'dashboard/src/components/overview/CostQuadrant.tsx', template: false },
|
|
137
|
+
{ src: 'shared/dashboard/src/components/overview/ActivityFeed.tsx', dest: 'dashboard/src/components/overview/ActivityFeed.tsx', template: false },
|
|
138
|
+
{ src: 'shared/dashboard/src/components/ui/AlertBanner.tsx', dest: 'dashboard/src/components/ui/AlertBanner.tsx', template: false },
|
|
139
|
+
{ src: 'shared/dashboard/src/components/ui/Sparkline.tsx', dest: 'dashboard/src/components/ui/Sparkline.tsx', template: false },
|
|
140
|
+
{ src: 'shared/dashboard/src/components/ui/StatusDot.tsx', dest: 'dashboard/src/components/ui/StatusDot.tsx', template: false },
|
|
141
|
+
{ src: 'shared/dashboard/src/components/ui/index.ts', dest: 'dashboard/src/components/ui/index.ts', template: false },
|
|
142
|
+
{ src: 'shared/dashboard/src/components/resources/ResourceTabs.tsx', dest: 'dashboard/src/components/resources/ResourceTabs.tsx', template: false },
|
|
143
|
+
{ src: 'shared/dashboard/src/components/resources/CostCentreOverview.tsx', dest: 'dashboard/src/components/resources/CostCentreOverview.tsx', template: false },
|
|
144
|
+
{ src: 'shared/dashboard/src/components/resources/AllowanceStatus.tsx', dest: 'dashboard/src/components/resources/AllowanceStatus.tsx', template: false },
|
|
145
|
+
{ src: 'shared/dashboard/src/components/resources/index.ts', dest: 'dashboard/src/components/resources/index.ts', template: false },
|
|
146
|
+
{ src: 'shared/dashboard/src/components/settings/SettingsCard.tsx', dest: 'dashboard/src/components/settings/SettingsCard.tsx', template: false },
|
|
147
|
+
{ src: 'shared/dashboard/src/components/settings/index.ts', dest: 'dashboard/src/components/settings/index.ts', template: false },
|
|
148
|
+
{ src: 'shared/dashboard/src/lib/types.ts', dest: 'dashboard/src/lib/types.ts', template: false },
|
|
149
|
+
{ src: 'shared/dashboard/src/lib/fetch.ts', dest: 'dashboard/src/lib/fetch.ts', template: false },
|
|
87
150
|
];
|
|
88
151
|
const STANDARD_FILES = [
|
|
89
152
|
// Additional migrations
|
|
@@ -105,6 +168,28 @@ const STANDARD_FILES = [
|
|
|
105
168
|
{ src: 'standard/workers/lib/sentinel/gap-detection.ts', dest: 'workers/lib/sentinel/gap-detection.ts', template: false },
|
|
106
169
|
// Workers — shared lib (used by standard-tier workers)
|
|
107
170
|
{ src: 'standard/workers/lib/shared/slack-alerts.ts', dest: 'workers/lib/shared/slack-alerts.ts', template: false },
|
|
171
|
+
// Topology mapper (infrastructure discovery + attribution)
|
|
172
|
+
{ src: 'standard/migrations/009_topology_mapper.sql', dest: 'storage/d1/migrations/009_topology_mapper.sql', template: false },
|
|
173
|
+
{ src: 'standard/wrangler.mapper.jsonc.hbs', dest: 'wrangler.{{projectSlug}}-mapper.jsonc', template: true },
|
|
174
|
+
{ src: 'standard/workers/platform-mapper.ts', dest: 'workers/platform-mapper.ts', template: false },
|
|
175
|
+
{ src: 'standard/workers/lib/mapper/index.ts', dest: 'workers/lib/mapper/index.ts', template: false },
|
|
176
|
+
{ src: 'standard/workers/lib/mapper/attribution-check.ts', dest: 'workers/lib/mapper/attribution-check.ts', template: false },
|
|
177
|
+
// SDK test client (telemetry + circuit breaker validation)
|
|
178
|
+
{ src: 'standard/wrangler.sdk-test-client.jsonc.hbs', dest: 'wrangler.{{projectSlug}}-sdk-test-client.jsonc', template: true },
|
|
179
|
+
{ src: 'standard/workers/platform-sdk-test-client.ts', dest: 'workers/platform-sdk-test-client.ts', template: false },
|
|
180
|
+
// Dashboard — standard tier (error management, health, DLQ)
|
|
181
|
+
{ src: 'standard/dashboard/src/pages/health.astro', dest: 'dashboard/src/pages/health.astro', template: false },
|
|
182
|
+
{ src: 'standard/dashboard/src/pages/errors.astro', dest: 'dashboard/src/pages/errors.astro', template: false },
|
|
183
|
+
{ src: 'standard/dashboard/src/pages/api/errors/index.ts', dest: 'dashboard/src/pages/api/errors/index.ts', template: false },
|
|
184
|
+
{ src: 'standard/dashboard/src/pages/api/errors/stats.ts', dest: 'dashboard/src/pages/api/errors/stats.ts', template: false },
|
|
185
|
+
{ src: 'standard/dashboard/src/pages/api/health/dlq.ts', dest: 'dashboard/src/pages/api/health/dlq.ts', template: false },
|
|
186
|
+
{ src: 'standard/dashboard/src/components/health/HealthTabs.tsx', dest: 'dashboard/src/components/health/HealthTabs.tsx', template: false },
|
|
187
|
+
{ src: 'standard/dashboard/src/components/health/DlqStatusCard.tsx', dest: 'dashboard/src/components/health/DlqStatusCard.tsx', template: false },
|
|
188
|
+
{ src: 'standard/dashboard/src/components/health/index.ts', dest: 'dashboard/src/components/health/index.ts', template: false },
|
|
189
|
+
{ src: 'standard/dashboard/src/components/errors/ErrorsTable.tsx', dest: 'dashboard/src/components/errors/ErrorsTable.tsx', template: false },
|
|
190
|
+
{ src: 'standard/dashboard/src/components/errors/ErrorStats.tsx', dest: 'dashboard/src/components/errors/ErrorStats.tsx', template: false },
|
|
191
|
+
{ src: 'standard/dashboard/src/components/errors/index.ts', dest: 'dashboard/src/components/errors/index.ts', template: false },
|
|
192
|
+
{ src: 'standard/dashboard/src/lib/errors.ts', dest: 'dashboard/src/lib/errors.ts', template: false },
|
|
108
193
|
];
|
|
109
194
|
const FULL_FILES = [
|
|
110
195
|
// Additional migrations
|
|
@@ -133,6 +218,40 @@ const FULL_FILES = [
|
|
|
133
218
|
{ src: 'full/workers/platform-search.ts', dest: 'workers/platform-search.ts', template: false },
|
|
134
219
|
// Workers — platform-settings (settings management API)
|
|
135
220
|
{ src: 'full/workers/platform-settings.ts', dest: 'workers/platform-settings.ts', template: false },
|
|
221
|
+
// Additional migrations — auditor
|
|
222
|
+
{ src: 'full/migrations/008_auditor.sql', dest: 'storage/d1/migrations/008_auditor.sql', template: false },
|
|
223
|
+
// Auditor config
|
|
224
|
+
{ src: 'full/config/audit-targets.yaml', dest: 'config/audit-targets.yaml', template: false },
|
|
225
|
+
// Wrangler config — auditor
|
|
226
|
+
{ src: 'full/wrangler.auditor.jsonc.hbs', dest: 'wrangler.{{projectSlug}}-auditor.jsonc', template: true },
|
|
227
|
+
// Workers — platform-auditor (SDK integration auditor + AI Judge)
|
|
228
|
+
{ src: 'full/workers/platform-auditor.ts', dest: 'workers/platform-auditor.ts', template: false },
|
|
229
|
+
{ src: 'full/workers/lib/ai-judge-schema.ts', dest: 'workers/lib/ai-judge-schema.ts', template: false },
|
|
230
|
+
{ src: 'full/workers/lib/auditor/index.ts', dest: 'workers/lib/auditor/index.ts', template: false },
|
|
231
|
+
{ src: 'full/workers/lib/auditor/types.ts', dest: 'workers/lib/auditor/types.ts', template: false },
|
|
232
|
+
{ src: 'full/workers/lib/auditor/feature-coverage.ts', dest: 'workers/lib/auditor/feature-coverage.ts', template: false },
|
|
233
|
+
{ src: 'full/workers/lib/auditor/comprehensive-report.ts', dest: 'workers/lib/auditor/comprehensive-report.ts', template: false },
|
|
234
|
+
// Pricing version tracking
|
|
235
|
+
{ src: 'full/migrations/010_pricing_versions.sql', dest: 'storage/d1/migrations/010_pricing_versions.sql', template: false },
|
|
236
|
+
// Multi-account Cloudflare support
|
|
237
|
+
{ src: 'full/migrations/011_multi_account.sql', dest: 'storage/d1/migrations/011_multi_account.sql', template: false },
|
|
238
|
+
// KV pricing configuration script
|
|
239
|
+
{ src: 'full/scripts/ops/set-kv-pricing.ts', dest: 'scripts/ops/set-kv-pricing.ts', template: false },
|
|
240
|
+
// Dashboard — full tier (patterns, notifications, search)
|
|
241
|
+
{ src: 'full/dashboard/src/pages/notifications.astro', dest: 'dashboard/src/pages/notifications.astro', template: false },
|
|
242
|
+
{ src: 'full/dashboard/src/pages/api/patterns/index.ts', dest: 'dashboard/src/pages/api/patterns/index.ts', template: false },
|
|
243
|
+
{ src: 'full/dashboard/src/pages/api/patterns/approve.ts', dest: 'dashboard/src/pages/api/patterns/approve.ts', template: false },
|
|
244
|
+
{ src: 'full/dashboard/src/pages/api/patterns/reject.ts', dest: 'dashboard/src/pages/api/patterns/reject.ts', template: false },
|
|
245
|
+
{ src: 'full/dashboard/src/pages/api/notifications/index.ts', dest: 'dashboard/src/pages/api/notifications/index.ts', template: false },
|
|
246
|
+
{ src: 'full/dashboard/src/pages/api/notifications/unread-count.ts', dest: 'dashboard/src/pages/api/notifications/unread-count.ts', template: false },
|
|
247
|
+
{ src: 'full/dashboard/src/pages/api/search/index.ts', dest: 'dashboard/src/pages/api/search/index.ts', template: false },
|
|
248
|
+
{ src: 'full/dashboard/src/components/patterns/SuggestionsQueue.tsx', dest: 'dashboard/src/components/patterns/SuggestionsQueue.tsx', template: false },
|
|
249
|
+
{ src: 'full/dashboard/src/components/patterns/PatternStats.tsx', dest: 'dashboard/src/components/patterns/PatternStats.tsx', template: false },
|
|
250
|
+
{ src: 'full/dashboard/src/components/patterns/index.ts', dest: 'dashboard/src/components/patterns/index.ts', template: false },
|
|
251
|
+
{ src: 'full/dashboard/src/components/notifications/NotificationBell.tsx', dest: 'dashboard/src/components/notifications/NotificationBell.tsx', template: false },
|
|
252
|
+
{ src: 'full/dashboard/src/components/notifications/NotificationList.tsx', dest: 'dashboard/src/components/notifications/NotificationList.tsx', template: false },
|
|
253
|
+
{ src: 'full/dashboard/src/components/notifications/index.ts', dest: 'dashboard/src/components/notifications/index.ts', template: false },
|
|
254
|
+
{ src: 'full/dashboard/src/components/search/SearchModal.tsx', dest: 'dashboard/src/components/search/SearchModal.tsx', template: false },
|
|
136
255
|
];
|
|
137
256
|
export function getFilesForTier(tier) {
|
|
138
257
|
const files = [...SHARED_FILES];
|
package/package.json
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# Audit Targets Configuration
|
|
3
|
+
# =============================================================================
|
|
4
|
+
# Define the projects that the platform-auditor worker should audit.
|
|
5
|
+
# Sync this to KV via: npm run sync:config
|
|
6
|
+
#
|
|
7
|
+
# Each target specifies:
|
|
8
|
+
# repo — GitHub org/repo path (for fetching source files)
|
|
9
|
+
# wranglerPath — Path to main wrangler config (for config checks)
|
|
10
|
+
# entryPoint — Primary SDK integration file (for code smell detection)
|
|
11
|
+
# expectedStatus — Expected audit result: HEALTHY, ZOMBIE, UNTRACKED, BROKEN
|
|
12
|
+
# notes — Human-readable description
|
|
13
|
+
# deepScanFiles — Files per audit dimension (for AI Judge analysis)
|
|
14
|
+
#
|
|
15
|
+
# Status definitions:
|
|
16
|
+
# HEALTHY — SDK integrated, heartbeats fresh, config correct
|
|
17
|
+
# ZOMBIE — Was integrated but dormant (no recent heartbeats)
|
|
18
|
+
# UNTRACKED — Running in production but not using SDK
|
|
19
|
+
# BROKEN — SDK integration present but misconfigured
|
|
20
|
+
# NOT_INTEGRATED — No SDK integration detected
|
|
21
|
+
# =============================================================================
|
|
22
|
+
|
|
23
|
+
targets: {}
|
|
24
|
+
# Example target (uncomment and customise):
|
|
25
|
+
#
|
|
26
|
+
# my-project:
|
|
27
|
+
# repo: my-org/my-project
|
|
28
|
+
# wranglerPath: wrangler.jsonc
|
|
29
|
+
# entryPoint: src/index.ts
|
|
30
|
+
# entryPointFallback: src/worker.ts
|
|
31
|
+
# expectedStatus: HEALTHY
|
|
32
|
+
# notes: Main application worker
|
|
33
|
+
# deepScanFiles:
|
|
34
|
+
# sdk:
|
|
35
|
+
# - src/index.ts
|
|
36
|
+
# - src/middleware/platform-sdk.ts
|
|
37
|
+
# - src/lib/features.ts
|
|
38
|
+
# observability:
|
|
39
|
+
# - src/lib/logger.ts
|
|
40
|
+
# - src/middleware/tracing.ts
|
|
41
|
+
# cost:
|
|
42
|
+
# - src/lib/budgets.ts
|
|
43
|
+
# - src/middleware/circuit-breaker.ts
|
|
44
|
+
# security:
|
|
45
|
+
# - src/middleware/auth.ts
|
|
46
|
+
# - src/lib/validation.ts
|
|
47
|
+
|
|
48
|
+
# SDK patterns to detect in source code (for code smell checks)
|
|
49
|
+
sdkPatterns:
|
|
50
|
+
- withFeatureBudget
|
|
51
|
+
- withCronBudget
|
|
52
|
+
- withQueueBudget
|
|
53
|
+
- trackedEnv
|
|
54
|
+
- platform-sdk
|
|
55
|
+
- completeTracking
|
|
56
|
+
- CircuitBreakerError
|
|
57
|
+
- PLATFORM_CACHE
|
|
58
|
+
- PLATFORM_TELEMETRY
|
|
59
|
+
|
|
60
|
+
# Rubric weights for composite score calculation (must sum to 1.0)
|
|
61
|
+
rubricWeights:
|
|
62
|
+
sdk: 0.3
|
|
63
|
+
observability: 0.25
|
|
64
|
+
costProtection: 0.25
|
|
65
|
+
security: 0.2
|
|
66
|
+
|
|
67
|
+
# Thresholds
|
|
68
|
+
heartbeatFreshnessHours: 24
|
|
69
|
+
focusedScanThreshold: 90
|
|
70
|
+
aiCacheTtlSeconds: 259200 # 3 days
|
|
71
|
+
behavioralAnalysisDays: 30
|
|
72
|
+
hotspotMinCommits: 3
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
export function NotificationBell() {
|
|
4
|
+
const [count, setCount] = useState(0);
|
|
5
|
+
const intervalRef = useRef<ReturnType<typeof setInterval>>();
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
function fetchCount() {
|
|
9
|
+
fetch('/api/notifications/unread-count')
|
|
10
|
+
.then(res => res.json())
|
|
11
|
+
.then((data: { count: number }) => setCount(data.count ?? 0))
|
|
12
|
+
.catch(() => {});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fetchCount();
|
|
16
|
+
intervalRef.current = setInterval(fetchCount, 30000);
|
|
17
|
+
return () => clearInterval(intervalRef.current);
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<a href="/notifications" className="relative p-2 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700">
|
|
22
|
+
<span className="text-lg">🔔</span>
|
|
23
|
+
{count > 0 && (
|
|
24
|
+
<span className="absolute -top-1 -right-1 inline-flex items-center justify-center w-5 h-5 text-xs font-bold text-white bg-red-500 rounded-full">
|
|
25
|
+
{count > 99 ? '99+' : count}
|
|
26
|
+
</span>
|
|
27
|
+
)}
|
|
28
|
+
</a>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Notification {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
body: string | null;
|
|
7
|
+
category: string;
|
|
8
|
+
priority: string;
|
|
9
|
+
source: string;
|
|
10
|
+
created_at: number;
|
|
11
|
+
read_at: number | null;
|
|
12
|
+
action_url: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function NotificationList() {
|
|
16
|
+
const [notifications, setNotifications] = useState<Notification[]>([]);
|
|
17
|
+
const [total, setTotal] = useState(0);
|
|
18
|
+
const [page, setPage] = useState(1);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
fetch(`/api/notifications?page=${page}&limit=20`)
|
|
22
|
+
.then(res => res.json())
|
|
23
|
+
.then((data: { notifications: Notification[]; total: number }) => {
|
|
24
|
+
setNotifications(data.notifications ?? []);
|
|
25
|
+
setTotal(data.total ?? 0);
|
|
26
|
+
})
|
|
27
|
+
.catch(() => {});
|
|
28
|
+
}, [page]);
|
|
29
|
+
|
|
30
|
+
function formatTime(ts: number): string {
|
|
31
|
+
const now = Date.now();
|
|
32
|
+
const diffMs = now - ts * 1000;
|
|
33
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
34
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
35
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
36
|
+
|
|
37
|
+
if (diffMins < 1) return 'Just now';
|
|
38
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
39
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
40
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
41
|
+
return new Date(ts * 1000).toLocaleDateString('en-AU', { day: 'numeric', month: 'short' });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const categoryIcons: Record<string, string> = {
|
|
45
|
+
error: '[ERR]',
|
|
46
|
+
warning: '[WARN]',
|
|
47
|
+
info: '[INFO]',
|
|
48
|
+
success: '[OK]',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="space-y-3">
|
|
53
|
+
{notifications.length === 0 ? (
|
|
54
|
+
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
55
|
+
No notifications.
|
|
56
|
+
</div>
|
|
57
|
+
) : (
|
|
58
|
+
notifications.map(n => (
|
|
59
|
+
<div
|
|
60
|
+
key={n.id}
|
|
61
|
+
className={`bg-white dark:bg-gray-800 rounded-lg border p-4 ${
|
|
62
|
+
n.read_at ? 'border-gray-200 dark:border-gray-700' : 'border-blue-300 dark:border-blue-700'
|
|
63
|
+
}`}
|
|
64
|
+
>
|
|
65
|
+
<div className="flex items-start gap-3">
|
|
66
|
+
<span className="text-xs font-mono text-gray-500 dark:text-gray-400 mt-0.5">
|
|
67
|
+
{categoryIcons[n.category] ?? '[INFO]'}
|
|
68
|
+
</span>
|
|
69
|
+
<div className="flex-1 min-w-0">
|
|
70
|
+
<div className="flex items-center justify-between gap-2">
|
|
71
|
+
<h4 className="text-sm font-medium text-gray-900 dark:text-white truncate">
|
|
72
|
+
{n.action_url ? (
|
|
73
|
+
<a href={n.action_url} className="hover:text-blue-600 dark:hover:text-blue-400">{n.title}</a>
|
|
74
|
+
) : n.title}
|
|
75
|
+
</h4>
|
|
76
|
+
<span className="text-xs text-gray-400 dark:text-gray-500 flex-shrink-0">
|
|
77
|
+
{formatTime(n.created_at)}
|
|
78
|
+
</span>
|
|
79
|
+
</div>
|
|
80
|
+
{n.body && (
|
|
81
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1 line-clamp-2">{n.body}</p>
|
|
82
|
+
)}
|
|
83
|
+
<div className="flex items-center gap-2 mt-1.5">
|
|
84
|
+
<span className="text-xs text-gray-400 dark:text-gray-500">{n.source}</span>
|
|
85
|
+
<span className="text-xs text-gray-400 dark:text-gray-500">{n.priority}</span>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
))
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{total > 20 && (
|
|
94
|
+
<div className="flex items-center justify-between pt-2">
|
|
95
|
+
<button
|
|
96
|
+
onClick={() => setPage(p => Math.max(1, p - 1))}
|
|
97
|
+
disabled={page === 1}
|
|
98
|
+
className="text-sm text-blue-600 dark:text-blue-400 disabled:opacity-50"
|
|
99
|
+
>
|
|
100
|
+
Previous
|
|
101
|
+
</button>
|
|
102
|
+
<span className="text-sm text-gray-500 dark:text-gray-400">
|
|
103
|
+
Page {page} of {Math.ceil(total / 20)}
|
|
104
|
+
</span>
|
|
105
|
+
<button
|
|
106
|
+
onClick={() => setPage(p => p + 1)}
|
|
107
|
+
disabled={page >= Math.ceil(total / 20)}
|
|
108
|
+
className="text-sm text-blue-600 dark:text-blue-400 disabled:opacity-50"
|
|
109
|
+
>
|
|
110
|
+
Next
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Stats {
|
|
4
|
+
pending: number;
|
|
5
|
+
shadow: number;
|
|
6
|
+
approved: number;
|
|
7
|
+
rejected: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function PatternStats() {
|
|
11
|
+
const [stats, setStats] = useState<Stats | null>(null);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
Promise.all([
|
|
15
|
+
fetch('/api/patterns?status=pending').then(r => r.json()),
|
|
16
|
+
fetch('/api/patterns?status=shadow').then(r => r.json()),
|
|
17
|
+
fetch('/api/patterns?status=approved').then(r => r.json()),
|
|
18
|
+
])
|
|
19
|
+
.then(([pending, shadow, approved]) => {
|
|
20
|
+
setStats({
|
|
21
|
+
pending: (pending as { suggestions: unknown[] }).suggestions?.length ?? 0,
|
|
22
|
+
shadow: (shadow as { suggestions: unknown[] }).suggestions?.length ?? 0,
|
|
23
|
+
approved: (approved as { suggestions: unknown[] }).suggestions?.length ?? 0,
|
|
24
|
+
rejected: 0,
|
|
25
|
+
});
|
|
26
|
+
})
|
|
27
|
+
.catch(() => {});
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
if (!stats) {
|
|
31
|
+
return (
|
|
32
|
+
<div className="grid grid-cols-4 gap-4">
|
|
33
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
34
|
+
<div key={i} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 animate-pulse">
|
|
35
|
+
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-12 mb-2" />
|
|
36
|
+
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-8" />
|
|
37
|
+
</div>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const items = [
|
|
44
|
+
{ label: 'Pending', value: stats.pending, colour: 'text-yellow-600 dark:text-yellow-400' },
|
|
45
|
+
{ label: 'Shadow', value: stats.shadow, colour: 'text-purple-600 dark:text-purple-400' },
|
|
46
|
+
{ label: 'Approved', value: stats.approved, colour: 'text-green-600 dark:text-green-400' },
|
|
47
|
+
{ label: 'Rejected', value: stats.rejected, colour: 'text-red-600 dark:text-red-400' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="grid grid-cols-4 gap-4">
|
|
52
|
+
{items.map(item => (
|
|
53
|
+
<div key={item.label} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
54
|
+
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">{item.label}</div>
|
|
55
|
+
<div className={`text-2xl font-bold mt-1 ${item.colour}`}>{item.value}</div>
|
|
56
|
+
</div>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
interface PatternSuggestion {
|
|
4
|
+
id: number;
|
|
5
|
+
pattern_type: string;
|
|
6
|
+
pattern_value: string;
|
|
7
|
+
error_type: string;
|
|
8
|
+
priority: string;
|
|
9
|
+
status: string;
|
|
10
|
+
match_count: number;
|
|
11
|
+
source: string;
|
|
12
|
+
created_at: string;
|
|
13
|
+
reviewed_at: string | null;
|
|
14
|
+
reviewer_notes: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function SuggestionsQueue() {
|
|
18
|
+
const [suggestions, setSuggestions] = useState<PatternSuggestion[]>([]);
|
|
19
|
+
const [filter, setFilter] = useState<string>('pending');
|
|
20
|
+
const [actioningId, setActioningId] = useState<number | null>(null);
|
|
21
|
+
|
|
22
|
+
const loadSuggestions = useCallback(() => {
|
|
23
|
+
fetch(`/api/patterns?status=${filter}`)
|
|
24
|
+
.then(res => res.json())
|
|
25
|
+
.then((data: { suggestions: PatternSuggestion[] }) => setSuggestions(data.suggestions ?? []))
|
|
26
|
+
.catch(() => {});
|
|
27
|
+
}, [filter]);
|
|
28
|
+
|
|
29
|
+
useEffect(() => { loadSuggestions(); }, [loadSuggestions]);
|
|
30
|
+
|
|
31
|
+
async function handleAction(id: number, action: 'approve' | 'reject') {
|
|
32
|
+
setActioningId(id);
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(`/api/patterns/${action}`, {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'Content-Type': 'application/json' },
|
|
37
|
+
body: JSON.stringify({ id }),
|
|
38
|
+
});
|
|
39
|
+
if (res.ok) {
|
|
40
|
+
setSuggestions(prev => prev.filter(s => s.id !== id));
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// Silently fail
|
|
44
|
+
}
|
|
45
|
+
setActioningId(null);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="space-y-4">
|
|
50
|
+
<div className="flex gap-2">
|
|
51
|
+
{['pending', 'shadow', 'approved', 'rejected'].map(s => (
|
|
52
|
+
<button
|
|
53
|
+
key={s}
|
|
54
|
+
onClick={() => setFilter(s)}
|
|
55
|
+
className={`text-sm px-3 py-1 rounded-full transition-colors ${
|
|
56
|
+
filter === s
|
|
57
|
+
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300'
|
|
58
|
+
: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-600'
|
|
59
|
+
}`}
|
|
60
|
+
>
|
|
61
|
+
{s.charAt(0).toUpperCase() + s.slice(1)}
|
|
62
|
+
</button>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{suggestions.length === 0 ? (
|
|
67
|
+
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
|
|
68
|
+
No {filter} patterns.
|
|
69
|
+
</div>
|
|
70
|
+
) : (
|
|
71
|
+
<div className="space-y-3">
|
|
72
|
+
{suggestions.map(s => (
|
|
73
|
+
<div key={s.id} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
74
|
+
<div className="flex items-start justify-between gap-4">
|
|
75
|
+
<div className="flex-1 min-w-0">
|
|
76
|
+
<div className="flex items-center gap-2 mb-1">
|
|
77
|
+
<span className="text-xs px-1.5 py-0.5 rounded bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300 font-mono">
|
|
78
|
+
{s.pattern_type}
|
|
79
|
+
</span>
|
|
80
|
+
<span className="text-xs text-gray-500 dark:text-gray-400">
|
|
81
|
+
{s.source} · {s.match_count} matches
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
84
|
+
<p className="text-sm text-gray-900 dark:text-white font-mono truncate">{s.pattern_value}</p>
|
|
85
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
86
|
+
{s.error_type} · {s.priority} · {new Date(s.created_at).toLocaleDateString('en-AU')}
|
|
87
|
+
</p>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{filter === 'pending' && (
|
|
91
|
+
<div className="flex gap-2 flex-shrink-0">
|
|
92
|
+
<button
|
|
93
|
+
onClick={() => handleAction(s.id, 'approve')}
|
|
94
|
+
disabled={actioningId === s.id}
|
|
95
|
+
className="text-xs px-3 py-1.5 rounded bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300 hover:bg-green-200 dark:hover:bg-green-900/60 disabled:opacity-50"
|
|
96
|
+
>
|
|
97
|
+
Approve
|
|
98
|
+
</button>
|
|
99
|
+
<button
|
|
100
|
+
onClick={() => handleAction(s.id, 'reject')}
|
|
101
|
+
disabled={actioningId === s.id}
|
|
102
|
+
className="text-xs px-3 py-1.5 rounded bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300 hover:bg-red-200 dark:hover:bg-red-900/60 disabled:opacity-50"
|
|
103
|
+
>
|
|
104
|
+
Reject
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|