@littlebearapps/platform-admin-sdk 2.0.0 → 2.2.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 (185) hide show
  1. package/README.md +4 -7
  2. package/dist/templates.d.ts +1 -1
  3. package/dist/templates.js +206 -4
  4. package/package.json +1 -1
  5. package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
  6. package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
  7. package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
  8. package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
  9. package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
  10. package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
  11. package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
  12. package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
  13. package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
  14. package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
  15. package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
  16. package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
  17. package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
  18. package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
  19. package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
  20. package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
  21. package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
  22. package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
  23. package/templates/full/dashboard/src/lib/search/api.ts +258 -0
  24. package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
  25. package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
  26. package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
  27. package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
  28. package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
  29. package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
  30. package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
  31. package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
  32. package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
  33. package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
  34. package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
  35. package/templates/full/dashboard/src/pages/feedback.astro +365 -0
  36. package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
  37. package/templates/full/dashboard/src/pages/map.astro +561 -0
  38. package/templates/full/dashboard/src/pages/revenue.astro +72 -0
  39. package/templates/full/dashboard/src/pages/tests.astro +431 -0
  40. package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
  41. package/templates/full/scripts/ops/verify-account-total.ts +256 -0
  42. package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
  43. package/templates/full/tests/integration/r2-archive.test.ts +108 -0
  44. package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
  45. package/templates/shared/.github/workflows/validate-controls.yml +27 -0
  46. package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
  47. package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
  48. package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
  49. package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
  50. package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
  51. package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
  52. package/templates/shared/dashboard/src/components/Toast.astro +170 -0
  53. package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
  54. package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
  55. package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
  56. package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
  57. package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
  58. package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
  59. package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
  60. package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
  61. package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
  62. package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
  63. package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
  64. package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
  65. package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
  66. package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
  67. package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
  68. package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
  69. package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
  70. package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
  71. package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
  72. package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
  73. package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
  74. package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
  75. package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
  76. package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
  77. package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
  78. package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
  79. package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
  80. package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
  81. package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
  82. package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
  83. package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
  84. package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
  85. package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
  86. package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
  87. package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
  88. package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
  89. package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
  90. package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
  91. package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
  92. package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
  93. package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
  94. package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
  95. package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
  96. package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
  97. package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
  98. package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
  99. package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
  100. package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
  101. package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
  102. package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
  103. package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
  104. package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
  105. package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
  106. package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
  107. package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
  108. package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
  109. package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
  110. package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
  111. package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
  112. package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
  113. package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
  114. package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
  115. package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
  116. package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
  117. package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
  118. package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
  119. package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
  120. package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
  121. package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
  122. package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
  123. package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
  124. package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
  125. package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
  126. package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
  127. package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
  128. package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
  129. package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
  130. package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
  131. package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
  132. package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
  133. package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
  134. package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
  135. package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
  136. package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
  137. package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
  138. package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
  139. package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
  140. package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
  141. package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
  142. package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
  143. package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
  144. package/templates/shared/tests/helpers/mock-storage.ts +166 -0
  145. package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
  146. package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
  147. package/templates/shared/tests/unit/billing.test.ts +331 -0
  148. package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
  149. package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
  150. package/templates/shared/tests/unit/control.test.ts +226 -0
  151. package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
  152. package/templates/shared/tests/unit/economics.test.ts +365 -0
  153. package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
  154. package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
  155. package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
  156. package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
  157. package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
  158. package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
  159. package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
  160. package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
  161. package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
  162. package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
  163. package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
  164. package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
  165. package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
  166. package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
  167. package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
  168. package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
  169. package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
  170. package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
  171. package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
  172. package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
  173. package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
  174. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
  175. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
  176. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
  177. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
  178. package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
  179. package/templates/standard/tests/integration/connectors.test.ts +241 -0
  180. package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
  181. package/templates/standard/tests/integration/ingestion.test.ts +211 -0
  182. package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
  183. package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
  184. package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
  185. package/templates/standard/tests/unit/error-collector/github.test.ts +187 -0
