@littlebearapps/platform-admin-sdk 2.1.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 (115) hide show
  1. package/README.md +2 -5
  2. package/dist/templates.d.ts +1 -1
  3. package/dist/templates.js +121 -3
  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/FeatureUsageReport.tsx +339 -0
  9. package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
  10. package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
  11. package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
  12. package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
  13. package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
  14. package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
  15. package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
  16. package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
  17. package/templates/full/dashboard/src/pages/feedback.astro +365 -0
  18. package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
  19. package/templates/full/dashboard/src/pages/map.astro +561 -0
  20. package/templates/full/dashboard/src/pages/revenue.astro +72 -0
  21. package/templates/full/dashboard/src/pages/tests.astro +431 -0
  22. package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
  23. package/templates/full/scripts/ops/verify-account-total.ts +256 -0
  24. package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
  25. package/templates/full/tests/integration/r2-archive.test.ts +108 -0
  26. package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
  27. package/templates/shared/.github/workflows/validate-controls.yml +27 -0
  28. package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
  29. package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
  30. package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
  31. package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
  32. package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
  33. package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
  34. package/templates/shared/dashboard/src/components/Toast.astro +170 -0
  35. package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
  36. package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
  37. package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
  38. package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
  39. package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
  40. package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
  41. package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
  42. package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
  43. package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
  44. package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
  45. package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
  46. package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
  47. package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
  48. package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
  49. package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
  50. package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
  51. package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
  52. package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
  53. package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
  54. package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
  55. package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
  56. package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
  57. package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
  58. package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
  59. package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
  60. package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
  61. package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
  62. package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
  63. package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
  64. package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
  65. package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
  66. package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
  67. package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
  68. package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
  69. package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
  70. package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
  71. package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
  72. package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
  73. package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
  74. package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
  75. package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
  76. package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
  77. package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
  78. package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
  79. package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
  80. package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
  81. package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
  82. package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
  83. package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
  84. package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
  85. package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
  86. package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
  87. package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
  88. package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
  89. package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
  90. package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
  91. package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
  92. package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
  93. package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
  94. package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
  95. package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
  96. package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
  97. package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
  98. package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
  99. package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
  100. package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
  101. package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
  102. package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
  103. package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
  104. package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
  105. package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
  106. package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
  107. package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
  108. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
  109. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
  110. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
  111. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
  112. package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
  113. package/templates/standard/tests/integration/connectors.test.ts +241 -0
  114. package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
  115. package/templates/standard/tests/integration/ingestion.test.ts +211 -0
