@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,284 @@
1
+ ---
2
+ /**
3
+ * ProjectSelect Component
4
+ *
5
+ * Project filter dropdown using nanostores for reactive state.
6
+ * Fetches projects dynamically from the D1 registry via /api/usage/projects.
7
+ * No page reload on selection.
8
+ *
9
+ * Part of task-21: Usage Dashboard Refactor Phase 3 - Integrated Filter Bar
10
+ * Updated: task-XX: D1-backed project registry
11
+ */
12
+
13
+ interface Props {
14
+ /** Initial project from URL (for SSR hydration) */
15
+ initialProject?: string;
16
+ }
17
+
18
+ const { initialProject = 'all' } = Astro.props;
19
+
20
+ // Fallback projects shown during initial render (before API response)
21
+ const fallbackProjects = [{ value: 'all', label: 'All Projects' }];
22
+ ---
23
+
24
+ <div class="project-select" data-component="project-select" data-initial={initialProject}>
25
+ <label for="project-filter" class="select-label">Project</label>
26
+ <div class="select-wrapper">
27
+ <select id="project-filter" class="select-input">
28
+ {
29
+ fallbackProjects.map((proj) => (
30
+ <option value={proj.value} selected={initialProject === proj.value}>
31
+ {proj.label}
32
+ </option>
33
+ ))
34
+ }
35
+ </select>
36
+ <svg
37
+ class="select-chevron"
38
+ width="12"
39
+ height="12"
40
+ viewBox="0 0 12 12"
41
+ fill="none"
42
+ xmlns="http://www.w3.org/2000/svg"
43
+ >
44
+ <path
45
+ d="M3 4.5L6 7.5L9 4.5"
46
+ stroke="currentColor"
47
+ stroke-width="1.5"
48
+ stroke-linecap="round"
49
+ stroke-linejoin="round"></path>
50
+ </svg>
51
+ </div>
52
+ </div>
53
+
54
+ <style>
55
+ .project-select {
56
+ display: flex;
57
+ flex-direction: column;
58
+ gap: 0.25rem;
59
+ }
60
+
61
+ .select-label {
62
+ font-size: 0.625rem;
63
+ font-weight: 600;
64
+ color: var(--usage-text-muted);
65
+ text-transform: uppercase;
66
+ letter-spacing: 0.05em;
67
+ }
68
+
69
+ .select-wrapper {
70
+ position: relative;
71
+ display: flex;
72
+ align-items: center;
73
+ }
74
+
75
+ .select-input {
76
+ appearance: none;
77
+ padding: 0.5rem 2rem 0.5rem 0.75rem;
78
+ font-size: 0.8125rem;
79
+ font-family: inherit;
80
+ font-weight: 500;
81
+ color: var(--usage-text-primary);
82
+ background-color: var(--usage-bg-tertiary);
83
+ border: 1px solid var(--usage-border-primary);
84
+ border-radius: 0.5rem;
85
+ cursor: pointer;
86
+ transition: all 0.15s ease;
87
+ min-width: 140px;
88
+ }
89
+
90
+ .select-input:hover {
91
+ border-color: var(--usage-border-hover);
92
+ background-color: var(--usage-bg-secondary);
93
+ }
94
+
95
+ .select-input:focus {
96
+ outline: none;
97
+ border-color: var(--usage-accent-primary);
98
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--usage-accent-primary) 25%, transparent);
99
+ }
100
+
101
+ .select-chevron {
102
+ position: absolute;
103
+ right: 0.75rem;
104
+ pointer-events: none;
105
+ color: var(--usage-text-secondary);
106
+ transition: transform 0.15s ease;
107
+ }
108
+
109
+ .select-wrapper:has(.select-input:focus) .select-chevron {
110
+ transform: rotate(180deg);
111
+ }
112
+
113
+ @media (max-width: 640px) {
114
+ .select-input {
115
+ padding: 0.375rem 1.75rem 0.375rem 0.5rem;
116
+ font-size: 0.75rem;
117
+ min-width: 120px;
118
+ }
119
+
120
+ .select-chevron {
121
+ right: 0.5rem;
122
+ }
123
+ }
124
+
125
+ /* Dark mode overrides */
126
+ :global([data-theme='dark']) .select-label,
127
+ :global(.dark) .select-label {
128
+ color: #8b949e;
129
+ }
130
+
131
+ :global([data-theme='dark']) .select-input,
132
+ :global(.dark) .select-input {
133
+ background-color: #21262d;
134
+ border-color: #30363d;
135
+ color: #e6edf3;
136
+ }
137
+
138
+ :global([data-theme='dark']) .select-input:hover,
139
+ :global(.dark) .select-input:hover {
140
+ background-color: #161b22;
141
+ border-color: #484f58;
142
+ }
143
+
144
+ :global([data-theme='dark']) .select-input option,
145
+ :global(.dark) .select-input option {
146
+ background-color: #161b22;
147
+ color: #e6edf3;
148
+ }
149
+
150
+ :global([data-theme='dark']) .select-chevron,
151
+ :global(.dark) .select-chevron {
152
+ color: #8b949e;
153
+ }
154
+ </style>
155
+
156
+ <script>
157
+ import { $project } from '../state/usageStore';
158
+ import { setProject } from '../state/usageActions';
159
+
160
+ interface ProjectOption {
161
+ value: string;
162
+ label: string;
163
+ resourceCount?: number;
164
+ }
165
+
166
+ // Cache fetched projects
167
+ let projectsCache: ProjectOption[] | null = null;
168
+
169
+ /**
170
+ * Fetch projects from the D1 registry API
171
+ */
172
+ async function fetchProjects(): Promise<ProjectOption[]> {
173
+ if (projectsCache) return projectsCache;
174
+
175
+ try {
176
+ // Include credentials to pass Cloudflare Access JWT cookie
177
+ const response = await fetch('/api/usage/projects', {
178
+ credentials: 'include',
179
+ });
180
+ if (!response.ok) {
181
+ console.warn('[ProjectSelect] Failed to fetch projects, using fallback');
182
+ return getFallbackProjects();
183
+ }
184
+
185
+ const data = await response.json();
186
+ if (!data.success || !Array.isArray(data.projects)) {
187
+ return getFallbackProjects();
188
+ }
189
+
190
+ // Map to options: "All Projects" first, then sorted by resource count
191
+ const options: ProjectOption[] = [
192
+ { value: 'all', label: 'All Projects', resourceCount: data.totalResources },
193
+ ];
194
+
195
+ for (const project of data.projects) {
196
+ options.push({
197
+ value: project.projectId,
198
+ label: `${project.displayName} (${project.resourceCount})`,
199
+ resourceCount: project.resourceCount,
200
+ });
201
+ }
202
+
203
+ projectsCache = options;
204
+ return options;
205
+ } catch (error) {
206
+ console.error('[ProjectSelect] Error fetching projects:', error);
207
+ return getFallbackProjects();
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Fallback projects if API fails
213
+ */
214
+ function getFallbackProjects(): ProjectOption[] {
215
+ return [
216
+ { value: 'all', label: 'All Projects' },
217
+ { value: 'brand-copilot', label: 'Brand Copilot' },
218
+ { value: 'scout', label: 'Scout' },
219
+ { value: 'platform', label: 'Platform' },
220
+ { value: 'aus-history', label: 'Australian History MCP' },
221
+ { value: 'cloakpipe', label: 'CloakPipe' },
222
+ { value: 'shared', label: 'Shared' },
223
+ ];
224
+ }
225
+
226
+ /**
227
+ * Populate the select element with project options
228
+ */
229
+ function populateSelect(
230
+ select: HTMLSelectElement,
231
+ projects: ProjectOption[],
232
+ currentValue: string
233
+ ): void {
234
+ // Clear existing options
235
+ select.innerHTML = '';
236
+
237
+ // Add options
238
+ for (const proj of projects) {
239
+ const option = document.createElement('option');
240
+ option.value = proj.value;
241
+ option.textContent = proj.label;
242
+ if (proj.value === currentValue) {
243
+ option.selected = true;
244
+ }
245
+ select.appendChild(option);
246
+ }
247
+ }
248
+
249
+ async function initProjectSelect(): Promise<void> {
250
+ const container = document.querySelector('[data-component="project-select"]');
251
+ if (!container) return;
252
+
253
+ const select = container.querySelector<HTMLSelectElement>('#project-filter');
254
+ if (!select) return;
255
+
256
+ const initialProject = container.getAttribute('data-initial') || 'all';
257
+
258
+ // Fetch and populate projects from D1 registry
259
+ const projects = await fetchProjects();
260
+ populateSelect(select, projects, initialProject);
261
+
262
+ // Handle selection changes
263
+ select.addEventListener('change', () => {
264
+ setProject(select.value);
265
+ });
266
+
267
+ // Subscribe to store changes and update UI
268
+ $project.subscribe((project) => {
269
+ if (select.value !== project) {
270
+ select.value = project;
271
+ }
272
+ });
273
+ }
274
+
275
+ // Initialize on DOM ready
276
+ if (document.readyState === 'loading') {
277
+ document.addEventListener('DOMContentLoaded', () => initProjectSelect());
278
+ } else {
279
+ initProjectSelect();
280
+ }
281
+
282
+ // Re-initialize on Astro page transitions
283
+ document.addEventListener('astro:page-load', () => initProjectSelect());
284
+ </script>
@@ -0,0 +1,263 @@
1
+ /**
2
+ * DashboardShell Component
3
+ * Industrial Command Center orchestrator - data fetching, state, auto-refresh
4
+ */
5
+
6
+ import { useState, useEffect, useCallback } from 'react';
7
+ import { Activity, RefreshCw, AlertTriangle } from 'lucide-react';
8
+ import { clsx } from 'clsx';
9
+ import {
10
+ type Period,
11
+ type UsageRow,
12
+ type ProjectStatus,
13
+ type StatusResponse,
14
+ type QueryResponse,
15
+ } from './types';
16
+ import { UsageChart } from './UsageChart';
17
+ import { UsageTable } from './UsageTable';
18
+
19
+ interface DashboardShellProps {
20
+ initialPeriod?: Period;
21
+ }
22
+
23
+ const REFRESH_INTERVAL = 60_000; // 60 seconds
24
+
25
+ /**
26
+ * Period button component
27
+ */
28
+ function PeriodButton({
29
+ period,
30
+ currentPeriod,
31
+ onClick,
32
+ }: {
33
+ period: Period;
34
+ currentPeriod: Period;
35
+ onClick: (p: Period) => void;
36
+ }) {
37
+ const isActive = period === currentPeriod;
38
+ return (
39
+ <button
40
+ type="button"
41
+ onClick={() => onClick(period)}
42
+ className={clsx(
43
+ 'px-3 py-1.5 text-xs font-mono font-semibold uppercase tracking-wider rounded-sm transition-all',
44
+ isActive
45
+ ? 'bg-gray-200 dark:bg-slate-700 text-gray-900 dark:text-slate-100 shadow-inner'
46
+ : 'bg-gray-100 dark:bg-slate-800 text-gray-600 dark:text-slate-400 hover:bg-gray-200 dark:hover:bg-slate-700 hover:text-gray-800 dark:hover:text-slate-300'
47
+ )}
48
+ >
49
+ {period}
50
+ </button>
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Live indicator with pulse
56
+ */
57
+ function LiveIndicator({ isRefreshing }: { isRefreshing: boolean }) {
58
+ return (
59
+ <div className="flex items-center gap-2">
60
+ {isRefreshing ? (
61
+ <RefreshCw className="w-3.5 h-3.5 text-gray-500 dark:text-slate-400 animate-spin" />
62
+ ) : (
63
+ <span className="relative flex h-2.5 w-2.5">
64
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
65
+ <span className="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500" />
66
+ </span>
67
+ )}
68
+ <span className="text-xs font-mono text-gray-500 dark:text-slate-500 uppercase tracking-wider">
69
+ {isRefreshing ? 'Updating' : 'Live'}
70
+ </span>
71
+ </div>
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Error state component
77
+ */
78
+ function ErrorState({ message, onRetry }: { message: string; onRetry: () => void }) {
79
+ return (
80
+ <div className="bg-rose-500/10 border border-rose-500/30 rounded-sm p-6 flex items-start gap-4">
81
+ <AlertTriangle className="w-5 h-5 text-rose-400 flex-shrink-0 mt-0.5" />
82
+ <div className="flex-1">
83
+ <h3 className="text-rose-200 font-semibold text-sm">Failed to load data</h3>
84
+ <p className="text-rose-300/80 text-xs mt-1 font-mono">{message}</p>
85
+ <button
86
+ type="button"
87
+ onClick={onRetry}
88
+ className="mt-3 px-3 py-1.5 bg-rose-500/20 hover:bg-rose-500/30 text-rose-200 text-xs font-mono rounded-sm transition-colors"
89
+ >
90
+ Retry
91
+ </button>
92
+ </div>
93
+ </div>
94
+ );
95
+ }
96
+
97
+ /**
98
+ * Loading skeleton
99
+ */
100
+ function LoadingSkeleton() {
101
+ return (
102
+ <div className="space-y-6 animate-pulse">
103
+ {/* Header skeleton */}
104
+ <div className="flex justify-between items-center">
105
+ <div className="h-8 bg-gray-200 dark:bg-slate-800 rounded w-48" />
106
+ <div className="h-8 bg-gray-200 dark:bg-slate-800 rounded w-32" />
107
+ </div>
108
+
109
+ {/* Chart skeleton */}
110
+ <div className="space-y-3">
111
+ <div className="h-5 bg-gray-200 dark:bg-slate-800 rounded w-36" />
112
+ <div className="h-64 bg-gray-100 dark:bg-slate-900 rounded-sm border border-gray-200 dark:border-slate-800" />
113
+ </div>
114
+
115
+ {/* Table skeleton */}
116
+ <div className="space-y-3">
117
+ <div className="h-5 bg-gray-200 dark:bg-slate-800 rounded w-24" />
118
+ <div className="h-48 bg-gray-100 dark:bg-slate-900 rounded-sm border border-gray-200 dark:border-slate-800" />
119
+ </div>
120
+ </div>
121
+ );
122
+ }
123
+
124
+ export function DashboardShell({ initialPeriod = '24h' }: DashboardShellProps) {
125
+ const [period, setPeriod] = useState<Period>(initialPeriod);
126
+ const [loading, setLoading] = useState(true);
127
+ const [isRefreshing, setIsRefreshing] = useState(false);
128
+ const [error, setError] = useState<string | null>(null);
129
+ const [usageData, setUsageData] = useState<UsageRow[]>([]);
130
+ const [statusMap, setStatusMap] = useState<Record<string, ProjectStatus>>({});
131
+
132
+ /**
133
+ * Fetch data from both endpoints
134
+ */
135
+ const fetchData = useCallback(
136
+ async (isBackground = false) => {
137
+ if (isBackground) {
138
+ setIsRefreshing(true);
139
+ } else {
140
+ setLoading(true);
141
+ setError(null);
142
+ }
143
+
144
+ try {
145
+ const [statusRes, queryRes] = await Promise.all([
146
+ fetch(`/api/usage/status?period=${period}`),
147
+ fetch(`/api/usage/query?period=${period}`),
148
+ ]);
149
+
150
+ if (!statusRes.ok) {
151
+ throw new Error(`Status endpoint failed: ${statusRes.status}`);
152
+ }
153
+ if (!queryRes.ok) {
154
+ throw new Error(`Query endpoint failed: ${queryRes.status}`);
155
+ }
156
+
157
+ const statusData: StatusResponse = await statusRes.json();
158
+ const queryData: QueryResponse = await queryRes.json();
159
+
160
+ if (!statusData.success || !queryData.success) {
161
+ throw new Error('API returned unsuccessful response');
162
+ }
163
+
164
+ setStatusMap(statusData.projects);
165
+ setUsageData(queryData.data);
166
+ setError(null);
167
+ } catch (err) {
168
+ if (!isBackground) {
169
+ setError(err instanceof Error ? err.message : 'Unknown error occurred');
170
+ }
171
+ console.error('[DashboardShell] Fetch error:', err);
172
+ } finally {
173
+ setLoading(false);
174
+ setIsRefreshing(false);
175
+ }
176
+ },
177
+ [period]
178
+ );
179
+
180
+ /**
181
+ * Handle period change - update URL and refetch
182
+ */
183
+ const handlePeriodChange = useCallback((newPeriod: Period) => {
184
+ setPeriod(newPeriod);
185
+
186
+ // Update URL without reload
187
+ const url = new URL(window.location.href);
188
+ url.searchParams.set('period', newPeriod);
189
+ history.replaceState(null, '', url.toString());
190
+ }, []);
191
+
192
+ // Initial fetch and period change
193
+ useEffect(() => {
194
+ fetchData();
195
+ }, [fetchData]);
196
+
197
+ // Auto-refresh interval
198
+ useEffect(() => {
199
+ const interval = setInterval(() => {
200
+ fetchData(true);
201
+ }, REFRESH_INTERVAL);
202
+
203
+ return () => clearInterval(interval);
204
+ }, [fetchData]);
205
+
206
+ if (loading) {
207
+ return (
208
+ <div className="p-6 bg-gray-50 dark:bg-slate-950 min-h-screen">
209
+ <LoadingSkeleton />
210
+ </div>
211
+ );
212
+ }
213
+
214
+ if (error) {
215
+ return (
216
+ <div className="p-6 bg-gray-50 dark:bg-slate-950 min-h-screen">
217
+ <ErrorState message={error} onRetry={() => fetchData()} />
218
+ </div>
219
+ );
220
+ }
221
+
222
+ return (
223
+ <div className="p-6 bg-gray-50 dark:bg-slate-950 min-h-screen space-y-6">
224
+ {/* Header */}
225
+ <header className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
226
+ <div className="flex items-center gap-3">
227
+ <Activity className="w-5 h-5 text-gray-600 dark:text-slate-400" />
228
+ <h1 className="text-xl font-semibold text-gray-900 dark:text-slate-100 tracking-tight">
229
+ Usage Monitor
230
+ </h1>
231
+ <LiveIndicator isRefreshing={isRefreshing} />
232
+ </div>
233
+
234
+ <div className="flex items-center gap-2">
235
+ <PeriodButton period="24h" currentPeriod={period} onClick={handlePeriodChange} />
236
+ <PeriodButton period="7d" currentPeriod={period} onClick={handlePeriodChange} />
237
+ </div>
238
+ </header>
239
+
240
+ {/* Chart section */}
241
+ <section>
242
+ <h2 className="text-sm font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider mb-3">
243
+ Activity Timeline
244
+ </h2>
245
+ <UsageChart data={usageData} loading={isRefreshing && usageData.length === 0} />
246
+ </section>
247
+
248
+ {/* Table section */}
249
+ <section>
250
+ <h2 className="text-sm font-semibold text-gray-600 dark:text-slate-400 uppercase tracking-wider mb-3">
251
+ Projects
252
+ </h2>
253
+ <UsageTable
254
+ data={usageData}
255
+ statusMap={statusMap}
256
+ loading={isRefreshing && Object.keys(statusMap).length === 0}
257
+ />
258
+ </section>
259
+ </div>
260
+ );
261
+ }
262
+
263
+ export default DashboardShell;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * StatusBadge Component
3
+ * Industrial Command Center aesthetic - sharp corners, pulse dot, monospace
4
+ */
5
+
6
+ import { type OperationalStatus, STATUS_COLORS } from './types';
7
+ import { clsx } from 'clsx';
8
+
9
+ interface StatusBadgeProps {
10
+ status: OperationalStatus;
11
+ size?: 'sm' | 'md';
12
+ showLabel?: boolean;
13
+ }
14
+
15
+ export function StatusBadge({ status, size = 'md', showLabel = true }: StatusBadgeProps) {
16
+ const colors = STATUS_COLORS[status];
17
+
18
+ const sizeClasses = {
19
+ sm: 'px-1.5 py-0.5 text-[10px] gap-1',
20
+ md: 'px-2 py-1 text-xs gap-1.5',
21
+ };
22
+
23
+ const dotSizes = {
24
+ sm: 'w-1.5 h-1.5',
25
+ md: 'w-2 h-2',
26
+ };
27
+
28
+ return (
29
+ <span
30
+ className={clsx(
31
+ // Base styles - sharp industrial corners
32
+ 'inline-flex items-center rounded-sm font-mono font-semibold uppercase tracking-wider',
33
+ // Size
34
+ sizeClasses[size],
35
+ // Colours
36
+ colors.bg,
37
+ colors.text,
38
+ // Subtle glow effect
39
+ 'shadow-sm',
40
+ colors.glow
41
+ )}
42
+ >
43
+ {/* Pulse dot indicator */}
44
+ <span className="relative flex">
45
+ <span
46
+ className={clsx(
47
+ dotSizes[size],
48
+ 'rounded-full',
49
+ status === 'RUN' ? 'bg-emerald-400' : status === 'WARN' ? 'bg-amber-400' : 'bg-rose-400'
50
+ )}
51
+ />
52
+ {/* Pulse animation for RUN status */}
53
+ {status === 'RUN' && (
54
+ <span
55
+ className={clsx(
56
+ 'absolute inset-0 rounded-full bg-emerald-400 animate-ping opacity-75',
57
+ dotSizes[size]
58
+ )}
59
+ />
60
+ )}
61
+ {/* Slower pulse for WARN status */}
62
+ {status === 'WARN' && (
63
+ <span
64
+ className={clsx(
65
+ 'absolute inset-0 rounded-full bg-amber-400 animate-pulse opacity-50',
66
+ dotSizes[size]
67
+ )}
68
+ />
69
+ )}
70
+ </span>
71
+
72
+ {showLabel && <span>{status}</span>}
73
+ </span>
74
+ );
75
+ }
76
+
77
+ export default StatusBadge;