@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,464 @@
1
+ /**
2
+ * Tabs & Filters Controller
3
+ *
4
+ * Handles tab switching, filter state management, export functionality,
5
+ * and auto-refresh for the Usage Dashboard.
6
+ * Extracted from index.astro for task-22.5 (slim to <300 lines)
7
+ */
8
+
9
+ import {
10
+ renderResourceTable,
11
+ updateResourceCount,
12
+ type UnifiedResource,
13
+ } from './resource-table-builder';
14
+ import { getSSRConfig } from './constants';
15
+
16
+ // ========== Types ==========
17
+
18
+ export interface FilterState {
19
+ period: string;
20
+ project: string;
21
+ serviceTypes: string[];
22
+ searchQuery: string;
23
+ onlyChanged: boolean;
24
+ nonZeroCost: boolean;
25
+ compareMode: 'sequential' | 'same-period';
26
+ }
27
+
28
+ // ========== State ==========
29
+
30
+ let allResources: UnifiedResource[] = [];
31
+ let filteredResources: UnifiedResource[] = [];
32
+
33
+ const filterState: FilterState = {
34
+ period: '30d',
35
+ project: 'all',
36
+ serviceTypes: [],
37
+ searchQuery: '',
38
+ onlyChanged: false,
39
+ nonZeroCost: false,
40
+ compareMode: 'sequential',
41
+ };
42
+
43
+ // ========== Tab Switching ==========
44
+
45
+ function initTabSwitching(): void {
46
+ const tabButtons = document.querySelectorAll('.tab-button');
47
+ const tabPanels = document.querySelectorAll('.tab-panel');
48
+
49
+ tabButtons.forEach((button) => {
50
+ button.addEventListener('click', () => {
51
+ const tabId = button.getAttribute('data-tab');
52
+ if (!tabId) return;
53
+
54
+ // Update active states (using Tailwind hidden class)
55
+ tabButtons.forEach((btn) => {
56
+ btn.classList.remove('active');
57
+ btn.removeAttribute('data-active');
58
+ });
59
+ tabPanels.forEach((panel) => {
60
+ panel.classList.add('hidden');
61
+ });
62
+
63
+ button.classList.add('active');
64
+ button.setAttribute('data-active', '');
65
+ const targetPanel = document.getElementById(`tab-${tabId}`);
66
+ if (targetPanel) {
67
+ targetPanel.classList.remove('hidden');
68
+ }
69
+ });
70
+ });
71
+ }
72
+
73
+ // ========== Filter Event Listeners ==========
74
+
75
+ function initFilterListeners(): void {
76
+ // Period selector
77
+ const periodButtons = document.querySelectorAll('[data-period]');
78
+ periodButtons.forEach((btn) => {
79
+ btn.addEventListener('click', () => {
80
+ const period = btn.getAttribute('data-period');
81
+ if (period) {
82
+ filterState.period = period;
83
+ updateURL();
84
+ // nanostores will handle data refresh via store subscriptions
85
+ }
86
+ });
87
+ });
88
+
89
+ // Project filter
90
+ const projectSelect = document.getElementById('project-filter') as HTMLSelectElement;
91
+ if (projectSelect) {
92
+ projectSelect.addEventListener('change', () => {
93
+ filterState.project = projectSelect.value;
94
+ updateURL();
95
+ applyFiltersAndRender();
96
+ });
97
+ }
98
+
99
+ // Search filter
100
+ const searchInput = document.getElementById('resource-search') as HTMLInputElement;
101
+ if (searchInput) {
102
+ searchInput.addEventListener(
103
+ 'input',
104
+ debounce(() => {
105
+ filterState.searchQuery = searchInput.value;
106
+ applyFiltersAndRender();
107
+ }, 300)
108
+ );
109
+ }
110
+
111
+ // Toggle filters (only changed, non-zero cost)
112
+ const onlyChangedToggle = document.getElementById('only-changed-toggle') as HTMLInputElement;
113
+ if (onlyChangedToggle) {
114
+ onlyChangedToggle.addEventListener('change', () => {
115
+ filterState.onlyChanged = onlyChangedToggle.checked;
116
+ applyFiltersAndRender();
117
+ });
118
+ }
119
+
120
+ const nonZeroCostToggle = document.getElementById('non-zero-cost-toggle') as HTMLInputElement;
121
+ if (nonZeroCostToggle) {
122
+ nonZeroCostToggle.addEventListener('change', () => {
123
+ filterState.nonZeroCost = nonZeroCostToggle.checked;
124
+ applyFiltersAndRender();
125
+ });
126
+ }
127
+
128
+ // Compare mode selector
129
+ const compareModeSelect = document.getElementById('compare-mode') as HTMLSelectElement;
130
+ if (compareModeSelect) {
131
+ compareModeSelect.addEventListener('change', () => {
132
+ filterState.compareMode = compareModeSelect.value as 'sequential' | 'same-period';
133
+ updateURL();
134
+ });
135
+ }
136
+ }
137
+
138
+ // ========== URL Sync ==========
139
+
140
+ function updateURL(): void {
141
+ const params = new URLSearchParams(window.location.search);
142
+ params.set('period', filterState.period);
143
+ if (filterState.project !== 'all') {
144
+ params.set('project', filterState.project);
145
+ } else {
146
+ params.delete('project');
147
+ }
148
+ params.set('compare', filterState.compareMode);
149
+
150
+ const newURL = `${window.location.pathname}?${params.toString()}`;
151
+ window.history.replaceState({}, '', newURL);
152
+ }
153
+
154
+ // ========== Filter & Render ==========
155
+
156
+ function applyFiltersAndRender(): void {
157
+ filteredResources = allResources.filter((resource) => {
158
+ // Project filter
159
+ if (
160
+ filterState.project &&
161
+ filterState.project !== 'all' &&
162
+ resource.project !== filterState.project
163
+ ) {
164
+ return false;
165
+ }
166
+
167
+ // Search filter
168
+ if (filterState.searchQuery) {
169
+ const query = filterState.searchQuery.toLowerCase();
170
+ if (!resource.name.toLowerCase().includes(query)) {
171
+ return false;
172
+ }
173
+ }
174
+
175
+ // Only changed filter (>5% change or NEW)
176
+ if (filterState.onlyChanged) {
177
+ if (resource.costDeltaPct === 'NEW') return true;
178
+ if (typeof resource.costDeltaPct !== 'number') return false;
179
+ if (Math.abs(resource.costDeltaPct) <= 5) return false;
180
+ }
181
+
182
+ // Non-zero cost filter
183
+ if (filterState.nonZeroCost && resource.costCurrent === 0) {
184
+ return false;
185
+ }
186
+
187
+ return true;
188
+ });
189
+
190
+ renderResourceTable(filteredResources);
191
+ updateResourceCount(filteredResources.length);
192
+ }
193
+
194
+ // ========== Export Functionality (Task-17.15) ==========
195
+
196
+ function getExportFilename(ext: string): string {
197
+ const now = new Date();
198
+ const month = now.toLocaleString('en-AU', { month: 'short' }).toLowerCase();
199
+ const year = now.getFullYear();
200
+ const project = filterState.project === 'all' ? 'all-projects' : filterState.project;
201
+ // Format: usage-brandcopilot-jan2026.csv
202
+ return `usage-${project}-${month}${year}.${ext}`;
203
+ }
204
+
205
+ function downloadFile(content: string, filename: string, mimeType: string): void {
206
+ const blob = new Blob([content], { type: mimeType });
207
+ const url = URL.createObjectURL(blob);
208
+ const link = document.createElement('a');
209
+ link.href = url;
210
+ link.download = filename;
211
+ document.body.appendChild(link);
212
+ link.click();
213
+ document.body.removeChild(link);
214
+ URL.revokeObjectURL(url);
215
+ }
216
+
217
+ function exportToCsv(): void {
218
+ if (filteredResources.length === 0) {
219
+ alert('No data to export');
220
+ return;
221
+ }
222
+
223
+ const headers = [
224
+ 'Name',
225
+ 'Type',
226
+ 'Project',
227
+ 'Usage',
228
+ 'Unit',
229
+ '% of Limit',
230
+ 'Current Cost ($)',
231
+ 'Prior Cost ($)',
232
+ 'Change (%)',
233
+ 'Status',
234
+ ];
235
+ const rows = filteredResources.map((r) => [
236
+ r.name,
237
+ r.type,
238
+ r.project,
239
+ r.usage.value.toString(),
240
+ r.usage.unit,
241
+ r.limitPct != null ? r.limitPct.toFixed(1) : '',
242
+ r.costCurrent.toFixed(4),
243
+ r.costPrior.toFixed(4),
244
+ r.costDeltaPct === 'NEW' ? 'NEW' : (r.costDeltaPct?.toFixed(1) ?? '0'),
245
+ r.status,
246
+ ]);
247
+
248
+ const csvContent = [headers, ...rows]
249
+ .map((row) => row.map((cell) => `"${cell.toString().replace(/"/g, '""')}"`).join(','))
250
+ .join('\n');
251
+
252
+ downloadFile(csvContent, getExportFilename('csv'), 'text/csv;charset=utf-8;');
253
+ }
254
+
255
+ function exportToJson(): void {
256
+ if (filteredResources.length === 0) {
257
+ alert('No data to export');
258
+ return;
259
+ }
260
+
261
+ const exportData = {
262
+ exportedAt: new Date().toISOString(),
263
+ filters: {
264
+ period: filterState.period,
265
+ project: filterState.project,
266
+ serviceTypes: filterState.serviceTypes,
267
+ searchQuery: filterState.searchQuery,
268
+ onlyChanged: filterState.onlyChanged,
269
+ nonZeroCost: filterState.nonZeroCost,
270
+ compareMode: filterState.compareMode,
271
+ },
272
+ summary: {
273
+ totalResources: filteredResources.length,
274
+ totalCurrentCost: filteredResources.reduce((sum, r) => sum + r.costCurrent, 0),
275
+ totalPriorCost: filteredResources.reduce((sum, r) => sum + r.costPrior, 0),
276
+ resourcesByType: filteredResources.reduce(
277
+ (acc, r) => {
278
+ acc[r.type] = (acc[r.type] || 0) + 1;
279
+ return acc;
280
+ },
281
+ {} as Record<string, number>
282
+ ),
283
+ },
284
+ resources: filteredResources.map((r) => ({
285
+ id: r.id,
286
+ name: r.name,
287
+ type: r.type,
288
+ project: r.project,
289
+ repoUrl: r.repoUrl,
290
+ usage: r.usage,
291
+ limitPct: r.limitPct,
292
+ costCurrent: r.costCurrent,
293
+ costPrior: r.costPrior,
294
+ costDelta: r.costDelta,
295
+ costDeltaPct: r.costDeltaPct,
296
+ status: r.status,
297
+ })),
298
+ };
299
+
300
+ const jsonContent = JSON.stringify(exportData, null, 2);
301
+ downloadFile(jsonContent, getExportFilename('json'), 'application/json;charset=utf-8;');
302
+ }
303
+
304
+ function initExportButtons(): void {
305
+ const exportCsvBtn = document.getElementById('export-csv');
306
+ const exportJsonBtn = document.getElementById('export-json');
307
+
308
+ if (exportCsvBtn) {
309
+ exportCsvBtn.addEventListener('click', exportToCsv);
310
+ }
311
+
312
+ if (exportJsonBtn) {
313
+ exportJsonBtn.addEventListener('click', exportToJson);
314
+ }
315
+ }
316
+
317
+ // ========== Auto-Refresh Functionality (Task-17.17) ==========
318
+
319
+ const REFRESH_INTERVAL = 30; // seconds
320
+ let autoRefreshTimer: ReturnType<typeof setInterval> | null = null;
321
+ let countdownTimer: ReturnType<typeof setInterval> | null = null;
322
+ let countdownValue = REFRESH_INTERVAL;
323
+
324
+ function updateCountdown(): void {
325
+ const refreshCountdown = document.getElementById('refresh-countdown');
326
+ if (refreshCountdown) {
327
+ refreshCountdown.textContent = `${countdownValue}s`;
328
+ }
329
+ countdownValue--;
330
+ if (countdownValue < 0) {
331
+ countdownValue = REFRESH_INTERVAL;
332
+ }
333
+ }
334
+
335
+ async function refreshData(): Promise<void> {
336
+ try {
337
+ const params = new URLSearchParams();
338
+ params.set('period', filterState.period);
339
+ if (filterState.project !== 'all') {
340
+ params.set('project', filterState.project);
341
+ }
342
+ params.set('compare', filterState.compareMode);
343
+
344
+ // Include credentials to pass Cloudflare Access JWT cookie
345
+ const response = await fetch(`/api/usage?${params.toString()}`, {
346
+ credentials: 'include',
347
+ });
348
+ if (!response.ok) {
349
+ console.error('Failed to refresh usage data');
350
+ return;
351
+ }
352
+
353
+ const data = await response.json();
354
+ if (data.resources && Array.isArray(data.resources)) {
355
+ allResources = data.resources;
356
+ applyFiltersAndRender();
357
+ }
358
+ } catch (error) {
359
+ console.error('Error refreshing data:', error);
360
+ }
361
+ }
362
+
363
+ function startAutoRefresh(): void {
364
+ if (autoRefreshTimer) return;
365
+
366
+ const refreshCountdown = document.getElementById('refresh-countdown');
367
+ countdownValue = REFRESH_INTERVAL;
368
+ if (refreshCountdown) {
369
+ refreshCountdown.classList.remove('hidden');
370
+ updateCountdown();
371
+ }
372
+
373
+ countdownTimer = setInterval(updateCountdown, 1000);
374
+ autoRefreshTimer = setInterval(async () => {
375
+ await refreshData();
376
+ countdownValue = REFRESH_INTERVAL;
377
+ }, REFRESH_INTERVAL * 1000);
378
+ }
379
+
380
+ function stopAutoRefresh(): void {
381
+ if (autoRefreshTimer) {
382
+ clearInterval(autoRefreshTimer);
383
+ autoRefreshTimer = null;
384
+ }
385
+ if (countdownTimer) {
386
+ clearInterval(countdownTimer);
387
+ countdownTimer = null;
388
+ }
389
+ const refreshCountdown = document.getElementById('refresh-countdown');
390
+ if (refreshCountdown) {
391
+ refreshCountdown.classList.add('hidden');
392
+ }
393
+ }
394
+
395
+ function initAutoRefresh(): void {
396
+ const autoRefreshToggle = document.getElementById('auto-refresh-toggle') as HTMLInputElement;
397
+ if (!autoRefreshToggle) return;
398
+
399
+ // Restore from localStorage
400
+ const savedAutoRefresh = localStorage.getItem('usage-auto-refresh');
401
+ if (savedAutoRefresh === 'true') {
402
+ autoRefreshToggle.checked = true;
403
+ startAutoRefresh();
404
+ }
405
+
406
+ autoRefreshToggle.addEventListener('change', () => {
407
+ if (autoRefreshToggle.checked) {
408
+ startAutoRefresh();
409
+ localStorage.setItem('usage-auto-refresh', 'true');
410
+ } else {
411
+ stopAutoRefresh();
412
+ localStorage.setItem('usage-auto-refresh', 'false');
413
+ }
414
+ });
415
+
416
+ // Stop auto-refresh when page is hidden
417
+ document.addEventListener('visibilitychange', () => {
418
+ if (document.hidden && autoRefreshToggle.checked) {
419
+ stopAutoRefresh();
420
+ } else if (!document.hidden && autoRefreshToggle.checked) {
421
+ startAutoRefresh();
422
+ }
423
+ });
424
+ }
425
+
426
+ // ========== Utility Functions ==========
427
+
428
+ function debounce<T extends (...args: unknown[]) => void>(fn: T, delay: number): T {
429
+ let timeoutId: ReturnType<typeof setTimeout>;
430
+ return ((...args: unknown[]) => {
431
+ clearTimeout(timeoutId);
432
+ timeoutId = setTimeout(() => fn(...args), delay);
433
+ }) as T;
434
+ }
435
+
436
+ // ========== Resource Management ==========
437
+
438
+ export function setResources(resources: UnifiedResource[]): void {
439
+ allResources = resources;
440
+ applyFiltersAndRender();
441
+ }
442
+
443
+ export function getFilterState(): FilterState {
444
+ return { ...filterState };
445
+ }
446
+
447
+ // ========== Main Initialization ==========
448
+
449
+ export function initTabsAndFilters(): void {
450
+ // Load initial config from SSR data attributes
451
+ const config = getSSRConfig();
452
+ filterState.period = config.period;
453
+ filterState.project = config.project;
454
+
455
+ // Initialize all subsystems
456
+ initTabSwitching();
457
+ initFilterListeners();
458
+ initExportButtons();
459
+ initAutoRefresh();
460
+
461
+ // Expose setResources globally for data loading
462
+ (window as unknown as { usagePageSetResources: typeof setResources }).usagePageSetResources =
463
+ setResources;
464
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Usage Dashboard State
3
+ *
4
+ * Re-exports all store atoms, computed values, and actions.
5
+ * Part of task-19: Usage Dashboard Refactor Phase 1 - Foundation
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import {
10
+ * $period, $dailyData, $isLoading,
11
+ * fetchDailyData, syncFromURL, setPeriod
12
+ * } from './state';
13
+ * ```
14
+ */
15
+
16
+ // Store atoms
17
+ export {
18
+ $period,
19
+ $project,
20
+ $customDateRange,
21
+ $timezone,
22
+ $dailyData,
23
+ $isLoading,
24
+ $selectedDate,
25
+ $totalCost,
26
+ $dateRangeDisplay,
27
+ $dayCount,
28
+ $averageDailyCost,
29
+ $resourceTotals,
30
+ type Period,
31
+ } from './usageStore';
32
+
33
+ // Actions
34
+ export {
35
+ // Melbourne formatters
36
+ formatDateMelbourne,
37
+ formatDateTimeMelbourne,
38
+ formatDateShort,
39
+ getMelbourneDateString,
40
+ // URL sync
41
+ syncFromURL,
42
+ syncToURL,
43
+ // Data fetching
44
+ fetchDailyData,
45
+ // Subscriptions
46
+ initSubscriptions,
47
+ destroySubscriptions,
48
+ // UI actions
49
+ setPeriod,
50
+ setProject,
51
+ setCustomDateRange,
52
+ selectDate,
53
+ clearSelection,
54
+ resetFilters,
55
+ } from './usageActions';