@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.
- package/README.md +2 -5
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +121 -3
- package/package.json +1 -1
- package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
- package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
- package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
- package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
- package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
- package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
- package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
- package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
- package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
- package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
- package/templates/full/dashboard/src/pages/feedback.astro +365 -0
- package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
- package/templates/full/dashboard/src/pages/map.astro +561 -0
- package/templates/full/dashboard/src/pages/revenue.astro +72 -0
- package/templates/full/dashboard/src/pages/tests.astro +431 -0
- package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
- package/templates/full/scripts/ops/verify-account-total.ts +256 -0
- package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
- package/templates/full/tests/integration/r2-archive.test.ts +108 -0
- package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
- package/templates/shared/.github/workflows/validate-controls.yml +27 -0
- package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
- package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
- package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
- package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
- package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
- package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
- package/templates/shared/dashboard/src/components/Toast.astro +170 -0
- package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
- package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
- package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
- package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
- package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
- package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
- package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
- package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
- package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
- package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
- package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
- package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
- package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
- package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
- package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
- package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
- package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
- package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
- package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
- package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
- package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
- package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
- package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
- package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
- package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
- package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
- package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
- package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
- package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
- package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
- package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
- package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
- package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
- package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
- package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
- package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
- package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
- package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
- package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
- package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
- package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
- package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
- package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
- package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
- package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
- package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
- package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
- package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
- package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
- package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
- package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
- package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
- package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
- package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
- package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
- package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
- package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
- package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
- package/templates/standard/tests/integration/connectors.test.ts +241 -0
- package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
- package/templates/standard/tests/integration/ingestion.test.ts +211 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Healthchecks Status Component
|
|
3
|
+
* Shows Gatus heartbeat statuses with flip history
|
|
4
|
+
*/
|
|
5
|
+
import { useState, useEffect } from 'react';
|
|
6
|
+
import { fetchHealthchecks, fetchHealthcheckFlips, type FlipsResponse } from '../../lib/infrastructure/api';
|
|
7
|
+
import type { HealthcheckJob } from '../../lib/infrastructure/types';
|
|
8
|
+
|
|
9
|
+
const statusColours: Record<string, string> = {
|
|
10
|
+
up: 'bg-green-500',
|
|
11
|
+
down: 'bg-red-500',
|
|
12
|
+
grace: 'bg-yellow-500',
|
|
13
|
+
paused: 'bg-gray-400',
|
|
14
|
+
new: 'bg-blue-500',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const statusText: Record<string, string> = {
|
|
18
|
+
up: 'Healthy',
|
|
19
|
+
down: 'Down',
|
|
20
|
+
grace: 'Grace Period',
|
|
21
|
+
paused: 'Paused',
|
|
22
|
+
new: 'New',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function formatPeriod(seconds: number): string {
|
|
26
|
+
if (seconds < 60) return `${seconds}s`;
|
|
27
|
+
if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
|
|
28
|
+
if (seconds < 86400) return `${Math.round(seconds / 3600)}h`;
|
|
29
|
+
return `${Math.round(seconds / 86400)}d`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatTimeSince(timestamp: number | null): string {
|
|
33
|
+
if (!timestamp) return 'Never';
|
|
34
|
+
const now = Math.floor(Date.now() / 1000);
|
|
35
|
+
const diff = now - timestamp;
|
|
36
|
+
if (diff < 60) return 'Just now';
|
|
37
|
+
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
38
|
+
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
39
|
+
return `${Math.floor(diff / 86400)}d ago`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function formatTimestamp(timestamp: number): string {
|
|
43
|
+
const date = new Date(timestamp * 1000);
|
|
44
|
+
const now = new Date();
|
|
45
|
+
const isToday = date.toDateString() === now.toDateString();
|
|
46
|
+
|
|
47
|
+
if (isToday) {
|
|
48
|
+
return date.toLocaleTimeString('en-AU', { hour: '2-digit', minute: '2-digit' });
|
|
49
|
+
}
|
|
50
|
+
return date.toLocaleDateString('en-AU', {
|
|
51
|
+
day: 'numeric',
|
|
52
|
+
month: 'short',
|
|
53
|
+
hour: '2-digit',
|
|
54
|
+
minute: '2-digit'
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface FlipHistoryProps {
|
|
59
|
+
checkId: string;
|
|
60
|
+
isExpanded: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function FlipHistory({ checkId, isExpanded }: FlipHistoryProps) {
|
|
64
|
+
const [flipsData, setFlipsData] = useState<FlipsResponse | null>(null);
|
|
65
|
+
const [loading, setLoading] = useState(false);
|
|
66
|
+
const [error, setError] = useState<string | null>(null);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!isExpanded) return;
|
|
70
|
+
|
|
71
|
+
async function loadFlips() {
|
|
72
|
+
setLoading(true);
|
|
73
|
+
setError(null);
|
|
74
|
+
try {
|
|
75
|
+
const data = await fetchHealthcheckFlips(checkId);
|
|
76
|
+
setFlipsData(data);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
setError(e instanceof Error ? e.message : 'Failed to load flips');
|
|
79
|
+
} finally {
|
|
80
|
+
setLoading(false);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
loadFlips();
|
|
85
|
+
}, [checkId, isExpanded]);
|
|
86
|
+
|
|
87
|
+
if (!isExpanded) return null;
|
|
88
|
+
|
|
89
|
+
if (loading) {
|
|
90
|
+
return (
|
|
91
|
+
<div className="px-4 pb-4 pt-0">
|
|
92
|
+
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3 animate-pulse">
|
|
93
|
+
<div className="h-4 bg-gray-200 dark:bg-gray-600 rounded w-32 mb-2"></div>
|
|
94
|
+
<div className="h-3 bg-gray-200 dark:bg-gray-600 rounded w-48"></div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (error) {
|
|
101
|
+
return (
|
|
102
|
+
<div className="px-4 pb-4 pt-0">
|
|
103
|
+
<div className="bg-red-50 dark:bg-red-900/20 rounded-lg p-3 text-sm text-red-600 dark:text-red-400">
|
|
104
|
+
{error}
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!flipsData || flipsData.flips.length === 0) {
|
|
111
|
+
return (
|
|
112
|
+
<div className="px-4 pb-4 pt-0">
|
|
113
|
+
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3 text-sm text-gray-500 dark:text-gray-400">
|
|
114
|
+
No status changes recorded in the last 30 days
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<div className="px-4 pb-4 pt-0">
|
|
122
|
+
<div className="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3">
|
|
123
|
+
<div className="flex items-center justify-between mb-2">
|
|
124
|
+
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
125
|
+
Recent Status Changes
|
|
126
|
+
</h4>
|
|
127
|
+
{flipsData.flipsToday > 0 && (
|
|
128
|
+
<span className="text-xs px-2 py-0.5 bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 rounded-full">
|
|
129
|
+
{flipsData.flipsToday} flip{flipsData.flipsToday !== 1 ? 's' : ''} today
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div className="space-y-1">
|
|
135
|
+
{flipsData.flips.slice(0, 10).map((flip, index) => (
|
|
136
|
+
<div
|
|
137
|
+
key={index}
|
|
138
|
+
className="flex items-center gap-2 text-sm"
|
|
139
|
+
>
|
|
140
|
+
<span className={`w-2 h-2 rounded-full ${flip.up ? 'bg-green-500' : 'bg-red-500'}`}></span>
|
|
141
|
+
<span className={flip.up ? 'text-green-700 dark:text-green-400' : 'text-red-700 dark:text-red-400'}>
|
|
142
|
+
{flip.up ? 'Recovered' : 'Failed'}
|
|
143
|
+
</span>
|
|
144
|
+
<span className="text-gray-500 dark:text-gray-400">
|
|
145
|
+
{formatTimestamp(flip.timestamp)}
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
{flipsData.lastFailure && (
|
|
152
|
+
<div className="mt-2 pt-2 border-t border-gray-200 dark:border-gray-600 text-xs text-gray-500 dark:text-gray-400">
|
|
153
|
+
Last failure: {formatTimeSince(flipsData.lastFailure)}
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function HealthchecksStatus() {
|
|
162
|
+
const [checks, setChecks] = useState<HealthcheckJob[]>([]);
|
|
163
|
+
const [loading, setLoading] = useState(true);
|
|
164
|
+
const [error, setError] = useState<string | null>(null);
|
|
165
|
+
const [expandedCheck, setExpandedCheck] = useState<string | null>(null);
|
|
166
|
+
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
async function load() {
|
|
169
|
+
try {
|
|
170
|
+
const data = await fetchHealthchecks();
|
|
171
|
+
setChecks(data);
|
|
172
|
+
} catch (e) {
|
|
173
|
+
setError(e instanceof Error ? e.message : 'Unknown error');
|
|
174
|
+
} finally {
|
|
175
|
+
setLoading(false);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
load();
|
|
179
|
+
}, []);
|
|
180
|
+
|
|
181
|
+
if (loading) {
|
|
182
|
+
return (
|
|
183
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
184
|
+
<div className="space-y-3">
|
|
185
|
+
{[...Array(3)].map((_, i) => (
|
|
186
|
+
<div key={i} className="animate-pulse flex items-center gap-3">
|
|
187
|
+
<div className="w-3 h-3 rounded-full bg-gray-200 dark:bg-gray-700"></div>
|
|
188
|
+
<div className="flex-1 h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
|
|
189
|
+
</div>
|
|
190
|
+
))}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (error) {
|
|
197
|
+
return (
|
|
198
|
+
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 text-red-700 dark:text-red-300">
|
|
199
|
+
Error loading healthchecks: {error}
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (checks.length === 0) {
|
|
205
|
+
return (
|
|
206
|
+
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-8 text-center">
|
|
207
|
+
<span className="text-4xl mb-2 block">⏰</span>
|
|
208
|
+
<p className="text-gray-600 dark:text-gray-400">No cron jobs configured</p>
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const upCount = checks.filter((c) => c.status === 'up').length;
|
|
214
|
+
const downCount = checks.filter((c) => c.status === 'down').length;
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
|
218
|
+
{/* Summary Header */}
|
|
219
|
+
<div className="p-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
|
220
|
+
<div>
|
|
221
|
+
<span className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
222
|
+
{upCount}/{checks.length} Healthy
|
|
223
|
+
</span>
|
|
224
|
+
{downCount > 0 && (
|
|
225
|
+
<span className="ml-3 text-sm text-red-600 dark:text-red-400">
|
|
226
|
+
{downCount} down
|
|
227
|
+
</span>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
<a
|
|
231
|
+
href="https://status.littlebearapps.com"
|
|
232
|
+
target="_blank"
|
|
233
|
+
rel="noopener noreferrer"
|
|
234
|
+
className="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400"
|
|
235
|
+
>
|
|
236
|
+
Open Status Page
|
|
237
|
+
</a>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
{/* Check List */}
|
|
241
|
+
<div className="divide-y divide-gray-200 dark:divide-gray-700">
|
|
242
|
+
{checks.map((check) => (
|
|
243
|
+
<div key={check.id}>
|
|
244
|
+
<div
|
|
245
|
+
className="p-4 flex items-center gap-4 hover:bg-gray-50 dark:hover:bg-gray-700/50 cursor-pointer"
|
|
246
|
+
onClick={() => setExpandedCheck(expandedCheck === check.id ? null : check.id)}
|
|
247
|
+
>
|
|
248
|
+
<div className={`w-3 h-3 rounded-full ${statusColours[check.status]}`} title={statusText[check.status]}></div>
|
|
249
|
+
<div className="flex-1 min-w-0">
|
|
250
|
+
<div className="flex items-center gap-2">
|
|
251
|
+
<p className="font-medium text-gray-900 dark:text-white truncate">{check.name}</p>
|
|
252
|
+
{/* Show indicator if check has recent activity */}
|
|
253
|
+
{check.status === 'down' && (
|
|
254
|
+
<span className="px-1.5 py-0.5 text-xs bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 rounded">
|
|
255
|
+
DOWN
|
|
256
|
+
</span>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
<div className="flex items-center gap-2 mt-1">
|
|
260
|
+
{check.tags.map((tag) => (
|
|
261
|
+
<span key={tag} className="px-1.5 py-0.5 text-xs bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 rounded">
|
|
262
|
+
{tag}
|
|
263
|
+
</span>
|
|
264
|
+
))}
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
<div className="text-right">
|
|
268
|
+
<p className="text-sm text-gray-700 dark:text-gray-300">
|
|
269
|
+
Every {formatPeriod(check.period)}
|
|
270
|
+
</p>
|
|
271
|
+
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
272
|
+
Last: {formatTimeSince(check.lastPing)}
|
|
273
|
+
</p>
|
|
274
|
+
</div>
|
|
275
|
+
{/* Expand indicator */}
|
|
276
|
+
<svg
|
|
277
|
+
className={`w-5 h-5 text-gray-400 transition-transform ${expandedCheck === check.id ? 'rotate-180' : ''}`}
|
|
278
|
+
fill="none"
|
|
279
|
+
stroke="currentColor"
|
|
280
|
+
viewBox="0 0 24 24"
|
|
281
|
+
>
|
|
282
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
283
|
+
</svg>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
{/* Flip History (expandable) */}
|
|
287
|
+
<FlipHistory checkId={check.id} isExpanded={expandedCheck === check.id} />
|
|
288
|
+
</div>
|
|
289
|
+
))}
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Tabs Component
|
|
3
|
+
* Organises infrastructure content into navigable tabs
|
|
4
|
+
*/
|
|
5
|
+
import { useState, useEffect } from 'react';
|
|
6
|
+
import { InfrastructureStats } from './InfrastructureStats';
|
|
7
|
+
import { ServiceRegistry } from './ServiceRegistry';
|
|
8
|
+
import { UptimeStatus } from './UptimeStatus';
|
|
9
|
+
import { HealthchecksStatus } from './HealthchecksStatus';
|
|
10
|
+
import { AlertHistory } from './AlertHistory';
|
|
11
|
+
|
|
12
|
+
type TabId = 'overview' | 'monitoring' | 'services' | 'alerts';
|
|
13
|
+
|
|
14
|
+
interface Tab {
|
|
15
|
+
id: TabId;
|
|
16
|
+
label: string;
|
|
17
|
+
icon: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const tabs: Tab[] = [
|
|
21
|
+
{ id: 'overview', label: 'Overview', icon: '📊' },
|
|
22
|
+
{ id: 'monitoring', label: 'Monitoring', icon: '📡' },
|
|
23
|
+
{ id: 'services', label: 'Services', icon: '⚙️' },
|
|
24
|
+
{ id: 'alerts', label: 'Alerts', icon: '🔔' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
interface AutomationData {
|
|
28
|
+
cloakpipeErrors: number;
|
|
29
|
+
homeostatTotals: { total_fixes: number; successful_fixes: number };
|
|
30
|
+
gatekeeperTotals: { total_publishes: number; successful_publishes: number };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface Props {
|
|
34
|
+
automationData?: AutomationData;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function InfrastructureTabs({ automationData }: Props) {
|
|
38
|
+
const [activeTab, setActiveTab] = useState<TabId>('overview');
|
|
39
|
+
|
|
40
|
+
// Sync with URL hash
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const hash = window.location.hash.slice(1) as TabId;
|
|
43
|
+
if (hash && tabs.some((t) => t.id === hash)) {
|
|
44
|
+
setActiveTab(hash);
|
|
45
|
+
}
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
const handleTabChange = (tabId: TabId) => {
|
|
49
|
+
setActiveTab(tabId);
|
|
50
|
+
window.history.replaceState(null, '', `#${tabId}`);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="space-y-6">
|
|
55
|
+
{/* Tab Navigation */}
|
|
56
|
+
<div className="border-b border-gray-200 dark:border-gray-700">
|
|
57
|
+
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
|
|
58
|
+
{tabs.map((tab) => (
|
|
59
|
+
<button
|
|
60
|
+
key={tab.id}
|
|
61
|
+
onClick={() => handleTabChange(tab.id)}
|
|
62
|
+
className={`
|
|
63
|
+
whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm
|
|
64
|
+
${
|
|
65
|
+
activeTab === tab.id
|
|
66
|
+
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
|
67
|
+
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
|
68
|
+
}
|
|
69
|
+
`}
|
|
70
|
+
>
|
|
71
|
+
<span className="mr-2">{tab.icon}</span>
|
|
72
|
+
{tab.label}
|
|
73
|
+
</button>
|
|
74
|
+
))}
|
|
75
|
+
</nav>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Tab Content */}
|
|
79
|
+
<div className="min-h-[400px]">
|
|
80
|
+
{activeTab === 'overview' && (
|
|
81
|
+
<OverviewTab
|
|
82
|
+
automationData={automationData}
|
|
83
|
+
onNavigate={handleTabChange}
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
{activeTab === 'monitoring' && <MonitoringTab />}
|
|
87
|
+
{activeTab === 'services' && <ServicesTab />}
|
|
88
|
+
{activeTab === 'alerts' && <AlertsTab />}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Overview Tab - Summary of everything
|
|
95
|
+
function OverviewTab({
|
|
96
|
+
automationData,
|
|
97
|
+
onNavigate,
|
|
98
|
+
}: {
|
|
99
|
+
automationData?: AutomationData;
|
|
100
|
+
onNavigate: (tab: TabId) => void;
|
|
101
|
+
}) {
|
|
102
|
+
return (
|
|
103
|
+
<div className="space-y-6">
|
|
104
|
+
{/* Stats Cards */}
|
|
105
|
+
<InfrastructureStats />
|
|
106
|
+
|
|
107
|
+
{/* Quick Status Cards */}
|
|
108
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
109
|
+
{/* Monitoring Quick View */}
|
|
110
|
+
<QuickViewCard
|
|
111
|
+
title="Monitoring"
|
|
112
|
+
icon="📡"
|
|
113
|
+
onClick={() => onNavigate('monitoring')}
|
|
114
|
+
>
|
|
115
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
116
|
+
View uptime monitors and cron job health
|
|
117
|
+
</p>
|
|
118
|
+
</QuickViewCard>
|
|
119
|
+
|
|
120
|
+
{/* Services Quick View */}
|
|
121
|
+
<QuickViewCard
|
|
122
|
+
title="Services"
|
|
123
|
+
icon="⚙️"
|
|
124
|
+
onClick={() => onNavigate('services')}
|
|
125
|
+
>
|
|
126
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
127
|
+
Browse and filter the service registry
|
|
128
|
+
</p>
|
|
129
|
+
</QuickViewCard>
|
|
130
|
+
|
|
131
|
+
{/* Alerts Quick View */}
|
|
132
|
+
<QuickViewCard
|
|
133
|
+
title="Alerts"
|
|
134
|
+
icon="🔔"
|
|
135
|
+
onClick={() => onNavigate('alerts')}
|
|
136
|
+
>
|
|
137
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
138
|
+
View and acknowledge recent alerts
|
|
139
|
+
</p>
|
|
140
|
+
</QuickViewCard>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{/* Automation Pipeline (Legacy) - Collapsed by default */}
|
|
144
|
+
{automationData && (
|
|
145
|
+
<details className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
|
146
|
+
<summary className="px-4 py-3 cursor-pointer text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">
|
|
147
|
+
Automation Pipeline (CloakPipe/HomeoStat/GateKeeper)
|
|
148
|
+
</summary>
|
|
149
|
+
<div className="px-4 pb-4">
|
|
150
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
151
|
+
<AutomationCard
|
|
152
|
+
title="CloakPipe"
|
|
153
|
+
value={automationData.cloakpipeErrors}
|
|
154
|
+
label="Errors logged (24h)"
|
|
155
|
+
/>
|
|
156
|
+
<AutomationCard
|
|
157
|
+
title="HomeoStat"
|
|
158
|
+
value={`${automationData.homeostatTotals.successful_fixes}/${automationData.homeostatTotals.total_fixes}`}
|
|
159
|
+
label="Successful fixes (24h)"
|
|
160
|
+
/>
|
|
161
|
+
<AutomationCard
|
|
162
|
+
title="GateKeeper"
|
|
163
|
+
value={`${automationData.gatekeeperTotals.successful_publishes}/${automationData.gatekeeperTotals.total_publishes}`}
|
|
164
|
+
label="Successful publishes (24h)"
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</details>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Monitoring Tab - Uptime + Healthchecks
|
|
175
|
+
function MonitoringTab() {
|
|
176
|
+
return (
|
|
177
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
178
|
+
<section>
|
|
179
|
+
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
180
|
+
Uptime & Services
|
|
181
|
+
</h2>
|
|
182
|
+
<UptimeStatus />
|
|
183
|
+
</section>
|
|
184
|
+
|
|
185
|
+
<section>
|
|
186
|
+
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
187
|
+
Cron Jobs & Servers
|
|
188
|
+
</h2>
|
|
189
|
+
<HealthchecksStatus />
|
|
190
|
+
</section>
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Services Tab - Full Service Registry
|
|
196
|
+
function ServicesTab() {
|
|
197
|
+
return (
|
|
198
|
+
<section>
|
|
199
|
+
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
200
|
+
Service Registry
|
|
201
|
+
</h2>
|
|
202
|
+
<ServiceRegistry />
|
|
203
|
+
</section>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Alerts Tab - Alert History
|
|
208
|
+
function AlertsTab() {
|
|
209
|
+
return (
|
|
210
|
+
<section>
|
|
211
|
+
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
|
212
|
+
Alert History
|
|
213
|
+
</h2>
|
|
214
|
+
<AlertHistory />
|
|
215
|
+
</section>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Helper Components
|
|
220
|
+
function QuickViewCard({
|
|
221
|
+
title,
|
|
222
|
+
icon,
|
|
223
|
+
onClick,
|
|
224
|
+
children,
|
|
225
|
+
}: {
|
|
226
|
+
title: string;
|
|
227
|
+
icon: string;
|
|
228
|
+
onClick: () => void;
|
|
229
|
+
children: React.ReactNode;
|
|
230
|
+
}) {
|
|
231
|
+
return (
|
|
232
|
+
<button
|
|
233
|
+
onClick={onClick}
|
|
234
|
+
className="w-full text-left bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 hover:border-blue-500 dark:hover:border-blue-400 transition-colors"
|
|
235
|
+
>
|
|
236
|
+
<div className="flex items-center gap-2 mb-2">
|
|
237
|
+
<span>{icon}</span>
|
|
238
|
+
<h3 className="font-medium text-gray-900 dark:text-white">{title}</h3>
|
|
239
|
+
<span className="ml-auto text-gray-400">→</span>
|
|
240
|
+
</div>
|
|
241
|
+
{children}
|
|
242
|
+
</button>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function AutomationCard({
|
|
247
|
+
title,
|
|
248
|
+
value,
|
|
249
|
+
label,
|
|
250
|
+
}: {
|
|
251
|
+
title: string;
|
|
252
|
+
value: number | string;
|
|
253
|
+
label: string;
|
|
254
|
+
}) {
|
|
255
|
+
return (
|
|
256
|
+
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-3">
|
|
257
|
+
<div className="text-sm font-medium text-gray-600 dark:text-gray-400">
|
|
258
|
+
{title}
|
|
259
|
+
</div>
|
|
260
|
+
<div className="text-xl font-bold mt-1 text-gray-900 dark:text-white">
|
|
261
|
+
{value}
|
|
262
|
+
</div>
|
|
263
|
+
<div className="mt-1 text-xs text-gray-500 dark:text-gray-500">
|
|
264
|
+
{label}
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
import DashboardLayout from '../layouts/DashboardLayout.astro';
|
|
3
|
+
|
|
4
|
+
interface ProductMetricRow {
|
|
5
|
+
source: string;
|
|
6
|
+
metric_type: string;
|
|
7
|
+
value: number;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const runtime = (Astro.locals as App.Locals | undefined)?.runtime;
|
|
12
|
+
const db = runtime?.env?.PLATFORM_DB;
|
|
13
|
+
|
|
14
|
+
const metricsByType: Record<string, number> = {};
|
|
15
|
+
|
|
16
|
+
if (db) {
|
|
17
|
+
const result = await db
|
|
18
|
+
.prepare(
|
|
19
|
+
`SELECT source, metric_type, value, timestamp
|
|
20
|
+
FROM product_metrics
|
|
21
|
+
WHERE source IN ('plausible', 'ga4')
|
|
22
|
+
ORDER BY timestamp DESC
|
|
23
|
+
LIMIT 100`
|
|
24
|
+
)
|
|
25
|
+
.all<ProductMetricRow>();
|
|
26
|
+
|
|
27
|
+
for (const row of result.results ?? []) {
|
|
28
|
+
const key = `${row.source}:${row.metric_type}`;
|
|
29
|
+
if (!(key in metricsByType)) {
|
|
30
|
+
metricsByType[key] = row.value ?? 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const pageviews = metricsByType['plausible:pageviews'] ?? 0;
|
|
36
|
+
const activeUsers = metricsByType['plausible:active_users'] ?? 0;
|
|
37
|
+
const installs = metricsByType['ga4:installs'] ?? 0;
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<DashboardLayout title="Analytics">
|
|
41
|
+
<div class="space-y-6">
|
|
42
|
+
<h2 class="text-3xl font-bold text-gray-900 dark:text-white">Product Analytics</h2>
|
|
43
|
+
|
|
44
|
+
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
|
45
|
+
<div class="metric-card">
|
|
46
|
+
<div class="metric-title">Pageviews</div>
|
|
47
|
+
<div class="metric-value">{pageviews.toLocaleString()}</div>
|
|
48
|
+
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">Last 30 days (Plausible)</div>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="metric-card">
|
|
52
|
+
<div class="metric-title">Active Users</div>
|
|
53
|
+
<div class="metric-value">{activeUsers.toLocaleString()}</div>
|
|
54
|
+
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">Last 30 days (Plausible)</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="metric-card">
|
|
58
|
+
<div class="metric-title">Chrome Installs</div>
|
|
59
|
+
<div class="metric-value">{installs.toLocaleString()}</div>
|
|
60
|
+
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">Last 30 days (GA4)</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</DashboardLayout>
|