@@ -0,0 +1,264 @@
1
+ /**
2
+ * NotificationItem.tsx
3
+ *
4
+ * Individual notification rendering with category-based styling,
5
+ * priority colours, and project badges.
6
+ *
7
+ * @module dashboard/components/notifications/NotificationItem
8
+ * @created 2026-02-03
9
+ * @updated 2026-02-04 - Added priority colours and project badges
10
+ * @task task-303.2, task-314
11
+ */
12
+
13
+ import type { Notification, NotificationCategory, NotificationPriority } from '../../lib/notifications/types';
14
+
15
+ export interface NotificationItemProps {
16
+ notification: Notification;
17
+ isRead: boolean;
18
+ onMarkRead?: (id: string) => void;
19
+ onDismiss?: (id: string) => void;
20
+ compact?: boolean;
21
+ }
22
+
23
+ /** Category-based icon and colour configuration */
24
+ const categoryConfig: Record<
25
+ NotificationCategory,
26
+ { icon: string; bgClass: string; iconClass: string }
27
+ > = {
28
+ error: {
29
+ icon: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z',
30
+ bgClass: 'bg-red-100 dark:bg-red-900/30',
31
+ iconClass: 'text-red-600 dark:text-red-400',
32
+ },
33
+ warning: {
34
+ icon: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z',
35
+ bgClass: 'bg-yellow-100 dark:bg-yellow-900/30',
36
+ iconClass: 'text-yellow-600 dark:text-yellow-400',
37
+ },
38
+ info: {
39
+ icon: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
40
+ bgClass: 'bg-blue-100 dark:bg-blue-900/30',
41
+ iconClass: 'text-blue-600 dark:text-blue-400',
42
+ },
43
+ success: {
44
+ icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
45
+ bgClass: 'bg-green-100 dark:bg-green-900/30',
46
+ iconClass: 'text-green-600 dark:text-green-400',
47
+ },
48
+ };
49
+
50
+ /** Priority-based colour configuration */
51
+ const priorityConfig: Record<
52
+ NotificationPriority,
53
+ { borderClass: string; badgeClass: string; label: string }
54
+ > = {
55
+ critical: {
56
+ borderClass: 'border-l-4 border-l-red-500',
57
+ badgeClass: 'bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300',
58
+ label: 'Critical',
59
+ },
60
+ high: {
61
+ borderClass: 'border-l-4 border-l-orange-500',
62
+ badgeClass: 'bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-300',
63
+ label: 'High',
64
+ },
65
+ medium: {
66
+ borderClass: 'border-l-4 border-l-yellow-500',
67
+ badgeClass: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-300',
68
+ label: 'Medium',
69
+ },
70
+ low: {
71
+ borderClass: 'border-l-4 border-l-blue-400',
72
+ badgeClass: 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300',
73
+ label: 'Low',
74
+ },
75
+ info: {
76
+ borderClass: 'border-l-4 border-l-gray-300 dark:border-l-gray-600',
77
+ badgeClass: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400',
78
+ label: 'Info',
79
+ },
80
+ };
81
+
82
+ /** Project display configuration */
83
+ const projectConfig: Record<string, { label: string; bgClass: string }> = {
84
+ platform: {
85
+ label: 'Platform',
86
+ bgClass: 'bg-purple-100 text-purple-700 dark:bg-purple-900/50 dark:text-purple-300',
87
+ },
88
+ 'brand-copilot': {
89
+ label: 'Brand Copilot',
90
+ bgClass: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:text-indigo-300',
91
+ },
92
+ scout: {
93
+ label: 'Scout',
94
+ bgClass: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300',
95
+ },
96
+ };
97
+
98
+ /** Source display configuration */
99
+ const sourceConfig: Record<string, { label: string; icon: string }> = {
100
+ 'error-collector': { label: 'Error', icon: 'M12 9v2m0 4h.01' },
101
+ 'pattern-discovery': { label: 'Pattern', icon: 'M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2z' },
102
+ 'circuit-breaker': { label: 'Circuit', icon: 'M13 10V3L4 14h7v7l9-11h-7z' },
103
+ usage: { label: 'Usage', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' },
104
+ gatus: { label: 'Monitoring', icon: 'M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M13 12a1 1 0 11-2 0 1 1 0 012 0z' },
105
+ system: { label: 'System', icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z' },
106
+ };
107
+
108
+ /** Format relative time string */
109
+ function formatRelativeTime(timestamp: number): string {
110
+ const now = Date.now();
111
+ const seconds = Math.floor((now - timestamp * 1000) / 1000);
112
+
113
+ if (seconds < 60) return 'Just now';
114
+ if (seconds < 3600) {
115
+ const mins = Math.floor(seconds / 60);
116
+ return `${mins}m ago`;
117
+ }
118
+ if (seconds < 86400) {
119
+ const hours = Math.floor(seconds / 3600);
120
+ return `${hours}h ago`;
121
+ }
122
+ const days = Math.floor(seconds / 86400);
123
+ if (days === 1) return 'Yesterday';
124
+ if (days < 7) return `${days}d ago`;
125
+ return new Date(timestamp * 1000).toLocaleDateString('en-AU', {
126
+ day: 'numeric',
127
+ month: 'short',
128
+ });
129
+ }
130
+
131
+ export function NotificationItem({
132
+ notification,
133
+ isRead,
134
+ onMarkRead,
135
+ onDismiss,
136
+ compact = false,
137
+ }: NotificationItemProps): JSX.Element {
138
+ const catConfig = categoryConfig[notification.category] || categoryConfig.info;
139
+ const prioConfig = priorityConfig[notification.priority] || priorityConfig.info;
140
+ const projConfig = notification.project ? projectConfig[notification.project] : null;
141
+ const srcConfig = sourceConfig[notification.source] || sourceConfig.system;
142
+
143
+ const handleClick = (): void => {
144
+ if (!isRead && onMarkRead) {
145
+ onMarkRead(notification.id);
146
+ }
147
+ if (notification.action_url) {
148
+ window.location.href = notification.action_url;
149
+ }
150
+ };
151
+
152
+ const handleDismiss = (e: React.MouseEvent): void => {
153
+ e.stopPropagation();
154
+ onDismiss?.(notification.id);
155
+ };
156
+
157
+ const baseClasses = isRead
158
+ ? 'hover:bg-gray-50 dark:hover:bg-gray-700/50'
159
+ : 'bg-blue-50 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-900/30';
160
+
161
+ // Show priority border for critical/high only
162
+ const priorityBorder = ['critical', 'high'].includes(notification.priority)
163
+ ? prioConfig.borderClass
164
+ : '';
165
+
166
+ return (
167
+ <div
168
+ className={`notification-item group flex items-start gap-3 px-4 py-3 cursor-pointer border-b border-gray-100 dark:border-gray-700 transition-colors ${baseClasses} ${priorityBorder}`}
169
+ onClick={handleClick}
170
+ role="button"
171
+ tabIndex={0}
172
+ onKeyDown={(e) => e.key === 'Enter' && handleClick()}
173
+ >
174
+ {/* Category icon */}
175
+ <div
176
+ className={`flex-shrink-0 w-8 h-8 rounded-full ${catConfig.bgClass} flex items-center justify-center`}
177
+ >
178
+ <svg
179
+ className={`w-4 h-4 ${catConfig.iconClass}`}
180
+ fill="none"
181
+ stroke="currentColor"
182
+ viewBox="0 0 24 24"
183
+ >
184
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={catConfig.icon} />
185
+ </svg>
186
+ </div>
187
+
188
+ {/* Content */}
189
+ <div className="flex-1 min-w-0">
190
+ {/* Title row with priority badge */}
191
+ <div className="flex items-center gap-2 flex-wrap">
192
+ <p
193
+ className={`text-sm ${isRead ? 'text-gray-700 dark:text-gray-300' : 'font-medium text-gray-900 dark:text-white'}`}
194
+ >
195
+ {notification.title}
196
+ </p>
197
+ {/* Priority badge for critical/high */}
198
+ {['critical', 'high'].includes(notification.priority) && (
199
+ <span className={`px-1.5 py-0.5 text-xs font-medium rounded ${prioConfig.badgeClass}`}>
200
+ {prioConfig.label}
201
+ </span>
202
+ )}
203
+ </div>
204
+
205
+ {/* Description */}
206
+ {notification.description && !compact && (
207
+ <p className="text-xs text-gray-500 dark:text-gray-400 truncate mt-0.5">
208
+ {notification.description}
209
+ </p>
210
+ )}
211
+
212
+ {/* Metadata row: time, project, source */}
213
+ <div className="flex items-center gap-2 mt-1 flex-wrap">
214
+ <span className="text-xs text-gray-400 dark:text-gray-500">
215
+ {formatRelativeTime(notification.created_at)}
216
+ </span>
217
+
218
+ {/* Project badge */}
219
+ {projConfig && (
220
+ <span className={`px-1.5 py-0.5 text-xs rounded ${projConfig.bgClass}`}>
221
+ {projConfig.label}
222
+ </span>
223
+ )}
224
+
225
+ {/* Source indicator */}
226
+ <span className="text-xs text-gray-400 dark:text-gray-500 flex items-center gap-1">
227
+ <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
228
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={srcConfig.icon} />
229
+ </svg>
230
+ {srcConfig.label}
231
+ </span>
232
+ </div>
233
+ </div>
234
+
235
+ {/* Unread indicator */}
236
+ {!isRead && <div className="flex-shrink-0 w-2 h-2 mt-2 bg-blue-500 rounded-full" />}
237
+
238
+ {/* Dismiss button (optional) */}
239
+ {onDismiss && (
240
+ <button
241
+ onClick={handleDismiss}
242
+ className="flex-shrink-0 p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600 opacity-0 group-hover:opacity-100 transition-opacity"
243
+ aria-label="Dismiss notification"
244
+ >
245
+ <svg
246
+ className="w-4 h-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
247
+ fill="none"
248
+ stroke="currentColor"
249
+ viewBox="0 0 24 24"
250
+ >
251
+ <path
252
+ strokeLinecap="round"
253
+ strokeLinejoin="round"
254
+ strokeWidth={2}
255
+ d="M6 18L18 6M6 6l12 12"
256
+ />
257
+ </svg>
258
+ </button>
259
+ )}
260
+ </div>
261
+ );
262
+ }
263
+
264
+ export default NotificationItem;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * PatternInfoButton Component
3
+ * Info icon button that toggles a panel explaining how pattern discovery works
4
+ */
5
+ import { useState, useRef, useEffect } from 'react';
6
+
7
+ export function PatternInfoButton() {
8
+ const [open, setOpen] = useState(false);
9
+ const panelRef = useRef<HTMLDivElement>(null);
10
+
11
+ useEffect(() => {
12
+ if (!open) return;
13
+
14
+ function handleClickOutside(event: MouseEvent) {
15
+ if (panelRef.current && !panelRef.current.contains(event.target as Node)) {
16
+ setOpen(false);
17
+ }
18
+ }
19
+
20
+ document.addEventListener('mousedown', handleClickOutside);
21
+ return () => document.removeEventListener('mousedown', handleClickOutside);
22
+ }, [open]);
23
+
24
+ return (
25
+ <div className="relative" ref={panelRef}>
26
+ <button
27
+ onClick={() => setOpen(!open)}
28
+ className="inline-flex items-center justify-center w-9 h-9 rounded-full bg-blue-100 hover:bg-blue-200 dark:bg-blue-900/40 dark:hover:bg-blue-900/60 text-blue-600 dark:text-blue-400 transition-colors"
29
+ title="How Pattern Discovery Works"
30
+ aria-label="How Pattern Discovery Works"
31
+ >
32
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
33
+ <path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
34
+ </svg>
35
+ </button>
36
+
37
+ {open && (
38
+ <div className="absolute right-0 top-12 z-50 w-96 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-lg p-4 shadow-lg">
39
+ <h3 className="font-semibold text-blue-800 dark:text-blue-300 mb-2">
40
+ How Pattern Discovery Works
41
+ </h3>
42
+ <ol className="text-sm text-blue-700 dark:text-blue-400 space-y-1.5 list-decimal list-inside">
43
+ <li>Daily cron job (2am UTC) analyses unclassified errors in D1</li>
44
+ <li>Similar errors are clustered and sent to AI (DeepSeek) for pattern suggestions</li>
45
+ <li>AI suggests patterns using a constrained DSL (contains, startsWith, statusCode, regex)</li>
46
+ <li>Human reviews and approves/rejects suggestions with backtest validation</li>
47
+ <li>Approved patterns are cached in KV and loaded by error-collector at runtime</li>
48
+ <li>System patterns are always active (built into error-collector code)</li>
49
+ </ol>
50
+ <button
51
+ onClick={() => setOpen(false)}
52
+ className="mt-3 text-xs text-blue-600 dark:text-blue-400 hover:underline"
53
+ >
54
+ Close
55
+ </button>
56
+ </div>
57
+ )}
58
+ </div>
59
+ );
60
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Digest Statistics Cards Component
3
+ * Shows warning digest summary stats and trends
4
+ */
5
+ import { useState, useEffect } from 'react';
6
+ import type { DigestStats as DigestStatsType } from '../../lib/reports/types';
7
+
8
+ interface StatCardProps {
9
+ label: string;
10
+ value: number | string;
11
+ subValue?: string;
12
+ colour: string;
13
+ icon: string;
14
+ }
15
+
16
+ function StatCard({ label, value, subValue, colour, icon }: StatCardProps) {
17
+ return (
18
+ <div className={`bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 ${colour}`}>
19
+ <div className="flex items-center justify-between">
20
+ <div>
21
+ <p className="text-sm font-medium text-gray-600 dark:text-gray-400">{label}</p>
22
+ <p className="text-2xl font-bold mt-1">{value}</p>
23
+ {subValue && (
24
+ <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">{subValue}</p>
25
+ )}
26
+ </div>
27
+ <span className="text-2xl">{icon}</span>
28
+ </div>
29
+ </div>
30
+ );
31
+ }
32
+
33
+ export function DigestStats() {
34
+ const [stats, setStats] = useState<DigestStatsType | null>(null);
35
+ const [loading, setLoading] = useState(true);
36
+ const [error, setError] = useState<string | null>(null);
37
+
38
+ useEffect(() => {
39
+ async function fetchStats() {
40
+ try {
41
+ const response = await fetch('/api/reports/digests/stats');
42
+ if (!response.ok) throw new Error('Failed to fetch stats');
43
+ const data = await response.json();
44
+ setStats(data);
45
+ } catch (e) {
46
+ setError(e instanceof Error ? e.message : 'Unknown error');
47
+ } finally {
48
+ setLoading(false);
49
+ }
50
+ }
51
+ fetchStats();
52
+ }, []);
53
+
54
+ if (loading) {
55
+ return (
56
+ <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
57
+ {[...Array(6)].map((_, i) => (
58
+ <div key={i} className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 animate-pulse">
59
+ <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-16 mb-2"></div>
60
+ <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-12"></div>
61
+ </div>
62
+ ))}
63
+ </div>
64
+ );
65
+ }
66
+
67
+ if (error) {
68
+ return (
69
+ <div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 text-red-700 dark:text-red-300">
70
+ Error loading stats: {error}
71
+ </div>
72
+ );
73
+ }
74
+
75
+ if (!stats) return null;
76
+
77
+ // Calculate trend (comparing today vs yesterday)
78
+ const todayOcc = stats.todayDigests.occurrences;
79
+ const yesterdayOcc = stats.yesterdayDigests.occurrences;
80
+ const trend = yesterdayOcc > 0
81
+ ? Math.round(((todayOcc - yesterdayOcc) / yesterdayOcc) * 100)
82
+ : todayOcc > 0 ? 100 : 0;
83
+ const trendText = trend > 0 ? `+${trend}%` : trend < 0 ? `${trend}%` : 'Same';
84
+
85
+ return (
86
+ <div className="space-y-4">
87
+ {/* Main Stats */}
88
+ <div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
89
+ <StatCard
90
+ label="Today's Warnings"
91
+ value={stats.todayDigests.occurrences}
92
+ subValue={`${stats.todayDigests.count} digest(s)`}
93
+ colour="border-l-4 border-l-yellow-500"
94
+ icon="⚠️"
95
+ />
96
+ <StatCard
97
+ label="Yesterday"
98
+ value={stats.yesterdayDigests.occurrences}
99
+ subValue={`${stats.yesterdayDigests.count} digest(s)`}
100
+ colour="border-l-4 border-l-gray-400"
101
+ icon="📅"
102
+ />
103
+ <StatCard
104
+ label="Daily Trend"
105
+ value={trendText}
106
+ subValue={trend > 0 ? 'Increase' : trend < 0 ? 'Decrease' : 'No change'}
107
+ colour={`border-l-4 ${trend > 20 ? 'border-l-red-500' : trend < -20 ? 'border-l-green-500' : 'border-l-blue-500'}`}
108
+ icon={trend > 0 ? '📈' : trend < 0 ? '📉' : '➡️'}
109
+ />
110
+ <StatCard
111
+ label="Total Digests"
112
+ value={stats.totalDigests}
113
+ colour="border-l-4 border-l-purple-500"
114
+ icon="📊"
115
+ />
116
+ <StatCard
117
+ label="Total Occurrences"
118
+ value={stats.totalOccurrences.toLocaleString()}
119
+ colour="border-l-4 border-l-indigo-500"
120
+ icon="🔢"
121
+ />
122
+ <StatCard
123
+ label="Active Scripts"
124
+ value={stats.byScript.length}
125
+ colour="border-l-4 border-l-teal-500"
126
+ icon="📜"
127
+ />
128
+ </div>
129
+
130
+ {/* Top Warnings Summary */}
131
+ {stats.topWarnings.length > 0 && (
132
+ <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
133
+ <h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-3">Top Warning Types</h3>
134
+ <div className="space-y-2">
135
+ {stats.topWarnings.slice(0, 3).map((warning, i) => (
136
+ <div key={i} className="flex items-center justify-between text-sm">
137
+ <span className="text-gray-600 dark:text-gray-400 truncate max-w-md">
138
+ {warning.normalized_message.slice(0, 60)}
139
+ {warning.normalized_message.length > 60 && '...'}
140
+ </span>
141
+ <span className="text-gray-900 dark:text-white font-medium ml-2">
142
+ {warning.occurrences.toLocaleString()} ({warning.days_occurred}d)
143
+ </span>
144
+ </div>
145
+ ))}
146
+ </div>
147
+ </div>
148
+ )}
149
+ </div>
150
+ );
151
+ }