@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.
Files changed (189) hide show
  1. package/dist/templates.d.ts +1 -1
  2. package/dist/templates.js +232 -2
  3. package/package.json +1 -1
  4. package/templates/full/config/audit-targets.yaml +72 -0
  5. package/templates/full/dashboard/src/components/notifications/NotificationBell.tsx +30 -0
  6. package/templates/full/dashboard/src/components/notifications/NotificationList.tsx +116 -0
  7. package/templates/full/dashboard/src/components/notifications/index.ts +2 -0
  8. package/templates/full/dashboard/src/components/patterns/ActivePatterns.tsx +62 -0
  9. package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
  10. package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
  11. package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
  12. package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
  13. package/templates/full/dashboard/src/components/patterns/index.ts +5 -0
  14. package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
  15. package/templates/full/dashboard/src/components/reports/SdkAuditReport.tsx +72 -0
  16. package/templates/full/dashboard/src/components/reports/index.ts +2 -0
  17. package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
  18. package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
  19. package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
  20. package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -0
  21. package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
  22. package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
  23. package/templates/full/dashboard/src/pages/api/patterns/cache-refresh.ts +38 -0
  24. package/templates/full/dashboard/src/pages/api/patterns/discover.ts +36 -0
  25. package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
  26. package/templates/full/dashboard/src/pages/api/patterns/ready-for-review.ts +39 -0
  27. package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
  28. package/templates/full/dashboard/src/pages/api/patterns/stats.ts +39 -0
  29. package/templates/full/dashboard/src/pages/api/patterns/suggestions.ts +43 -0
  30. package/templates/full/dashboard/src/pages/api/reports/audit.ts +45 -0
  31. package/templates/full/dashboard/src/pages/api/reports/usage.ts +52 -0
  32. package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
  33. package/templates/full/dashboard/src/pages/api/search/reindex.ts +28 -0
  34. package/templates/full/dashboard/src/pages/api/search/stats.ts +27 -0
  35. package/templates/full/dashboard/src/pages/api/settings/index.ts +37 -0
  36. package/templates/full/dashboard/src/pages/api/settings/update.ts +41 -0
  37. package/templates/full/dashboard/src/pages/api/topology/index.ts +56 -0
  38. package/templates/full/dashboard/src/pages/notifications.astro +11 -0
  39. package/templates/full/migrations/008_auditor.sql +99 -0
  40. package/templates/full/migrations/010_pricing_versions.sql +110 -0
  41. package/templates/full/migrations/011_multi_account.sql +51 -0
  42. package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
  43. package/templates/full/scripts/ops/universal-backfill.ts +147 -0
  44. package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
  45. package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
  46. package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
  47. package/templates/full/workers/lib/auditor/index.ts +9 -0
  48. package/templates/full/workers/lib/auditor/types.ts +167 -0
  49. package/templates/full/workers/platform-auditor.ts +1071 -0
  50. package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
  51. package/templates/shared/.github/workflows/contract-check.yml.hbs +42 -0
  52. package/templates/shared/.github/workflows/dashboard-deploy.yml.hbs +39 -0
  53. package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
  54. package/templates/shared/.github/workflows/security.yml +33 -0
  55. package/templates/shared/config/observability.yaml.hbs +276 -0
  56. package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
  57. package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
  58. package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
  59. package/templates/shared/dashboard/astro.config.mjs +21 -0
  60. package/templates/shared/dashboard/package.json.hbs +29 -0
  61. package/templates/shared/dashboard/src/components/Header.astro +29 -0
  62. package/templates/shared/dashboard/src/components/Nav.astro.hbs +59 -0
  63. package/templates/shared/dashboard/src/components/infrastructure/AlertHistory.tsx +57 -0
  64. package/templates/shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx +73 -0
  65. package/templates/shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx +55 -0
  66. package/templates/shared/dashboard/src/components/infrastructure/UptimeStatus.tsx +56 -0
  67. package/templates/shared/dashboard/src/components/infrastructure/index.ts +4 -0
  68. package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
  69. package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
  70. package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
  71. package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
  72. package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
  73. package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
  74. package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
  75. package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
  76. package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
  77. package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
  78. package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
  79. package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
  80. package/templates/shared/dashboard/src/components/ui/Breadcrumbs.tsx +27 -0
  81. package/templates/shared/dashboard/src/components/ui/EmptyState.tsx +26 -0
  82. package/templates/shared/dashboard/src/components/ui/ErrorBoundary.tsx +42 -0
  83. package/templates/shared/dashboard/src/components/ui/LoadingSkeleton.tsx +18 -0
  84. package/templates/shared/dashboard/src/components/ui/PageShell.tsx +26 -0
  85. package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
  86. package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
  87. package/templates/shared/dashboard/src/components/ui/Toast.tsx +44 -0
  88. package/templates/shared/dashboard/src/components/ui/index.ts +9 -0
  89. package/templates/shared/dashboard/src/components/usage/AnomaliesWidget.tsx +68 -0
  90. package/templates/shared/dashboard/src/components/usage/HourlyUsageChart.tsx +55 -0
  91. package/templates/shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx +67 -0
  92. package/templates/shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx +55 -0
  93. package/templates/shared/dashboard/src/components/usage/index.ts +4 -0
  94. package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
  95. package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
  96. package/templates/shared/dashboard/src/lib/cloudflare/costs.ts +21 -0
  97. package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
  98. package/templates/shared/dashboard/src/lib/types.ts +72 -0
  99. package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
  100. package/templates/shared/dashboard/src/middleware/index.ts +1 -0
  101. package/templates/shared/dashboard/src/pages/api/costs/overview.ts +65 -0
  102. package/templates/shared/dashboard/src/pages/api/costs/providers.ts +47 -0
  103. package/templates/shared/dashboard/src/pages/api/infrastructure/services.ts +55 -0
  104. package/templates/shared/dashboard/src/pages/api/infrastructure/stats.ts +99 -0
  105. package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
  106. package/templates/shared/dashboard/src/pages/api/usage/allowances.ts +56 -0
  107. package/templates/shared/dashboard/src/pages/api/usage/anomalies.ts +45 -0
  108. package/templates/shared/dashboard/src/pages/api/usage/billing.ts +53 -0
  109. package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
  110. package/templates/shared/dashboard/src/pages/api/usage/granular.ts +50 -0
  111. package/templates/shared/dashboard/src/pages/api/usage/hourly.ts +45 -0
  112. package/templates/shared/dashboard/src/pages/api/usage/projects.ts +51 -0
  113. package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
  114. package/templates/shared/dashboard/src/pages/api/user/identity.ts +11 -0
  115. package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
  116. package/templates/shared/dashboard/src/pages/index.astro +3 -0
  117. package/templates/shared/dashboard/src/pages/resources.astro +11 -0
  118. package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
  119. package/templates/shared/dashboard/src/pages/settings/notifications.astro +34 -0
  120. package/templates/shared/dashboard/src/pages/settings/thresholds.astro +39 -0
  121. package/templates/shared/dashboard/src/pages/settings/usage.astro +28 -0
  122. package/templates/shared/dashboard/src/styles/global.css +29 -0
  123. package/templates/shared/dashboard/tailwind.config.mjs +9 -0
  124. package/templates/shared/dashboard/tsconfig.json +9 -0
  125. package/templates/shared/dashboard/wrangler.json.hbs +47 -0
  126. package/templates/shared/docs/architecture.md +89 -0
  127. package/templates/shared/docs/post-deploy-runbook.md +126 -0
  128. package/templates/shared/docs/troubleshooting.md +91 -0
  129. package/templates/shared/package.json.hbs +17 -1
  130. package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
  131. package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
  132. package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -0
  133. package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
  134. package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
  135. package/templates/shared/scripts/ops/validate-controls.js +141 -0
  136. package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
  137. package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
  138. package/templates/shared/scripts/validate-schemas.js +61 -0
  139. package/templates/shared/tests/contract/validate-schemas.test.ts +130 -0
  140. package/templates/shared/tests/fixtures/telemetry-envelope-invalid.json +9 -0
  141. package/templates/shared/tests/fixtures/telemetry-envelope-valid.json +27 -0
  142. package/templates/shared/tests/helpers/mock-d1.ts +61 -0
  143. package/templates/shared/tests/helpers/mock-kv.ts +37 -0
  144. package/templates/shared/tests/unit/workers/batch-persistence.test.ts +133 -0
  145. package/templates/shared/tests/unit/workers/budget-enforcement.test.ts +214 -0
  146. package/templates/shared/vitest.config.ts +18 -0
  147. package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
  148. package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
  149. package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
  150. package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
  151. package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
  152. package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
  153. package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
  154. package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
  155. package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
  156. package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
  157. package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
  158. package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
  159. package/templates/shared/workers/platform-usage.ts +98 -8
  160. package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
  161. package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
  162. package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
  163. package/templates/standard/dashboard/src/components/health/CircuitBreakerEvents.tsx +69 -0
  164. package/templates/standard/dashboard/src/components/health/CircuitBreakerPanel.tsx +97 -0
  165. package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
  166. package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
  167. package/templates/standard/dashboard/src/components/health/index.ts +4 -0
  168. package/templates/standard/dashboard/src/lib/errors.ts +28 -0
  169. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts +49 -0
  170. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts +36 -0
  171. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint].ts +55 -0
  172. package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
  173. package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
  174. package/templates/standard/dashboard/src/pages/api/health/audit-history.ts +37 -0
  175. package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
  176. package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -0
  177. package/templates/standard/dashboard/src/pages/errors.astro +13 -0
  178. package/templates/standard/dashboard/src/pages/health.astro +11 -0
  179. package/templates/standard/migrations/009_topology_mapper.sql +65 -0
  180. package/templates/standard/tests/unit/error-collector/capture.test.ts +106 -0
  181. package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -0
  182. package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
  183. package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
  184. package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
  185. package/templates/standard/workers/lib/mapper/index.ts +7 -0
  186. package/templates/standard/workers/platform-mapper.ts +482 -0
  187. package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
  188. package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
  189. package/templates/standard/wrangler.sdk-test-client.jsonc.hbs +62 -0