@@ -0,0 +1,46 @@
1
+ /**
2
+ * SearchResultGroup Component
3
+ *
4
+ * Renders a group header for search results by content type.
5
+ *
6
+ * @module dashboard/components/search/SearchResultGroup
7
+ * @created 2026-02-03
8
+ * @task task-303.3
9
+ */
10
+
11
+ import type { SearchContentType } from '../../lib/search/types';
12
+
13
+ /** Labels for each content type */
14
+ const CONTENT_TYPE_LABELS: Record<SearchContentType, string> = {
15
+ error: 'Errors',
16
+ pattern: 'Patterns',
17
+ setting: 'Settings',
18
+ page: 'Pages',
19
+ service: 'Services',
20
+ opportunity: 'Opportunities',
21
+ draft: 'Drafts',
22
+ project: 'Projects',
23
+ };
24
+
25
+ interface SearchResultGroupProps {
26
+ contentType: SearchContentType;
27
+ count: number;
28
+ }
29
+
30
+ export function SearchResultGroup({
31
+ contentType,
32
+ count,
33
+ }: SearchResultGroupProps): JSX.Element {
34
+ const label = CONTENT_TYPE_LABELS[contentType] || contentType;
35
+
36
+ return (
37
+ <div className="px-3 py-2 flex items-center justify-between">
38
+ <span className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
39
+ {label}
40
+ </span>
41
+ <span className="text-xs text-gray-400 dark:text-gray-500">
42
+ {count} {count === 1 ? 'result' : 'results'}
43
+ </span>
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * SearchResultItem Component
3
+ *
4
+ * Renders a single search result with icon, title, snippet, and content type indicator.
5
+ *
6
+ * @module dashboard/components/search/SearchResultItem
7
+ * @created 2026-02-03
8
+ * @task task-303.3
9
+ */
10
+
11
+ import type { SearchResult, SearchContentType } from '../../lib/search/types';
12
+
13
+ /** Icons for each content type */
14
+ const CONTENT_TYPE_ICONS: Record<SearchContentType, JSX.Element> = {
15
+ error: (
16
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
17
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
18
+ </svg>
19
+ ),
20
+ pattern: (
21
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
22
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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 2zM9 9h6v6H9V9z" />
23
+ </svg>
24
+ ),
25
+ setting: (
26
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
27
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
28
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
29
+ </svg>
30
+ ),
31
+ page: (
32
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
33
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
34
+ </svg>
35
+ ),
36
+ service: (
37
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
38
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
39
+ </svg>
40
+ ),
41
+ opportunity: (
42
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
43
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
44
+ </svg>
45
+ ),
46
+ draft: (
47
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
48
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
49
+ </svg>
50
+ ),
51
+ project: (
52
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
53
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
54
+ </svg>
55
+ ),
56
+ };
57
+
58
+ /** Colour classes for each content type */
59
+ const CONTENT_TYPE_COLORS: Record<SearchContentType, string> = {
60
+ error: 'text-red-500 dark:text-red-400 bg-red-100 dark:bg-red-900/30',
61
+ pattern: 'text-purple-500 dark:text-purple-400 bg-purple-100 dark:bg-purple-900/30',
62
+ setting: 'text-gray-500 dark:text-gray-400 bg-gray-100 dark:bg-gray-700',
63
+ page: 'text-blue-500 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/30',
64
+ service: 'text-green-500 dark:text-green-400 bg-green-100 dark:bg-green-900/30',
65
+ opportunity: 'text-orange-500 dark:text-orange-400 bg-orange-100 dark:bg-orange-900/30',
66
+ draft: 'text-cyan-500 dark:text-cyan-400 bg-cyan-100 dark:bg-cyan-900/30',
67
+ project: 'text-indigo-500 dark:text-indigo-400 bg-indigo-100 dark:bg-indigo-900/30',
68
+ };
69
+
70
+ /** Labels for each content type */
71
+ const CONTENT_TYPE_LABELS: Record<SearchContentType, string> = {
72
+ error: 'Error',
73
+ pattern: 'Pattern',
74
+ setting: 'Setting',
75
+ page: 'Page',
76
+ service: 'Service',
77
+ opportunity: 'Opportunity',
78
+ draft: 'Draft',
79
+ project: 'Project',
80
+ };
81
+
82
+ interface SearchResultItemProps {
83
+ result: SearchResult;
84
+ isSelected: boolean;
85
+ query: string;
86
+ onClick: () => void;
87
+ }
88
+
89
+ export function SearchResultItem({
90
+ result,
91
+ isSelected,
92
+ query,
93
+ onClick,
94
+ }: SearchResultItemProps): JSX.Element {
95
+ const icon = CONTENT_TYPE_ICONS[result.content_type] || CONTENT_TYPE_ICONS.page;
96
+ const colorClass = CONTENT_TYPE_COLORS[result.content_type] || CONTENT_TYPE_COLORS.page;
97
+ const label = CONTENT_TYPE_LABELS[result.content_type] || result.content_type;
98
+
99
+ // Highlight matching terms in the snippet - sanitized by only allowing alphanumeric matches wrapped in mark tags
100
+ const snippetText = result.snippet || result.content || '';
101
+ const highlightedParts = getHighlightedParts(snippetText, query);
102
+
103
+ return (
104
+ <a
105
+ href={result.url}
106
+ onClick={(e) => {
107
+ e.preventDefault();
108
+ onClick();
109
+ }}
110
+ className={`flex items-start gap-3 px-3 py-2 rounded-lg cursor-pointer transition-colors ${
111
+ isSelected
112
+ ? 'bg-blue-50 dark:bg-blue-900/30'
113
+ : 'hover:bg-gray-100 dark:hover:bg-gray-700'
114
+ }`}
115
+ >
116
+ {/* Icon */}
117
+ <span
118
+ className={`flex-shrink-0 w-8 h-8 rounded-lg flex items-center justify-center ${colorClass}`}
119
+ >
120
+ {icon}
121
+ </span>
122
+
123
+ {/* Content */}
124
+ <div className="flex-1 min-w-0">
125
+ <div className="flex items-center gap-2">
126
+ <span className="font-medium text-gray-900 dark:text-white truncate">
127
+ {result.title}
128
+ </span>
129
+ <span
130
+ className={`text-xs px-1.5 py-0.5 rounded ${colorClass}`}
131
+ >
132
+ {label}
133
+ </span>
134
+ </div>
135
+ {snippetText && (
136
+ <p className="text-sm text-gray-500 dark:text-gray-400 line-clamp-2 mt-0.5">
137
+ {highlightedParts.map((part, i) =>
138
+ part.isMatch ? (
139
+ <mark key={i} className="bg-yellow-200 dark:bg-yellow-800 rounded px-0.5">
140
+ {part.text}
141
+ </mark>
142
+ ) : (
143
+ <span key={i}>{part.text}</span>
144
+ )
145
+ )}
146
+ </p>
147
+ )}
148
+ {result.project && (
149
+ <span className="text-xs text-gray-400 dark:text-gray-500">
150
+ {result.project}
151
+ </span>
152
+ )}
153
+ </div>
154
+
155
+ {/* Arrow indicator when selected */}
156
+ {isSelected && (
157
+ <kbd className="flex-shrink-0 text-xs text-gray-400 border border-gray-200 dark:border-gray-600 rounded px-1.5 py-0.5">
158
+
159
+ </kbd>
160
+ )}
161
+ </a>
162
+ );
163
+ }
164
+
165
+ interface HighlightPart {
166
+ text: string;
167
+ isMatch: boolean;
168
+ }
169
+
170
+ /**
171
+ * Get highlighted parts of text matching search terms
172
+ * Returns array of parts with isMatch flag for safe rendering
173
+ */
174
+ function getHighlightedParts(text: string, query: string): HighlightPart[] {
175
+ if (!text || !query.trim()) {
176
+ return [{ text, isMatch: false }];
177
+ }
178
+
179
+ const terms = query
180
+ .toLowerCase()
181
+ .split(/\s+/)
182
+ .filter((t) => t.length > 2);
183
+
184
+ if (terms.length === 0) {
185
+ return [{ text, isMatch: false }];
186
+ }
187
+
188
+ // Escape special regex characters
189
+ const escaped = terms.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
190
+ const pattern = new RegExp(`(${escaped.join('|')})`, 'gi');
191
+
192
+ const parts: HighlightPart[] = [];
193
+ let lastIndex = 0;
194
+ let match: RegExpExecArray | null;
195
+
196
+ while ((match = pattern.exec(text)) !== null) {
197
+ // Add non-matching text before this match
198
+ if (match.index > lastIndex) {
199
+ parts.push({ text: text.slice(lastIndex, match.index), isMatch: false });
200
+ }
201
+ // Add the matching text
202
+ parts.push({ text: match[0], isMatch: true });
203
+ lastIndex = pattern.lastIndex;
204
+ }
205
+
206
+ // Add remaining text after last match
207
+ if (lastIndex < text.length) {
208
+ parts.push({ text: text.slice(lastIndex), isMatch: false });
209
+ }
210
+
211
+ return parts.length > 0 ? parts : [{ text, isMatch: false }];
212
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * POST /api/patterns/:id/approve - Proxy to pattern-discovery /suggestions/:id?action=approve
3
+ * Approves a pending pattern suggestion
4
+ */
5
+ import type { APIRoute } from 'astro';
6
+
7
+ interface Env {
8
+ PATTERN_DISCOVERY_API?: Fetcher;
9
+ }
10
+
11
+ export const POST: APIRoute = async ({ locals, params, url }) => {
12
+ const api = (locals.runtime?.env as Env)?.PATTERN_DISCOVERY_API;
13
+
14
+ if (!api) {
15
+ return new Response(JSON.stringify({ error: 'Pattern Discovery API not configured' }), {
16
+ status: 503,
17
+ headers: { 'Content-Type': 'application/json' },
18
+ });
19
+ }
20
+
21
+ const { id } = params;
22
+ if (!id) {
23
+ return new Response(JSON.stringify({ error: 'Missing suggestion ID' }), {
24
+ status: 400,
25
+ headers: { 'Content-Type': 'application/json' },
26
+ });
27
+ }
28
+
29
+ try {
30
+ const by = url.searchParams.get('by') || 'dashboard';
31
+
32
+ const response = await api.fetch(
33
+ `https://pattern-discovery.littlebearapps.workers.dev/suggestions/${id}?action=approve&by=${by}`,
34
+ { method: 'POST' }
35
+ );
36
+
37
+ const data = await response.json();
38
+ return new Response(JSON.stringify(data), {
39
+ status: response.status,
40
+ headers: { 'Content-Type': 'application/json' },
41
+ });
42
+ } catch (error) {
43
+ console.error('Error approving suggestion:', error);
44
+ return new Response(JSON.stringify({ error: 'Failed to approve suggestion' }), {
45
+ status: 500,
46
+ headers: { 'Content-Type': 'application/json' },
47
+ });
48
+ }
49
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * POST /api/patterns/:id/reject - Proxy to pattern-discovery /suggestions/:id?action=reject
3
+ * Rejects a pending pattern suggestion
4
+ */
5
+ import type { APIRoute } from 'astro';
6
+
7
+ interface Env {
8
+ PATTERN_DISCOVERY_API?: Fetcher;
9
+ }
10
+
11
+ export const POST: APIRoute = async ({ locals, params, url }) => {
12
+ const api = (locals.runtime?.env as Env)?.PATTERN_DISCOVERY_API;
13
+
14
+ if (!api) {
15
+ return new Response(JSON.stringify({ error: 'Pattern Discovery API not configured' }), {
16
+ status: 503,
17
+ headers: { 'Content-Type': 'application/json' },
18
+ });
19
+ }
20
+
21
+ const { id } = params;
22
+ if (!id) {
23
+ return new Response(JSON.stringify({ error: 'Missing suggestion ID' }), {
24
+ status: 400,
25
+ headers: { 'Content-Type': 'application/json' },
26
+ });
27
+ }
28
+
29
+ try {
30
+ const by = url.searchParams.get('by') || 'dashboard';
31
+ const reason = url.searchParams.get('reason') || 'Rejected via dashboard';
32
+
33
+ const response = await api.fetch(
34
+ `https://pattern-discovery.littlebearapps.workers.dev/suggestions/${id}?action=reject&by=${by}&reason=${encodeURIComponent(reason)}`,
35
+ { method: 'POST' }
36
+ );
37
+
38
+ const data = await response.json();
39
+ return new Response(JSON.stringify(data), {
40
+ status: response.status,
41
+ headers: { 'Content-Type': 'application/json' },
42
+ });
43
+ } catch (error) {
44
+ console.error('Error rejecting suggestion:', error);
45
+ return new Response(JSON.stringify({ error: 'Failed to reject suggestion' }), {
46
+ status: 500,
47
+ headers: { 'Content-Type': 'application/json' },
48
+ });
49
+ }
50
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * GET /api/reports/digests/stats - Proxy to error-collector /digests/stats
3
+ */
4
+ import type { APIRoute } from 'astro';
5
+
6
+ interface Env {
7
+ ERROR_COLLECTOR_API?: Fetcher;
8
+ }
9
+
10
+ export const GET: APIRoute = async ({ locals }) => {
11
+ const api = (locals.runtime?.env as Env)?.ERROR_COLLECTOR_API;
12
+
13
+ if (!api) {
14
+ return new Response(JSON.stringify({ error: 'Error Collector API not configured' }), {
15
+ status: 503,
16
+ headers: { 'Content-Type': 'application/json' },
17
+ });
18
+ }
19
+
20
+ try {
21
+ const response = await api.fetch(
22
+ 'https://error-collector.littlebearapps.workers.dev/digests/stats',
23
+ { method: 'GET' }
24
+ );
25
+
26
+ const data = await response.json();
27
+ return new Response(JSON.stringify(data), {
28
+ status: response.status,
29
+ headers: { 'Content-Type': 'application/json' },
30
+ });
31
+ } catch (error) {
32
+ console.error('Error fetching digest stats:', error);
33
+ return new Response(JSON.stringify({ error: 'Failed to fetch digest stats' }), {
34
+ status: 500,
35
+ headers: { 'Content-Type': 'application/json' },
36
+ });
37
+ }
38
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * GET /api/reports/digests - Proxy to error-collector /digests
3
+ */
4
+ import type { APIRoute } from 'astro';
5
+
6
+ interface Env {
7
+ ERROR_COLLECTOR_API?: Fetcher;
8
+ }
9
+
10
+ export const GET: APIRoute = async ({ request, locals }) => {
11
+ const api = (locals.runtime?.env as Env)?.ERROR_COLLECTOR_API;
12
+
13
+ if (!api) {
14
+ return new Response(JSON.stringify({ error: 'Error Collector API not configured' }), {
15
+ status: 503,
16
+ headers: { 'Content-Type': 'application/json' },
17
+ });
18
+ }
19
+
20
+ try {
21
+ const url = new URL(request.url);
22
+ const response = await api.fetch(
23
+ `https://error-collector.littlebearapps.workers.dev/digests${url.search}`,
24
+ { method: 'GET' }
25
+ );
26
+
27
+ const data = await response.json();
28
+ return new Response(JSON.stringify(data), {
29
+ status: response.status,
30
+ headers: { 'Content-Type': 'application/json' },
31
+ });
32
+ } catch (error) {
33
+ console.error('Error fetching digests:', error);
34
+ return new Response(JSON.stringify({ error: 'Failed to fetch digests' }), {
35
+ status: 500,
36
+ headers: { 'Content-Type': 'application/json' },
37
+ });
38
+ }
39
+ };
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Reindex API Proxy
3
+ *
4
+ * Proxies reindex requests to platform-search worker.
5
+ *
6
+ * @module dashboard/pages/api/search/reindex
7
+ * @created 2026-02-03
8
+ * @task task-303.3
9
+ */
10
+
11
+ import type { APIRoute } from 'astro';
12
+
13
+ /**
14
+ * POST /api/search/reindex/:type - Reindex all documents of a content type
15
+ */
16
+ export const POST: APIRoute = async ({ params, locals }) => {
17
+ const api = (locals.runtime?.env as { SEARCH_API?: Fetcher })?.SEARCH_API;
18
+ const { type } = params;
19
+
20
+ if (!api) {
21
+ return new Response(JSON.stringify({ error: 'Search API not available' }), {
22
+ status: 503,
23
+ headers: { 'Content-Type': 'application/json' },
24
+ });
25
+ }
26
+
27
+ if (!type) {
28
+ return new Response(JSON.stringify({ error: 'Content type required' }), {
29
+ status: 400,
30
+ headers: { 'Content-Type': 'application/json' },
31
+ });
32
+ }
33
+
34
+ try {
35
+ const response = await api.fetch(
36
+ new Request(
37
+ `https://platform-search.littlebearapps.workers.dev/search/reindex/${encodeURIComponent(type)}`,
38
+ {
39
+ method: 'POST',
40
+ }
41
+ )
42
+ );
43
+
44
+ const data = await response.json();
45
+ return new Response(JSON.stringify(data), {
46
+ status: response.status,
47
+ headers: { 'Content-Type': 'application/json' },
48
+ });
49
+ } catch (error) {
50
+ console.error('Reindex error:', error);
51
+ return new Response(JSON.stringify({ error: 'Failed to reindex content type' }), {
52
+ status: 500,
53
+ headers: { 'Content-Type': 'application/json' },
54
+ });
55
+ }
56
+ };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Test Reports API Endpoint
3
+ *
4
+ * Serves HTML test reports from KV storage
5
+ * Reports are stored by platform-ingest-tester worker
6
+ * TTL: 7 days (passes) or 90 days (failures)
7
+ */
8
+
9
+ export const GET = async ({ params, locals }: any) => {
10
+ const { id: runId } = params;
11
+ const env = locals.runtime.env;
12
+
13
+ if (!runId) {
14
+ return new Response(
15
+ JSON.stringify({
16
+ error: 'Missing run ID',
17
+ message: 'Run ID is required',
18
+ }),
19
+ {
20
+ status: 400,
21
+ headers: { 'Content-Type': 'application/json' },
22
+ }
23
+ );
24
+ }
25
+
26
+ try {
27
+ // First, get the run from D1 to find the KV key
28
+ const run = await env.PLATFORM_DB.prepare(
29
+ `SELECT id, project, kv_report_key, timestamp, pass_rate
30
+ FROM runs
31
+ WHERE id = ?`
32
+ )
33
+ .bind(runId)
34
+ .first();
35
+
36
+ if (!run) {
37
+ return new Response(
38
+ JSON.stringify({
39
+ error: 'Run not found',
40
+ message: `No test run found with ID: ${runId}`,
41
+ }),
42
+ {
43
+ status: 404,
44
+ headers: { 'Content-Type': 'application/json' },
45
+ }
46
+ );
47
+ }
48
+
49
+ if (!run.kv_report_key) {
50
+ return new Response(
51
+ JSON.stringify({
52
+ error: 'Report not available',
53
+ message: 'HTML report was not uploaded for this run',
54
+ }),
55
+ {
56
+ status: 404,
57
+ headers: { 'Content-Type': 'application/json' },
58
+ }
59
+ );
60
+ }
61
+
62
+ // Fetch HTML report from KV
63
+ const htmlReport = await env.PLATFORM_CACHE.get(run.kv_report_key);
64
+
65
+ if (!htmlReport) {
66
+ return new Response(
67
+ JSON.stringify({
68
+ error: 'Report expired',
69
+ message: 'HTML report has expired or been deleted (TTL: 7d for passes, 90d for failures)',
70
+ }),
71
+ {
72
+ status: 410, // Gone
73
+ headers: { 'Content-Type': 'application/json' },
74
+ }
75
+ );
76
+ }
77
+
78
+ // Return HTML report
79
+ return new Response(htmlReport, {
80
+ headers: {
81
+ 'Content-Type': 'text/html; charset=utf-8',
82
+ 'Cache-Control': 'public, max-age=3600', // Cache for 1 hour
83
+ 'X-Run-ID': runId,
84
+ 'X-Project': run.project as string,
85
+ 'X-Timestamp': run.timestamp as string,
86
+ },
87
+ });
88
+ } catch (error: any) {
89
+ console.error('Failed to fetch test report:', error);
90
+
91
+ return new Response(
92
+ JSON.stringify({
93
+ error: 'Internal server error',
94
+ message: error.message,
95
+ }),
96
+ {
97
+ status: 500,
98
+ headers: { 'Content-Type': 'application/json' },
99
+ }
100
+ );
101
+ }
102
+ };