@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,946 @@
1
+ ---
2
+ /**
3
+ * CostTable.astro
4
+ *
5
+ * Sortable table for daily cost breakdown.
6
+ * Subscribes to nanostores for reactive data updates.
7
+ * Part of task-20.1: Phase 2.3 - Create CostTable.astro with improved styling
8
+ *
9
+ * Features:
10
+ * - Sortable columns (click header)
11
+ * - Search/filter input
12
+ * - CSV export
13
+ * - Melbourne timezone formatting
14
+ * - Reactive updates via nanostores
15
+ */
16
+ import type { ResourceType } from '../usage-colors';
17
+
18
+ // Generate unique table ID
19
+ const tableId = `cost-table-${Math.random().toString(36).slice(2, 9)}`;
20
+
21
+ // Resource columns
22
+ const resourceColumns: { key: ResourceType; label: string; short: string }[] = [
23
+ { key: 'workers', label: 'Workers', short: 'WK' },
24
+ { key: 'd1', label: 'D1', short: 'D1' },
25
+ { key: 'kv', label: 'KV', short: 'KV' },
26
+ { key: 'r2', label: 'R2', short: 'R2' },
27
+ { key: 'vectorize', label: 'Vectorize', short: 'VZ' },
28
+ { key: 'aiGateway', label: 'AI Gateway', short: 'AI' },
29
+ { key: 'durableObjects', label: 'Durable Objects', short: 'DO' },
30
+ { key: 'workersAI', label: 'Workers AI', short: 'WAI' },
31
+ { key: 'queues', label: 'Queues', short: 'Q' },
32
+ ];
33
+ ---
34
+
35
+ <div
36
+ class="cost-table-container"
37
+ data-component="cost-table"
38
+ id={tableId}
39
+ data-table-id={tableId}
40
+ data-columns={JSON.stringify(resourceColumns)}
41
+ >
42
+ <!-- Controls -->
43
+ <div class="table-controls">
44
+ <div class="search-box">
45
+ <svg class="search-icon" viewBox="0 0 16 16" fill="currentColor">
46
+ <path
47
+ fill-rule="evenodd"
48
+ d="M11.5 7a4.5 4.5 0 11-9 0 4.5 4.5 0 019 0zm-.82 4.74a6 6 0 111.06-1.06l3.04 3.04a.75.75 0 11-1.06 1.06l-3.04-3.04z"
49
+ ></path>
50
+ </svg>
51
+ <input
52
+ type="text"
53
+ class="search-input"
54
+ placeholder="Filter by date..."
55
+ data-action="search"
56
+ />
57
+ </div>
58
+
59
+ <div class="selection-badge" id={`${tableId}-badge`} hidden>
60
+ <span class="badge-text" id={`${tableId}-badge-text`}></span>
61
+ <button type="button" class="badge-clear" data-action="clear">
62
+ <svg viewBox="0 0 16 16" fill="currentColor">
63
+ <path
64
+ d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"
65
+ ></path>
66
+ </svg>
67
+ </button>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Table -->
72
+ <div class="table-scroll">
73
+ <table class="data-table">
74
+ <thead>
75
+ <tr>
76
+ <th class="sortable date-col" data-sort="date" data-order="desc">
77
+ <span class="th-inner">
78
+ Date
79
+ <svg class="sort-arrow active desc" viewBox="0 0 16 16" fill="currentColor">
80
+ <path
81
+ d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z"
82
+ ></path>
83
+ </svg>
84
+ </span>
85
+ </th>
86
+ {
87
+ resourceColumns.map((col) => (
88
+ <th class="sortable num-col" data-sort={col.key}>
89
+ <span class="th-inner">
90
+ <span class="label-full">{col.label}</span>
91
+ <span class="label-short">{col.short}</span>
92
+ <svg class="sort-arrow" viewBox="0 0 16 16" fill="currentColor">
93
+ <path d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z" />
94
+ </svg>
95
+ </span>
96
+ </th>
97
+ ))
98
+ }
99
+ <th class="sortable num-col total-col" data-sort="total">
100
+ <span class="th-inner">
101
+ Total
102
+ <svg class="sort-arrow" viewBox="0 0 16 16" fill="currentColor">
103
+ <path
104
+ d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z"
105
+ ></path>
106
+ </svg>
107
+ </span>
108
+ </th>
109
+ </tr>
110
+ </thead>
111
+ <tbody id={`${tableId}-body`}>
112
+ <tr class="empty-row visible">
113
+ <td colspan="12" class="empty-cell">
114
+ <span class="empty-text">Loading data...</span>
115
+ </td>
116
+ </tr>
117
+ </tbody>
118
+ <tfoot id={`${tableId}-foot`}>
119
+ <tr class="totals-row">
120
+ <td class="totals-label">Total</td>
121
+ <td class="num-col" colspan="10">-</td>
122
+ </tr>
123
+ </tfoot>
124
+ </table>
125
+ </div>
126
+
127
+ <!-- Footer -->
128
+ <div class="table-footer">
129
+ <span class="row-count" id={`${tableId}-count`}>0 rows</span>
130
+ <button type="button" class="export-btn" data-action="export">
131
+ <svg viewBox="0 0 16 16" fill="currentColor">
132
+ <path
133
+ d="M2.75 14A1.75 1.75 0 011 12.25v-2.5a.75.75 0 011.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25v-2.5a.75.75 0 011.5 0v2.5A1.75 1.75 0 0113.25 14H2.75z"
134
+ ></path>
135
+ <path
136
+ d="M7.25 7.689V2a.75.75 0 011.5 0v5.689l1.97-1.969a.75.75 0 111.06 1.06l-3.25 3.25a.75.75 0 01-1.06 0L4.22 6.78a.75.75 0 111.06-1.06l1.97 1.969z"
137
+ ></path>
138
+ </svg>
139
+ CSV
140
+ </button>
141
+ </div>
142
+ </div>
143
+
144
+ <style>
145
+ .cost-table-container {
146
+ display: flex;
147
+ flex-direction: column;
148
+ height: 100%;
149
+ min-height: 280px;
150
+ }
151
+
152
+ /* Controls */
153
+ .table-controls {
154
+ display: flex;
155
+ align-items: center;
156
+ gap: var(--usage-spacing-sm);
157
+ padding: var(--usage-spacing-sm) var(--usage-spacing-md);
158
+ border-bottom: 1px solid var(--usage-border-subtle);
159
+ }
160
+
161
+ .search-box {
162
+ position: relative;
163
+ flex: 1;
164
+ max-width: 200px;
165
+ }
166
+
167
+ .search-icon {
168
+ position: absolute;
169
+ left: 10px;
170
+ top: 50%;
171
+ transform: translateY(-50%);
172
+ width: 12px;
173
+ height: 12px;
174
+ color: var(--usage-text-muted);
175
+ pointer-events: none;
176
+ }
177
+
178
+ .search-input {
179
+ width: 100%;
180
+ padding: 6px 10px 6px 28px;
181
+ background: var(--usage-bg-card);
182
+ border: 1px solid var(--usage-border-subtle);
183
+ border-radius: var(--usage-radius-sm);
184
+ font-size: 0.75rem;
185
+ color: var(--usage-text-primary);
186
+ transition:
187
+ border-color 0.15s ease,
188
+ box-shadow 0.15s ease;
189
+ }
190
+
191
+ .search-input:focus {
192
+ outline: none;
193
+ border-color: var(--usage-accent-blue, #0969da);
194
+ box-shadow: 0 0 0 2px rgba(9, 105, 218, 0.15);
195
+ }
196
+
197
+ .search-input::placeholder {
198
+ color: var(--usage-text-muted);
199
+ }
200
+
201
+ .selection-badge {
202
+ display: flex;
203
+ align-items: center;
204
+ gap: 4px;
205
+ padding: 4px 8px;
206
+ background: var(--usage-accent-blue, #0969da);
207
+ border-radius: var(--usage-radius-sm);
208
+ color: white;
209
+ font-size: 0.6875rem;
210
+ font-weight: 500;
211
+ }
212
+
213
+ .selection-badge[hidden] {
214
+ display: none;
215
+ }
216
+
217
+ .badge-clear {
218
+ display: flex;
219
+ align-items: center;
220
+ justify-content: center;
221
+ width: 14px;
222
+ height: 14px;
223
+ padding: 0;
224
+ background: rgba(255, 255, 255, 0.25);
225
+ border: none;
226
+ border-radius: 2px;
227
+ color: white;
228
+ cursor: pointer;
229
+ transition: background 0.1s ease;
230
+ }
231
+
232
+ .badge-clear:hover {
233
+ background: rgba(255, 255, 255, 0.4);
234
+ }
235
+
236
+ .badge-clear svg {
237
+ width: 10px;
238
+ height: 10px;
239
+ }
240
+
241
+ /* Table scroll */
242
+ .table-scroll {
243
+ flex: 1;
244
+ overflow: auto;
245
+ -webkit-overflow-scrolling: touch;
246
+ }
247
+
248
+ /* Data table */
249
+ .data-table {
250
+ width: 100%;
251
+ border-collapse: collapse;
252
+ font-size: 0.75rem;
253
+ }
254
+
255
+ .data-table th {
256
+ padding: 8px 10px;
257
+ background: var(--usage-bg-tertiary, var(--usage-bg-secondary));
258
+ font-weight: 600;
259
+ color: var(--usage-text-secondary);
260
+ text-align: left;
261
+ white-space: nowrap;
262
+ border-bottom: 1px solid var(--usage-border-subtle);
263
+ position: sticky;
264
+ top: 0;
265
+ z-index: 1;
266
+ }
267
+
268
+ .data-table th.num-col {
269
+ text-align: right;
270
+ }
271
+
272
+ .data-table th.sortable {
273
+ cursor: pointer;
274
+ user-select: none;
275
+ transition: background 0.1s ease;
276
+ }
277
+
278
+ .data-table th.sortable:hover {
279
+ background: var(--usage-bg-hover, var(--usage-bg-secondary));
280
+ }
281
+
282
+ .th-inner {
283
+ display: inline-flex;
284
+ align-items: center;
285
+ gap: 3px;
286
+ }
287
+
288
+ .label-short {
289
+ display: none;
290
+ }
291
+
292
+ .sort-arrow {
293
+ width: 10px;
294
+ height: 10px;
295
+ opacity: 0.25;
296
+ transition:
297
+ opacity 0.1s ease,
298
+ transform 0.15s ease;
299
+ }
300
+
301
+ .sort-arrow.active {
302
+ opacity: 1;
303
+ }
304
+
305
+ .sort-arrow.asc {
306
+ transform: rotate(180deg);
307
+ }
308
+
309
+ .data-table td {
310
+ padding: 8px 10px;
311
+ border-bottom: 1px solid var(--usage-border-subtle);
312
+ color: var(--usage-text-primary);
313
+ font-variant-numeric: tabular-nums;
314
+ }
315
+
316
+ .data-table td.num-col {
317
+ text-align: right;
318
+ font-family: var(--usage-font-mono, 'JetBrains Mono', monospace);
319
+ font-size: 0.6875rem;
320
+ }
321
+
322
+ .data-table tr.data-row {
323
+ transition: background 0.1s ease;
324
+ }
325
+
326
+ .data-table tr.data-row:hover {
327
+ background: var(--usage-bg-hover, rgba(0, 0, 0, 0.02));
328
+ }
329
+
330
+ .data-table tr.data-row.selected {
331
+ background: rgba(9, 105, 218, 0.08);
332
+ }
333
+
334
+ .data-table tr.data-row.hidden {
335
+ display: none;
336
+ }
337
+
338
+ .date-cell {
339
+ font-weight: 500;
340
+ white-space: nowrap;
341
+ }
342
+
343
+ .cost-val {
344
+ display: inline-block;
345
+ }
346
+
347
+ .cost-val.zero {
348
+ color: var(--usage-text-muted);
349
+ }
350
+
351
+ .cost-val.total {
352
+ font-weight: 600;
353
+ }
354
+
355
+ .total-col {
356
+ background: rgba(0, 0, 0, 0.02);
357
+ }
358
+
359
+ :global([data-theme='dark']) .total-col {
360
+ background: rgba(255, 255, 255, 0.02);
361
+ }
362
+
363
+ /* Totals row */
364
+ .totals-row {
365
+ background: var(--usage-bg-tertiary, var(--usage-bg-secondary));
366
+ font-weight: 600;
367
+ }
368
+
369
+ .totals-row td {
370
+ padding: 10px;
371
+ border-bottom: none;
372
+ }
373
+
374
+ .totals-label {
375
+ font-weight: 600;
376
+ color: var(--usage-text-primary);
377
+ }
378
+
379
+ .grand-total {
380
+ font-weight: 700;
381
+ color: var(--usage-accent-blue, #0969da);
382
+ font-size: 0.8125rem;
383
+ }
384
+
385
+ /* Empty state */
386
+ .empty-row {
387
+ display: none;
388
+ }
389
+
390
+ .empty-row.visible {
391
+ display: table-row;
392
+ }
393
+
394
+ .empty-cell {
395
+ text-align: center;
396
+ padding: var(--usage-spacing-xl) !important;
397
+ }
398
+
399
+ .empty-text {
400
+ color: var(--usage-text-muted);
401
+ font-size: 0.8125rem;
402
+ }
403
+
404
+ /* Footer */
405
+ .table-footer {
406
+ display: flex;
407
+ align-items: center;
408
+ justify-content: space-between;
409
+ padding: var(--usage-spacing-xs) var(--usage-spacing-md);
410
+ border-top: 1px solid var(--usage-border-subtle);
411
+ background: var(--usage-bg-tertiary, var(--usage-bg-secondary));
412
+ }
413
+
414
+ .row-count {
415
+ font-size: 0.6875rem;
416
+ font-family: var(--usage-font-mono, monospace);
417
+ color: var(--usage-text-muted);
418
+ }
419
+
420
+ .export-btn {
421
+ display: inline-flex;
422
+ align-items: center;
423
+ gap: 4px;
424
+ padding: 4px 8px;
425
+ background: transparent;
426
+ border: 1px solid var(--usage-border-subtle);
427
+ border-radius: var(--usage-radius-sm);
428
+ font-size: 0.6875rem;
429
+ font-weight: 500;
430
+ color: var(--usage-text-secondary);
431
+ cursor: pointer;
432
+ transition:
433
+ background 0.1s ease,
434
+ border-color 0.1s ease,
435
+ color 0.1s ease;
436
+ }
437
+
438
+ .export-btn:hover {
439
+ background: var(--usage-bg-hover);
440
+ border-color: var(--usage-border-default);
441
+ color: var(--usage-text-primary);
442
+ }
443
+
444
+ .export-btn svg {
445
+ width: 12px;
446
+ height: 12px;
447
+ }
448
+
449
+ /* Responsive */
450
+ @media (max-width: 900px) {
451
+ .label-full {
452
+ display: none;
453
+ }
454
+
455
+ .label-short {
456
+ display: inline;
457
+ }
458
+
459
+ .data-table th,
460
+ .data-table td {
461
+ padding: 6px 4px;
462
+ }
463
+ }
464
+
465
+ /* Mobile: Condensed view with primary metrics only */
466
+ @media (max-width: 640px) {
467
+ .table-controls {
468
+ padding: var(--usage-spacing-xs) var(--usage-spacing-sm);
469
+ }
470
+
471
+ .search-box {
472
+ max-width: none;
473
+ }
474
+
475
+ .data-table {
476
+ font-size: 0.6875rem;
477
+ }
478
+
479
+ .data-table th,
480
+ .data-table td {
481
+ padding: 4px 2px;
482
+ }
483
+
484
+ .data-table td.num-col {
485
+ font-size: 0.625rem;
486
+ }
487
+
488
+ /* Hide less critical columns on mobile */
489
+ .data-table th:nth-child(n + 6):not(:last-child),
490
+ .data-table td:nth-child(n + 6):not(:last-child) {
491
+ display: none;
492
+ }
493
+
494
+ .date-cell {
495
+ font-size: 0.6875rem;
496
+ }
497
+
498
+ .table-footer {
499
+ padding: var(--usage-spacing-xs);
500
+ flex-direction: column;
501
+ gap: 0.5rem;
502
+ align-items: flex-start;
503
+ }
504
+
505
+ .export-btn {
506
+ width: 100%;
507
+ justify-content: center;
508
+ }
509
+ }
510
+
511
+ /* Extra small mobile */
512
+ @media (max-width: 480px) {
513
+ .cost-table-container {
514
+ min-height: 200px;
515
+ }
516
+
517
+ .data-table {
518
+ font-size: 0.625rem;
519
+ }
520
+
521
+ .data-table th,
522
+ .data-table td {
523
+ padding: 3px 2px;
524
+ }
525
+
526
+ /* Show only Date, Workers, D1, and Total on very small screens */
527
+ .data-table th:nth-child(n + 4):not(:last-child),
528
+ .data-table td:nth-child(n + 4):not(:last-child) {
529
+ display: none;
530
+ }
531
+
532
+ .label-short {
533
+ font-size: 0.5625rem;
534
+ }
535
+ }
536
+
537
+ /* Dark mode overrides */
538
+ :global([data-theme='dark']) .cost-table-container,
539
+ :global(.dark) .cost-table-container {
540
+ background: #21262d;
541
+ }
542
+
543
+ :global([data-theme='dark']) .table-controls,
544
+ :global(.dark) .table-controls {
545
+ background: #21262d;
546
+ border-color: #30363d;
547
+ }
548
+
549
+ :global([data-theme='dark']) .search-input,
550
+ :global(.dark) .search-input {
551
+ background: #161b22;
552
+ border-color: #30363d;
553
+ color: #e6edf3;
554
+ }
555
+
556
+ :global([data-theme='dark']) .search-input::placeholder,
557
+ :global(.dark) .search-input::placeholder {
558
+ color: #484f58;
559
+ }
560
+
561
+ :global([data-theme='dark']) .search-icon,
562
+ :global(.dark) .search-icon {
563
+ color: #484f58;
564
+ }
565
+
566
+ :global([data-theme='dark']) .data-table th,
567
+ :global(.dark) .data-table th {
568
+ background: #161b22;
569
+ color: #8b949e;
570
+ border-color: #21262d;
571
+ }
572
+
573
+ :global([data-theme='dark']) .data-table th.sortable:hover,
574
+ :global(.dark) .data-table th.sortable:hover {
575
+ background: #21262d;
576
+ }
577
+
578
+ :global([data-theme='dark']) .data-table td,
579
+ :global(.dark) .data-table td {
580
+ color: #e6edf3;
581
+ border-color: #21262d;
582
+ }
583
+
584
+ :global([data-theme='dark']) .data-table tr.data-row:hover,
585
+ :global(.dark) .data-table tr.data-row:hover {
586
+ background: rgba(56, 139, 253, 0.1);
587
+ }
588
+
589
+ :global([data-theme='dark']) .data-table tr.data-row.selected,
590
+ :global(.dark) .data-table tr.data-row.selected {
591
+ background: rgba(56, 139, 253, 0.15);
592
+ }
593
+
594
+ :global([data-theme='dark']) .cost-val.zero,
595
+ :global(.dark) .cost-val.zero {
596
+ color: #484f58;
597
+ }
598
+
599
+ :global([data-theme='dark']) .totals-row,
600
+ :global(.dark) .totals-row {
601
+ background: #161b22;
602
+ }
603
+
604
+ :global([data-theme='dark']) .totals-label,
605
+ :global(.dark) .totals-label {
606
+ color: #e6edf3;
607
+ }
608
+
609
+ :global([data-theme='dark']) .grand-total,
610
+ :global(.dark) .grand-total {
611
+ color: #58a6ff;
612
+ }
613
+
614
+ :global([data-theme='dark']) .table-footer,
615
+ :global(.dark) .table-footer {
616
+ background: #161b22;
617
+ border-color: #21262d;
618
+ }
619
+
620
+ :global([data-theme='dark']) .row-count,
621
+ :global(.dark) .row-count {
622
+ color: #8b949e;
623
+ }
624
+
625
+ :global([data-theme='dark']) .export-btn,
626
+ :global(.dark) .export-btn {
627
+ color: #8b949e;
628
+ border-color: #30363d;
629
+ }
630
+
631
+ :global([data-theme='dark']) .export-btn:hover,
632
+ :global(.dark) .export-btn:hover {
633
+ background: #21262d;
634
+ border-color: #484f58;
635
+ color: #e6edf3;
636
+ }
637
+
638
+ :global([data-theme='dark']) .empty-text,
639
+ :global(.dark) .empty-text {
640
+ color: #8b949e;
641
+ }
642
+ </style>
643
+
644
+ <script>
645
+ import { $dailyData, $selectedDate, $timezone } from '../state/usageStore';
646
+
647
+ // Get tableId and resourceColumns from data attributes
648
+ const containerEl = document.querySelector('[data-component="cost-table"]');
649
+ const tableId = containerEl?.dataset.tableId;
650
+ const cols = containerEl?.dataset.columns ? JSON.parse(containerEl.dataset.columns) : [];
651
+
652
+ if (!tableId) {
653
+ console.error('CostTable: Missing table ID');
654
+ }
655
+
656
+ const container = tableId ? document.getElementById(tableId) : null;
657
+ if (!container) console.error('CostTable container not found');
658
+
659
+ const tbody = document.getElementById(`${tableId}-body`);
660
+ const tfoot = document.getElementById(`${tableId}-foot`);
661
+ const countEl = document.getElementById(`${tableId}-count`);
662
+ const badge = document.getElementById(`${tableId}-badge`);
663
+ const badgeText = document.getElementById(`${tableId}-badge-text`);
664
+ const searchInput = container.querySelector('[data-action="search"]');
665
+ const clearBtn = container.querySelector('[data-action="clear"]');
666
+ const exportBtn = container.querySelector('[data-action="export"]');
667
+ const headers = container.querySelectorAll('th.sortable');
668
+
669
+ let currentSort = { key: 'date', order: 'desc' };
670
+ let searchTerm = '';
671
+
672
+ // Format cost value
673
+ function fmt(v) {
674
+ if (v === 0) return '-';
675
+ return '$' + v.toFixed(v < 0.01 ? 4 : 2);
676
+ }
677
+
678
+ // Format date for Melbourne
679
+ function fmtDate(dateStr) {
680
+ const date = new Date(dateStr);
681
+ return date.toLocaleDateString('en-AU', {
682
+ timeZone: $timezone.get(),
683
+ weekday: 'short',
684
+ day: 'numeric',
685
+ month: 'short',
686
+ });
687
+ }
688
+
689
+ // Create a cost value span
690
+ function createCostSpan(value, extraClass) {
691
+ const span = document.createElement('span');
692
+ span.className =
693
+ 'cost-val' + (value === 0 ? ' zero' : '') + (extraClass ? ' ' + extraClass : '');
694
+ span.textContent = fmt(value);
695
+ return span;
696
+ }
697
+
698
+ // Clear all children from an element
699
+ function clearChildren(el) {
700
+ while (el.firstChild) {
701
+ el.removeChild(el.firstChild);
702
+ }
703
+ }
704
+
705
+ // Render empty state
706
+ function renderEmptyState() {
707
+ clearChildren(tbody);
708
+ const tr = document.createElement('tr');
709
+ tr.className = 'empty-row visible';
710
+ const td = document.createElement('td');
711
+ td.colSpan = 12;
712
+ td.className = 'empty-cell';
713
+ const span = document.createElement('span');
714
+ span.className = 'empty-text';
715
+ span.textContent = 'No data available';
716
+ td.appendChild(span);
717
+ tr.appendChild(td);
718
+ tbody.appendChild(tr);
719
+
720
+ clearChildren(tfoot);
721
+ const footTr = document.createElement('tr');
722
+ footTr.className = 'totals-row';
723
+ const labelTd = document.createElement('td');
724
+ labelTd.className = 'totals-label';
725
+ labelTd.textContent = 'Total';
726
+ footTr.appendChild(labelTd);
727
+ const emptyTd = document.createElement('td');
728
+ emptyTd.className = 'num-col';
729
+ emptyTd.colSpan = 10;
730
+ emptyTd.textContent = '-';
731
+ footTr.appendChild(emptyTd);
732
+ tfoot.appendChild(footTr);
733
+
734
+ countEl.textContent = '0 rows';
735
+ }
736
+
737
+ // Render table rows using safe DOM methods
738
+ function renderRows() {
739
+ const data = $dailyData.get();
740
+ const selected = $selectedDate.get();
741
+
742
+ if (data.length === 0) {
743
+ renderEmptyState();
744
+ return;
745
+ }
746
+
747
+ // Sort data
748
+ const sorted = [...data].sort((a, b) => {
749
+ const key = currentSort.key;
750
+ const aVal = key === 'date' ? a.date : a[key] || 0;
751
+ const bVal = key === 'date' ? b.date : b[key] || 0;
752
+
753
+ if (currentSort.order === 'asc') {
754
+ return aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
755
+ }
756
+ return aVal > bVal ? -1 : aVal < bVal ? 1 : 0;
757
+ });
758
+
759
+ // Clear and rebuild tbody
760
+ clearChildren(tbody);
761
+
762
+ sorted.forEach((row) => {
763
+ const isSelected = row.date === selected;
764
+ const dateFormatted = fmtDate(row.date);
765
+ const matchesSearch = !searchTerm || dateFormatted.toLowerCase().includes(searchTerm);
766
+
767
+ const tr = document.createElement('tr');
768
+ tr.className =
769
+ 'data-row' + (isSelected ? ' selected' : '') + (!matchesSearch ? ' hidden' : '');
770
+ tr.dataset.date = row.date;
771
+
772
+ // Date cell
773
+ const dateCell = document.createElement('td');
774
+ dateCell.className = 'date-cell';
775
+ dateCell.textContent = dateFormatted;
776
+ tr.appendChild(dateCell);
777
+
778
+ // Resource cells
779
+ cols.forEach((c) => {
780
+ const td = document.createElement('td');
781
+ td.className = 'num-col';
782
+ td.appendChild(createCostSpan(row[c.key] || 0));
783
+ tr.appendChild(td);
784
+ });
785
+
786
+ // Total cell
787
+ const totalTd = document.createElement('td');
788
+ totalTd.className = 'num-col total-col';
789
+ totalTd.appendChild(createCostSpan(row.total, 'total'));
790
+ tr.appendChild(totalTd);
791
+
792
+ tbody.appendChild(tr);
793
+ });
794
+
795
+ // Calculate and render totals
796
+ const totals = data.reduce((acc, d) => {
797
+ cols.forEach((c) => {
798
+ acc[c.key] = (acc[c.key] || 0) + (d[c.key] || 0);
799
+ });
800
+ acc.total = (acc.total || 0) + d.total;
801
+ return acc;
802
+ }, {});
803
+
804
+ clearChildren(tfoot);
805
+ const footTr = document.createElement('tr');
806
+ footTr.className = 'totals-row';
807
+
808
+ const labelTd = document.createElement('td');
809
+ labelTd.className = 'totals-label';
810
+ labelTd.textContent = 'Total';
811
+ footTr.appendChild(labelTd);
812
+
813
+ cols.forEach((c) => {
814
+ const td = document.createElement('td');
815
+ td.className = 'num-col';
816
+ td.appendChild(createCostSpan(totals[c.key] || 0));
817
+ footTr.appendChild(td);
818
+ });
819
+
820
+ const grandTotalTd = document.createElement('td');
821
+ grandTotalTd.className = 'num-col total-col';
822
+ grandTotalTd.appendChild(createCostSpan(totals.total, 'grand-total'));
823
+ footTr.appendChild(grandTotalTd);
824
+
825
+ tfoot.appendChild(footTr);
826
+
827
+ // Update count
828
+ const visibleCount = sorted.filter(
829
+ (r) => !searchTerm || fmtDate(r.date).toLowerCase().includes(searchTerm)
830
+ ).length;
831
+ countEl.textContent = `${visibleCount} of ${data.length} rows`;
832
+
833
+ // Update badge
834
+ if (selected) {
835
+ badge.hidden = false;
836
+ badgeText.textContent = fmtDate(selected);
837
+ } else {
838
+ badge.hidden = true;
839
+ }
840
+ }
841
+
842
+ // Apply search filter
843
+ function applySearch() {
844
+ const rows = tbody.querySelectorAll('.data-row');
845
+ let visible = 0;
846
+
847
+ rows.forEach((row) => {
848
+ const dateCell = row.querySelector('.date-cell');
849
+ const matches = !searchTerm || dateCell.textContent.toLowerCase().includes(searchTerm);
850
+ row.classList.toggle('hidden', !matches);
851
+ if (matches) visible++;
852
+ });
853
+
854
+ const total = $dailyData.get().length;
855
+ countEl.textContent = `${visible} of ${total} rows`;
856
+ }
857
+
858
+ // Update sort visuals
859
+ function updateSortVisuals() {
860
+ headers.forEach((h) => {
861
+ const icon = h.querySelector('.sort-arrow');
862
+ if (h.dataset.sort === currentSort.key) {
863
+ h.dataset.order = currentSort.order;
864
+ icon.classList.add('active', currentSort.order);
865
+ icon.classList.remove(currentSort.order === 'asc' ? 'desc' : 'asc');
866
+ } else {
867
+ h.dataset.order = '';
868
+ icon.classList.remove('active', 'asc', 'desc');
869
+ }
870
+ });
871
+ }
872
+
873
+ // Export CSV
874
+ function exportCSV() {
875
+ const data = $dailyData.get();
876
+ if (data.length === 0) return;
877
+
878
+ const header = ['Date', ...cols.map((c) => c.label), 'Total'].join(',');
879
+ const rows = data.map((r) =>
880
+ [r.date, ...cols.map((c) => (r[c.key] || 0).toFixed(4)), r.total.toFixed(4)].join(',')
881
+ );
882
+
883
+ const totals = data.reduce((acc, d) => {
884
+ cols.forEach((c) => {
885
+ acc[c.key] = (acc[c.key] || 0) + (d[c.key] || 0);
886
+ });
887
+ acc.total = (acc.total || 0) + d.total;
888
+ return acc;
889
+ }, {});
890
+
891
+ rows.push(
892
+ ['TOTAL', ...cols.map((c) => (totals[c.key] || 0).toFixed(4)), totals.total.toFixed(4)].join(
893
+ ','
894
+ )
895
+ );
896
+
897
+ const csv = [header, ...rows].join('\n');
898
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
899
+ const url = URL.createObjectURL(blob);
900
+ const a = document.createElement('a');
901
+ a.href = url;
902
+ a.download = `cloudflare-costs-${new Date().toISOString().split('T')[0]}.csv`;
903
+ document.body.appendChild(a);
904
+ a.click();
905
+ document.body.removeChild(a);
906
+ URL.revokeObjectURL(url);
907
+ }
908
+
909
+ // Event handlers
910
+ searchInput?.addEventListener('input', (e) => {
911
+ searchTerm = e.target.value.toLowerCase();
912
+ applySearch();
913
+ });
914
+
915
+ clearBtn?.addEventListener('click', () => {
916
+ $selectedDate.set(null);
917
+ });
918
+
919
+ exportBtn?.addEventListener('click', exportCSV);
920
+
921
+ headers.forEach((h) => {
922
+ h.addEventListener('click', () => {
923
+ const key = h.dataset.sort;
924
+ if (currentSort.key === key) {
925
+ currentSort.order = currentSort.order === 'desc' ? 'asc' : 'desc';
926
+ } else {
927
+ currentSort = { key, order: 'desc' };
928
+ }
929
+ updateSortVisuals();
930
+ renderRows();
931
+ });
932
+ });
933
+
934
+ // Subscribe to store changes
935
+ $dailyData.subscribe(() => {
936
+ renderRows();
937
+ });
938
+
939
+ $selectedDate.subscribe(() => {
940
+ renderRows();
941
+ });
942
+
943
+ // Initial render
944
+ renderRows();
945
+ updateSortVisuals();
946
+ </script>