@@ -0,0 +1,39 @@
1
+ import { useState } from 'react';
2
+
3
+ interface Props {
4
+ type: 'error' | 'warning' | 'info';
5
+ message: string;
6
+ detail?: string;
7
+ dismissable?: boolean;
8
+ }
9
+
10
+ export function AlertBanner({ type, message, detail, dismissable = true }: Props) {
11
+ const [dismissed, setDismissed] = useState(false);
12
+ if (dismissed) return null;
13
+
14
+ const styles = {
15
+ error: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-800 dark:text-red-200',
16
+ warning: 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-200 dark:border-yellow-800 text-yellow-800 dark:text-yellow-200',
17
+ info: 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800 text-blue-800 dark:text-blue-200',
18
+ };
19
+
20
+ return (
21
+ <div className={`border rounded-lg p-4 ${styles[type]}`}>
22
+ <div className="flex items-start justify-between gap-2">
23
+ <div>
24
+ <p className="font-medium">{message}</p>
25
+ {detail && <p className="text-sm mt-1 opacity-80">{detail}</p>}
26
+ </div>
27
+ {dismissable && (
28
+ <button
29
+ onClick={() => setDismissed(true)}
30
+ className="text-current opacity-50 hover:opacity-100 p-1"
31
+ aria-label="Dismiss"
32
+ >
33
+ x
34
+ </button>
35
+ )}
36
+ </div>
37
+ </div>
38
+ );
39
+ }
@@ -0,0 +1,27 @@
1
+ interface Crumb {
2
+ label: string;
3
+ href?: string;
4
+ }
5
+
6
+ interface Props {
7
+ items: Crumb[];
8
+ }
9
+
10
+ export function Breadcrumbs({ items }: Props) {
11
+ return (
12
+ <nav className="flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400 mb-4">
13
+ {items.map((item, i) => (
14
+ <span key={i} className="flex items-center gap-1.5">
15
+ {i > 0 && <span className="opacity-40">/</span>}
16
+ {item.href ? (
17
+ <a href={item.href} className="hover:text-gray-900 dark:hover:text-white transition-colors">
18
+ {item.label}
19
+ </a>
20
+ ) : (
21
+ <span className="text-gray-900 dark:text-white font-medium">{item.label}</span>
22
+ )}
23
+ </span>
24
+ ))}
25
+ </nav>
26
+ );
27
+ }
@@ -0,0 +1,26 @@
1
+ interface Props {
2
+ icon?: string;
3
+ title: string;
4
+ description?: string;
5
+ action?: { label: string; href: string };
6
+ }
7
+
8
+ export function EmptyState({ icon = '\u2205', title, description, action }: Props) {
9
+ return (
10
+ <div className="flex flex-col items-center justify-center py-12 text-center">
11
+ <span className="text-4xl mb-3 opacity-40">{icon}</span>
12
+ <h3 className="text-sm font-medium text-gray-900 dark:text-white">{title}</h3>
13
+ {description && (
14
+ <p className="mt-1 text-sm text-gray-500 dark:text-gray-400 max-w-sm">{description}</p>
15
+ )}
16
+ {action && (
17
+ <a
18
+ href={action.href}
19
+ className="mt-4 text-sm font-medium text-blue-600 dark:text-blue-400 hover:underline"
20
+ >
21
+ {action.label}
22
+ </a>
23
+ )}
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,42 @@
1
+ import { Component } from 'react';
2
+ import type { ReactNode, ErrorInfo } from 'react';
3
+
4
+ interface Props {
5
+ children: ReactNode;
6
+ fallback?: ReactNode;
7
+ }
8
+
9
+ interface State {
10
+ hasError: boolean;
11
+ error?: Error;
12
+ }
13
+
14
+ export class ErrorBoundary extends Component<Props, State> {
15
+ constructor(props: Props) {
16
+ super(props);
17
+ this.state = { hasError: false };
18
+ }
19
+
20
+ static getDerivedStateFromError(error: Error): State {
21
+ return { hasError: true, error };
22
+ }
23
+
24
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
25
+ console.error('[ErrorBoundary]', error, errorInfo);
26
+ }
27
+
28
+ render(): ReactNode {
29
+ if (this.state.hasError) {
30
+ return (
31
+ this.props.fallback ?? (
32
+ <div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
33
+ <p className="text-sm font-medium text-red-800 dark:text-red-300">Something went wrong</p>
34
+ <p className="text-xs text-red-600 dark:text-red-400 mt-1">{this.state.error?.message}</p>
35
+ </div>
36
+ )
37
+ );
38
+ }
39
+
40
+ return this.props.children;
41
+ }
42
+ }
@@ -0,0 +1,18 @@
1
+ interface Props {
2
+ lines?: number;
3
+ className?: string;
4
+ }
5
+
6
+ export function LoadingSkeleton({ lines = 3, className = '' }: Props) {
7
+ return (
8
+ <div className={`animate-pulse space-y-3 ${className}`}>
9
+ {Array.from({ length: lines }).map((_, i) => (
10
+ <div
11
+ key={i}
12
+ className="h-4 bg-gray-200 dark:bg-gray-700 rounded"
13
+ style={{ width: `${85 - i * 15}%` }}
14
+ />
15
+ ))}
16
+ </div>
17
+ );
18
+ }
@@ -0,0 +1,26 @@
1
+ import type { ReactNode } from 'react';
2
+ import { Breadcrumbs } from './Breadcrumbs';
3
+
4
+ interface PageShellProps {
5
+ title: string;
6
+ description?: string;
7
+ breadcrumbs?: Array<{ label: string; href?: string }>;
8
+ actions?: ReactNode;
9
+ children: ReactNode;
10
+ }
11
+
12
+ export function PageShell({ title, description, breadcrumbs, actions, children }: PageShellProps) {
13
+ return (
14
+ <div className="space-y-4">
15
+ {breadcrumbs && breadcrumbs.length > 0 && <Breadcrumbs items={breadcrumbs} />}
16
+ <div className="flex items-start justify-between gap-4">
17
+ <div>
18
+ <h1 className="text-xl font-semibold text-gray-900 dark:text-white">{title}</h1>
19
+ {description && <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">{description}</p>}
20
+ </div>
21
+ {actions && <div className="shrink-0">{actions}</div>}
22
+ </div>
23
+ {children}
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Sparkline Component
3
+ *
4
+ * Inline SVG sparkline for activity trends.
5
+ * Minimal, industrial aesthetic.
6
+ */
7
+
8
+ import { useMemo } from 'react';
9
+
10
+ interface SparklineProps {
11
+ data: number[];
12
+ width?: number;
13
+ height?: number;
14
+ color?: string;
15
+ strokeWidth?: number;
16
+ showFill?: boolean;
17
+ className?: string;
18
+ }
19
+
20
+ export function Sparkline({
21
+ data,
22
+ width = 60,
23
+ height = 20,
24
+ color = '#3b82f6',
25
+ strokeWidth = 1.5,
26
+ showFill = true,
27
+ className = '',
28
+ }: SparklineProps) {
29
+ const pathData = useMemo(() => {
30
+ if (data.length < 2) return { line: '', fill: '' };
31
+
32
+ const min = Math.min(...data);
33
+ const max = Math.max(...data);
34
+ const range = max - min || 1;
35
+
36
+ // Padding to prevent clipping
37
+ const padding = 2;
38
+ const chartWidth = width - padding * 2;
39
+ const chartHeight = height - padding * 2;
40
+
41
+ const points = data.map((value, index) => {
42
+ const x = padding + (index / (data.length - 1)) * chartWidth;
43
+ const y = padding + chartHeight - ((value - min) / range) * chartHeight;
44
+ return { x, y };
45
+ });
46
+
47
+ // Build line path
48
+ const linePath = points
49
+ .map((p, i) => (i === 0 ? `M ${p.x} ${p.y}` : `L ${p.x} ${p.y}`))
50
+ .join(' ');
51
+
52
+ // Build fill path (close at bottom)
53
+ const fillPath = `${linePath} L ${points[points.length - 1].x} ${height - padding} L ${padding} ${height - padding} Z`;
54
+
55
+ return { line: linePath, fill: fillPath };
56
+ }, [data, width, height]);
57
+
58
+ if (data.length < 2) {
59
+ return (
60
+ <svg
61
+ width={width}
62
+ height={height}
63
+ className={className}
64
+ viewBox={`0 0 ${width} ${height}`}
65
+ aria-hidden="true"
66
+ >
67
+ <line
68
+ x1={2}
69
+ y1={height / 2}
70
+ x2={width - 2}
71
+ y2={height / 2}
72
+ stroke={color}
73
+ strokeWidth={strokeWidth}
74
+ strokeOpacity={0.3}
75
+ strokeDasharray="2 2"
76
+ />
77
+ </svg>
78
+ );
79
+ }
80
+
81
+ return (
82
+ <svg
83
+ width={width}
84
+ height={height}
85
+ className={className}
86
+ viewBox={`0 0 ${width} ${height}`}
87
+ aria-hidden="true"
88
+ >
89
+ {/* Fill gradient */}
90
+ {showFill && (
91
+ <>
92
+ <defs>
93
+ <linearGradient
94
+ id={`sparkline-fill-${color.replace('#', '')}`}
95
+ x1="0"
96
+ y1="0"
97
+ x2="0"
98
+ y2="1"
99
+ >
100
+ <stop offset="0%" stopColor={color} stopOpacity={0.3} />
101
+ <stop offset="100%" stopColor={color} stopOpacity={0} />
102
+ </linearGradient>
103
+ </defs>
104
+ <path d={pathData.fill} fill={`url(#sparkline-fill-${color.replace('#', '')})`} />
105
+ </>
106
+ )}
107
+ {/* Line */}
108
+ <path
109
+ d={pathData.line}
110
+ fill="none"
111
+ stroke={color}
112
+ strokeWidth={strokeWidth}
113
+ strokeLinecap="round"
114
+ strokeLinejoin="round"
115
+ />
116
+ {/* End dot */}
117
+ <circle
118
+ cx={width - 2}
119
+ cy={pathData.line ? parseFloat(pathData.line.split(' ').slice(-1)[0]) : height / 2}
120
+ r={2}
121
+ fill={color}
122
+ />
123
+ </svg>
124
+ );
125
+ }
126
+
127
+ export default Sparkline;
@@ -0,0 +1,21 @@
1
+ import clsx from 'clsx';
2
+
3
+ interface Props {
4
+ status: 'green' | 'yellow' | 'red' | 'gray';
5
+ size?: 'sm' | 'md';
6
+ }
7
+
8
+ export function StatusDot({ status, size = 'sm' }: Props) {
9
+ return (
10
+ <span
11
+ className={clsx(
12
+ 'inline-block rounded-full',
13
+ size === 'sm' ? 'w-2 h-2' : 'w-3 h-3',
14
+ status === 'green' && 'bg-green-500',
15
+ status === 'yellow' && 'bg-yellow-500',
16
+ status === 'red' && 'bg-red-500',
17
+ status === 'gray' && 'bg-gray-400',
18
+ )}
19
+ />
20
+ );
21
+ }
@@ -0,0 +1,44 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ interface Props {
4
+ type: 'success' | 'error' | 'info';
5
+ message: string;
6
+ duration?: number;
7
+ onDismiss?: () => void;
8
+ }
9
+
10
+ const styles: Record<Props['type'], string> = {
11
+ success: 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 text-green-800 dark:text-green-200',
12
+ error: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-800 dark:text-red-200',
13
+ info: 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800 text-blue-800 dark:text-blue-200',
14
+ };
15
+
16
+ export function Toast({ type, message, duration = 5000, onDismiss }: Props) {
17
+ const [visible, setVisible] = useState(true);
18
+
19
+ useEffect(() => {
20
+ if (duration <= 0) return;
21
+ const timer = setTimeout(() => {
22
+ setVisible(false);
23
+ onDismiss?.();
24
+ }, duration);
25
+ return () => clearTimeout(timer);
26
+ }, [duration, onDismiss]);
27
+
28
+ if (!visible) return null;
29
+
30
+ return (
31
+ <div className={`fixed bottom-4 right-4 z-50 border rounded-lg px-4 py-3 shadow-lg max-w-sm ${styles[type]}`}>
32
+ <div className="flex items-center justify-between gap-3">
33
+ <p className="text-sm font-medium">{message}</p>
34
+ <button
35
+ onClick={() => { setVisible(false); onDismiss?.(); }}
36
+ className="text-current opacity-50 hover:opacity-100 shrink-0"
37
+ aria-label="Dismiss"
38
+ >
39
+ x
40
+ </button>
41
+ </div>
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,9 @@
1
+ export { Sparkline } from './Sparkline';
2
+ export { StatusDot } from './StatusDot';
3
+ export { AlertBanner } from './AlertBanner';
4
+ export { EmptyState } from './EmptyState';
5
+ export { LoadingSkeleton } from './LoadingSkeleton';
6
+ export { Breadcrumbs } from './Breadcrumbs';
7
+ export { Toast } from './Toast';
8
+ export { ErrorBoundary } from './ErrorBoundary';
9
+ export { PageShell } from './PageShell';
@@ -0,0 +1,68 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { EmptyState } from '../ui/EmptyState';
3
+ import { LoadingSkeleton } from '../ui/LoadingSkeleton';
4
+
5
+ interface Anomaly {
6
+ id: number;
7
+ project: string;
8
+ metric: string;
9
+ current_value: number;
10
+ baseline_value: number;
11
+ deviation_pct: number;
12
+ detected_at: string;
13
+ status: string;
14
+ }
15
+
16
+ export function AnomaliesWidget() {
17
+ const [anomalies, setAnomalies] = useState<Anomaly[]>([]);
18
+ const [loading, setLoading] = useState(true);
19
+
20
+ useEffect(() => {
21
+ fetch('/api/usage/anomalies')
22
+ .then(res => res.json())
23
+ .then((data: { anomalies: Anomaly[] }) => { setAnomalies(data.anomalies); setLoading(false); })
24
+ .catch(() => setLoading(false));
25
+ }, []);
26
+
27
+ if (loading) return <LoadingSkeleton lines={3} />;
28
+
29
+ if (anomalies.length === 0) {
30
+ return (
31
+ <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
32
+ <h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
33
+ Anomalies
34
+ </h3>
35
+ <EmptyState title="No anomalies detected" description="Usage patterns look normal." />
36
+ </div>
37
+ );
38
+ }
39
+
40
+ return (
41
+ <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
42
+ <h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
43
+ Recent Anomalies
44
+ </h3>
45
+ <div className="space-y-2">
46
+ {anomalies.map(a => (
47
+ <div key={a.id} className="flex items-center justify-between py-1.5 border-b border-gray-100 dark:border-gray-700 last:border-0">
48
+ <div>
49
+ <p className="text-sm text-gray-900 dark:text-white">
50
+ {a.metric.replace(/_/g, ' ')} — {a.project}
51
+ </p>
52
+ <p className="text-xs text-gray-500 dark:text-gray-400">
53
+ {new Date(a.detected_at).toLocaleDateString()}
54
+ </p>
55
+ </div>
56
+ <span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
57
+ a.deviation_pct > 200
58
+ ? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
59
+ : 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400'
60
+ }`}>
61
+ +{a.deviation_pct}%
62
+ </span>
63
+ </div>
64
+ ))}
65
+ </div>
66
+ </div>
67
+ );
68
+ }
@@ -0,0 +1,55 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { Sparkline } from '../ui/Sparkline';
3
+ import { LoadingSkeleton } from '../ui/LoadingSkeleton';
4
+
5
+ interface Snapshot {
6
+ snapshot_hour: string;
7
+ d1_reads: number;
8
+ d1_writes: number;
9
+ kv_reads: number;
10
+ kv_writes: number;
11
+ total_cost_usd: number;
12
+ }
13
+
14
+ interface Props {
15
+ hours?: number;
16
+ }
17
+
18
+ export function HourlyUsageChart({ hours = 24 }: Props) {
19
+ const [snapshots, setSnapshots] = useState<Snapshot[]>([]);
20
+ const [loading, setLoading] = useState(true);
21
+
22
+ useEffect(() => {
23
+ fetch(`/api/usage/hourly?hours=${hours}`)
24
+ .then(res => res.json())
25
+ .then((data: { snapshots: Snapshot[] }) => { setSnapshots(data.snapshots); setLoading(false); })
26
+ .catch(() => setLoading(false));
27
+ }, [hours]);
28
+
29
+ if (loading) return <LoadingSkeleton lines={3} />;
30
+
31
+ const costData = snapshots.map(s => s.total_cost_usd);
32
+ const d1WriteData = snapshots.map(s => s.d1_writes);
33
+
34
+ return (
35
+ <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
36
+ <h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
37
+ Hourly Usage ({hours}h)
38
+ </h3>
39
+ {snapshots.length === 0 ? (
40
+ <p className="text-sm text-gray-400">No hourly data available yet.</p>
41
+ ) : (
42
+ <div className="space-y-4">
43
+ <div>
44
+ <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Cost (USD)</p>
45
+ <Sparkline data={costData} height={40} color="blue" />
46
+ </div>
47
+ <div>
48
+ <p className="text-xs text-gray-500 dark:text-gray-400 mb-1">D1 Writes</p>
49
+ <Sparkline data={d1WriteData} height={40} color="amber" />
50
+ </div>
51
+ </div>
52
+ )}
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,67 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { LoadingSkeleton } from '../ui/LoadingSkeleton';
3
+
4
+ interface Allowance {
5
+ resource: string;
6
+ allowance: number;
7
+ used: number;
8
+ pct: number;
9
+ }
10
+
11
+ function formatNumber(n: number): string {
12
+ if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`;
13
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
14
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
15
+ return String(n);
16
+ }
17
+
18
+ function pctColor(pct: number): string {
19
+ if (pct >= 90) return 'bg-red-500';
20
+ if (pct >= 75) return 'bg-orange-500';
21
+ if (pct >= 50) return 'bg-yellow-500';
22
+ return 'bg-green-500';
23
+ }
24
+
25
+ export function PlanAllowanceDashboard() {
26
+ const [allowances, setAllowances] = useState<Allowance[]>([]);
27
+ const [loading, setLoading] = useState(true);
28
+
29
+ useEffect(() => {
30
+ fetch('/api/usage/allowances')
31
+ .then(res => res.json())
32
+ .then((data: { allowances: Allowance[] }) => { setAllowances(data.allowances); setLoading(false); })
33
+ .catch(() => setLoading(false));
34
+ }, []);
35
+
36
+ if (loading) return <LoadingSkeleton lines={4} />;
37
+
38
+ if (allowances.length === 0) {
39
+ return <p className="text-sm text-gray-500 dark:text-gray-400">No allowance data available.</p>;
40
+ }
41
+
42
+ return (
43
+ <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
44
+ <h3 className="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">
45
+ Plan Allowances (MTD)
46
+ </h3>
47
+ <div className="space-y-3">
48
+ {allowances.map(a => (
49
+ <div key={a.resource}>
50
+ <div className="flex items-center justify-between text-sm mb-1">
51
+ <span className="text-gray-700 dark:text-gray-300">{a.resource.replace(/_/g, ' ')}</span>
52
+ <span className="text-gray-500 dark:text-gray-400">
53
+ {formatNumber(a.used)} / {formatNumber(a.allowance)} ({a.pct}%)
54
+ </span>
55
+ </div>
56
+ <div className="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
57
+ <div
58
+ className={`h-full rounded-full transition-all ${pctColor(a.pct)}`}
59
+ style={{ width: `${Math.min(a.pct, 100)}%` }}
60
+ />
61
+ </div>
62
+ </div>
63
+ ))}
64
+ </div>
65
+ </div>
66
+ );
67
+ }
@@ -0,0 +1,55 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { LoadingSkeleton } from '../ui/LoadingSkeleton';
3
+ import { EmptyState } from '../ui/EmptyState';
4
+
5
+ interface ProjectCost {
6
+ project: string;
7
+ total_cost_usd: number;
8
+ d1_writes: number;
9
+ worker_requests: number;
10
+ }
11
+
12
+ export function ProjectCostBreakdown() {
13
+ const [projects, setProjects] = useState<ProjectCost[]>([]);
14
+ const [loading, setLoading] = useState(true);
15
+
16
+ useEffect(() => {
17
+ fetch('/api/usage/projects')
18
+ .then((r) => r.json())
19
+ .then((data: { projects: ProjectCost[] }) => {
20
+ setProjects(data.projects ?? []);
21
+ setLoading(false);
22
+ })
23
+ .catch(() => setLoading(false));
24
+ }, []);
25
+
26
+ if (loading) return <LoadingSkeleton lines={4} />;
27
+ if (projects.length === 0) return <EmptyState title="No project data" description="Project cost data will appear after usage collection runs." />;
28
+
29
+ const maxCost = Math.max(...projects.map((p) => p.total_cost_usd), 0.01);
30
+
31
+ return (
32
+ <div className="space-y-3">
33
+ {projects.map((p) => (
34
+ <div key={p.project} className="bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 p-3">
35
+ <div className="flex items-center justify-between mb-1">
36
+ <span className="text-sm font-medium text-gray-900 dark:text-white">{p.project}</span>
37
+ <span className="text-sm font-mono text-gray-700 dark:text-gray-300">
38
+ ${p.total_cost_usd.toFixed(2)}
39
+ </span>
40
+ </div>
41
+ <div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
42
+ <div
43
+ className="bg-blue-500 h-1.5 rounded-full transition-all"
44
+ style={{ width: `${Math.min((p.total_cost_usd / maxCost) * 100, 100)}%` }}
45
+ />
46
+ </div>
47
+ <div className="flex gap-4 mt-1 text-xs text-gray-500 dark:text-gray-400">
48
+ <span>D1 writes: {p.d1_writes.toLocaleString()}</span>
49
+ <span>Requests: {p.worker_requests.toLocaleString()}</span>
50
+ </div>
51
+ </div>
52
+ ))}
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,4 @@
1
+ export { HourlyUsageChart } from './HourlyUsageChart';
2
+ export { PlanAllowanceDashboard } from './PlanAllowanceDashboard';
3
+ export { AnomaliesWidget } from './AnomaliesWidget';
4
+ export { ProjectCostBreakdown } from './ProjectCostBreakdown';
@@ -0,0 +1,34 @@
1
+ /// <reference types="astro/client" />
2
+
3
+ interface Env {
4
+ PLATFORM_DB: D1Database;
5
+ PLATFORM_CACHE: KVNamespace;
6
+ USAGE_API: Fetcher;
7
+ {{#if isStandard}}
8
+ ERROR_COLLECTOR_API: Fetcher;
9
+ {{/if}}
10
+ {{#if isFull}}
11
+ PATTERN_DISCOVERY_API: Fetcher;
12
+ NOTIFICATIONS_API: Fetcher;
13
+ SETTINGS_API: Fetcher;
14
+ SEARCH_API: Fetcher;
15
+ {{/if}}
16
+ CF_ACCESS_AUD?: string;
17
+ CF_ACCESS_ISSUER?: string;
18
+ CF_ACCESS_TEAM_DOMAIN?: string;
19
+ }
20
+
21
+ declare namespace App {
22
+ interface Locals {
23
+ runtime?: {
24
+ env: Env;
25
+ };
26
+ user?: {
27
+ email?: string;
28
+ name?: string;
29
+ sub?: string;
30
+ audience?: unknown;
31
+ issuer?: unknown;
32
+ };
33
+ }
34
+ }