@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,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/infrastructure/alerts - Fetch recent alerts from D1
|
|
3
|
+
* Reads from the `notifications` table (the active unified notification system).
|
|
4
|
+
* The legacy `alerts` table exists but has no data.
|
|
5
|
+
*
|
|
6
|
+
* Maps notification fields to the Alert shape that AlertHistory.tsx expects.
|
|
7
|
+
*/
|
|
8
|
+
import type { APIRoute } from 'astro';
|
|
9
|
+
|
|
10
|
+
interface Env {
|
|
11
|
+
PLATFORM_DB?: D1Database;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface NotificationRow {
|
|
15
|
+
id: string;
|
|
16
|
+
category: string;
|
|
17
|
+
priority: string;
|
|
18
|
+
source: string;
|
|
19
|
+
title: string;
|
|
20
|
+
description: string | null;
|
|
21
|
+
project: string | null;
|
|
22
|
+
createdAt: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Map notification priority to alert severity */
|
|
26
|
+
function priorityToSeverity(priority: string): 'info' | 'warning' | 'critical' {
|
|
27
|
+
switch (priority) {
|
|
28
|
+
case 'critical':
|
|
29
|
+
case 'high':
|
|
30
|
+
return 'critical';
|
|
31
|
+
case 'medium':
|
|
32
|
+
return 'warning';
|
|
33
|
+
default:
|
|
34
|
+
return 'info';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const GET: APIRoute = async ({ locals, url }) => {
|
|
39
|
+
const env = locals.runtime?.env as Env | undefined;
|
|
40
|
+
const db = env?.PLATFORM_DB;
|
|
41
|
+
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
|
42
|
+
|
|
43
|
+
if (!db) {
|
|
44
|
+
return new Response(JSON.stringify({ error: 'Database not configured', alerts: [] }), {
|
|
45
|
+
status: 503,
|
|
46
|
+
headers: { 'Content-Type': 'application/json' },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const result = await db
|
|
52
|
+
.prepare(
|
|
53
|
+
`SELECT
|
|
54
|
+
id, category, priority, source, title, description, project,
|
|
55
|
+
created_at AS createdAt
|
|
56
|
+
FROM notifications
|
|
57
|
+
WHERE created_at >= unixepoch() - (30 * 24 * 60 * 60)
|
|
58
|
+
AND (expires_at IS NULL OR expires_at > unixepoch())
|
|
59
|
+
ORDER BY created_at DESC
|
|
60
|
+
LIMIT ?`
|
|
61
|
+
)
|
|
62
|
+
.bind(limit)
|
|
63
|
+
.all<NotificationRow>();
|
|
64
|
+
|
|
65
|
+
const alerts = result.results.map((n) => ({
|
|
66
|
+
id: n.id,
|
|
67
|
+
type: n.category,
|
|
68
|
+
severity: priorityToSeverity(n.priority),
|
|
69
|
+
source: n.project ? `${n.source} (${n.project})` : n.source,
|
|
70
|
+
message: n.description ? `${n.title}: ${n.description}` : n.title,
|
|
71
|
+
createdAt: n.createdAt,
|
|
72
|
+
acknowledgedAt: null,
|
|
73
|
+
resolvedAt: n.category === 'success' ? n.createdAt : null,
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
return new Response(JSON.stringify({ alerts }), {
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('Error fetching alerts:', error);
|
|
81
|
+
return new Response(JSON.stringify({ alerts: [], error: 'Table not found or query failed' }), {
|
|
82
|
+
headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/infrastructure/healthchecks/[id]/flips - Fetch status change history for a Gatus heartbeat
|
|
3
|
+
* Proxies to Gatus API with caching
|
|
4
|
+
*/
|
|
5
|
+
import type { APIRoute } from 'astro';
|
|
6
|
+
|
|
7
|
+
import { fetchGatusEndpoint } from '../../../../../lib/infrastructure/gatus';
|
|
8
|
+
|
|
9
|
+
interface Env {
|
|
10
|
+
PLATFORM_CACHE?: KVNamespace;
|
|
11
|
+
GATUS_API_URL?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface FlipEvent {
|
|
15
|
+
timestamp: number;
|
|
16
|
+
up: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface FlipsResponse {
|
|
20
|
+
flips: FlipEvent[];
|
|
21
|
+
checkId: string;
|
|
22
|
+
cached: boolean;
|
|
23
|
+
flipsToday: number;
|
|
24
|
+
lastFailure: number | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const CACHE_TTL = 300; // 5 minutes
|
|
28
|
+
|
|
29
|
+
export const GET: APIRoute = async ({ params, locals }) => {
|
|
30
|
+
const checkId = params.id;
|
|
31
|
+
if (!checkId) {
|
|
32
|
+
return new Response(JSON.stringify({ error: 'Missing check ID' }), {
|
|
33
|
+
status: 400,
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const env = locals.runtime?.env as Env | undefined;
|
|
39
|
+
const kv = env?.PLATFORM_CACHE;
|
|
40
|
+
const gatusUrl = env?.GATUS_API_URL || 'https://status.littlebearapps.com';
|
|
41
|
+
|
|
42
|
+
const cacheKey = `HEALTHCHECKS_FLIPS:${checkId}`;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
// Check cache first
|
|
46
|
+
if (kv) {
|
|
47
|
+
const cached = await kv.get(cacheKey, 'json');
|
|
48
|
+
if (cached) {
|
|
49
|
+
return new Response(JSON.stringify({ ...cached, cached: true }), {
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Fetch from Gatus
|
|
56
|
+
const endpoint = await fetchGatusEndpoint(gatusUrl, checkId, kv);
|
|
57
|
+
|
|
58
|
+
// Transform Gatus events to flip events (filter out START events)
|
|
59
|
+
const flips: FlipEvent[] = (endpoint.events || [])
|
|
60
|
+
.filter((event) => event.type === 'HEALTHY' || event.type === 'UNHEALTHY')
|
|
61
|
+
.map((event) => ({
|
|
62
|
+
timestamp: Math.floor(new Date(event.timestamp).getTime() / 1000),
|
|
63
|
+
up: event.type === 'HEALTHY',
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
// Sort by timestamp descending (most recent first)
|
|
67
|
+
flips.sort((a, b) => b.timestamp - a.timestamp);
|
|
68
|
+
|
|
69
|
+
// Calculate today's flips
|
|
70
|
+
const todayStart = new Date();
|
|
71
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
72
|
+
const todayStartTs = Math.floor(todayStart.getTime() / 1000);
|
|
73
|
+
const flipsToday = flips.filter((f) => f.timestamp >= todayStartTs).length;
|
|
74
|
+
|
|
75
|
+
// Find last failure
|
|
76
|
+
const lastDownFlip = flips.find((f) => !f.up);
|
|
77
|
+
const lastFailure = lastDownFlip?.timestamp || null;
|
|
78
|
+
|
|
79
|
+
const result: Omit<FlipsResponse, 'cached'> = {
|
|
80
|
+
flips: flips.slice(0, 20), // Return last 20 flips
|
|
81
|
+
checkId,
|
|
82
|
+
flipsToday,
|
|
83
|
+
lastFailure,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Cache the result
|
|
87
|
+
if (kv) {
|
|
88
|
+
await kv.put(cacheKey, JSON.stringify(result), { expirationTtl: CACHE_TTL });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return new Response(JSON.stringify({ ...result, cached: false }), {
|
|
92
|
+
headers: { 'Content-Type': 'application/json' },
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('Error fetching heartbeat events:', error);
|
|
96
|
+
return new Response(
|
|
97
|
+
JSON.stringify({
|
|
98
|
+
error: 'Failed to fetch events',
|
|
99
|
+
flips: [],
|
|
100
|
+
checkId,
|
|
101
|
+
flipsToday: 0,
|
|
102
|
+
lastFailure: null,
|
|
103
|
+
}),
|
|
104
|
+
{
|
|
105
|
+
status: 500,
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/infrastructure/healthchecks - Fetch Gatus heartbeats
|
|
3
|
+
* Proxies to Gatus API with caching
|
|
4
|
+
*/
|
|
5
|
+
import type { APIRoute } from 'astro';
|
|
6
|
+
|
|
7
|
+
import { fetchAllGatusStatuses } from '../../../lib/infrastructure/gatus';
|
|
8
|
+
|
|
9
|
+
interface Env {
|
|
10
|
+
PLATFORM_CACHE?: KVNamespace;
|
|
11
|
+
GATUS_API_URL?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface HealthcheckJob {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
slug: string;
|
|
18
|
+
status: 'up' | 'down' | 'grace' | 'paused' | 'new';
|
|
19
|
+
lastPing: number | null;
|
|
20
|
+
nextPing: number | null;
|
|
21
|
+
period: number;
|
|
22
|
+
grace: number;
|
|
23
|
+
nPings: number;
|
|
24
|
+
tags: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const CACHE_KEY = 'HEALTHCHECKS_JOBS';
|
|
28
|
+
const CACHE_TTL = 300; // 5 minutes
|
|
29
|
+
|
|
30
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
31
|
+
const env = locals.runtime?.env as Env | undefined;
|
|
32
|
+
const kv = env?.PLATFORM_CACHE;
|
|
33
|
+
const gatusUrl = env?.GATUS_API_URL || 'https://status.littlebearapps.com';
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// Check cache first
|
|
37
|
+
if (kv) {
|
|
38
|
+
const cached = await kv.get(CACHE_KEY, 'json');
|
|
39
|
+
if (cached) {
|
|
40
|
+
return new Response(JSON.stringify({ checks: cached, cached: true }), {
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Fetch all statuses from Gatus
|
|
47
|
+
const allStatuses = await fetchAllGatusStatuses(gatusUrl);
|
|
48
|
+
|
|
49
|
+
// Filter to heartbeats and server monitors (heartbeats + hetzner)
|
|
50
|
+
const HEARTBEAT_GROUPS = new Set(['heartbeats', 'hetzner']);
|
|
51
|
+
const heartbeatEndpoints = allStatuses.filter((ep) => HEARTBEAT_GROUPS.has(ep.group));
|
|
52
|
+
|
|
53
|
+
const checks: HealthcheckJob[] = heartbeatEndpoints.map((ep) => {
|
|
54
|
+
const latestResult = ep.results.length > 0 ? ep.results[ep.results.length - 1] : null;
|
|
55
|
+
|
|
56
|
+
// Derive period from average interval between the last few results
|
|
57
|
+
let period = 900; // Default 15m
|
|
58
|
+
if (ep.results.length >= 2) {
|
|
59
|
+
const lastTwo = ep.results.slice(-2);
|
|
60
|
+
const t1 = new Date(lastTwo[0].timestamp).getTime();
|
|
61
|
+
const t2 = new Date(lastTwo[1].timestamp).getTime();
|
|
62
|
+
period = Math.round(Math.abs(t2 - t1) / 1000);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const lastPingTs = latestResult
|
|
66
|
+
? Math.floor(new Date(latestResult.timestamp).getTime() / 1000)
|
|
67
|
+
: null;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
id: ep.key,
|
|
71
|
+
name: ep.name,
|
|
72
|
+
slug: ep.name
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/\s+/g, '-')
|
|
75
|
+
.replace(/[^a-z0-9-]/g, ''),
|
|
76
|
+
status: latestResult ? (latestResult.success ? 'up' : 'down') : 'new',
|
|
77
|
+
lastPing: lastPingTs,
|
|
78
|
+
nextPing: lastPingTs ? lastPingTs + period : null,
|
|
79
|
+
period,
|
|
80
|
+
grace: 0, // Not exposed by Gatus API
|
|
81
|
+
nPings: ep.results.length,
|
|
82
|
+
tags: [],
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Cache the result
|
|
87
|
+
if (kv) {
|
|
88
|
+
await kv.put(CACHE_KEY, JSON.stringify(checks), { expirationTtl: CACHE_TTL });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return new Response(JSON.stringify({ checks, cached: false }), {
|
|
92
|
+
headers: { 'Content-Type': 'application/json' },
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('Error fetching healthchecks:', error);
|
|
96
|
+
return new Response(JSON.stringify({ error: 'Failed to fetch healthchecks', checks: [] }), {
|
|
97
|
+
status: 500,
|
|
98
|
+
headers: { 'Content-Type': 'application/json' },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
};
|
package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/infrastructure/uptime/[id]/response-times - Fetch response time history for a Gatus monitor
|
|
3
|
+
* Proxies to Gatus API with caching
|
|
4
|
+
*/
|
|
5
|
+
import type { APIRoute } from 'astro';
|
|
6
|
+
|
|
7
|
+
import { fetchGatusEndpoint } from '../../../../../lib/infrastructure/gatus';
|
|
8
|
+
|
|
9
|
+
interface Env {
|
|
10
|
+
PLATFORM_CACHE?: KVNamespace;
|
|
11
|
+
GATUS_API_URL?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ResponseTimeDataPoint {
|
|
15
|
+
timestamp: number;
|
|
16
|
+
value: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ResponseTimesResponse {
|
|
20
|
+
dataPoints: ResponseTimeDataPoint[];
|
|
21
|
+
monitorId: string;
|
|
22
|
+
cached: boolean;
|
|
23
|
+
stats: {
|
|
24
|
+
avg: number;
|
|
25
|
+
min: number;
|
|
26
|
+
max: number;
|
|
27
|
+
trend: 'improving' | 'stable' | 'degrading';
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const CACHE_TTL = 300; // 5 minutes
|
|
32
|
+
|
|
33
|
+
export const GET: APIRoute = async ({ params, locals }) => {
|
|
34
|
+
const monitorId = params.id;
|
|
35
|
+
if (!monitorId) {
|
|
36
|
+
return new Response(JSON.stringify({ error: 'Missing monitor ID' }), {
|
|
37
|
+
status: 400,
|
|
38
|
+
headers: { 'Content-Type': 'application/json' },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const env = locals.runtime?.env as Env | undefined;
|
|
43
|
+
const kv = env?.PLATFORM_CACHE;
|
|
44
|
+
const gatusUrl = env?.GATUS_API_URL || 'https://status.littlebearapps.com';
|
|
45
|
+
|
|
46
|
+
const cacheKey = `UPTIME_RESPONSE_TIMES:${monitorId}`;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
// Check cache first
|
|
50
|
+
if (kv) {
|
|
51
|
+
const cached = await kv.get(cacheKey, 'json');
|
|
52
|
+
if (cached) {
|
|
53
|
+
return new Response(JSON.stringify({ ...cached, cached: true }), {
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Fetch from Gatus
|
|
60
|
+
const endpoint = await fetchGatusEndpoint(gatusUrl, monitorId, kv);
|
|
61
|
+
|
|
62
|
+
// Transform results to response time data points (ns → ms)
|
|
63
|
+
const dataPoints: ResponseTimeDataPoint[] = endpoint.results.map((r) => ({
|
|
64
|
+
timestamp: Math.floor(new Date(r.timestamp).getTime() / 1000),
|
|
65
|
+
value: r.duration / 1_000_000, // ns → ms
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
// Calculate stats
|
|
69
|
+
const values = dataPoints.map((dp) => dp.value);
|
|
70
|
+
const avg = values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
|
|
71
|
+
const min = values.length > 0 ? Math.min(...values) : 0;
|
|
72
|
+
const max = values.length > 0 ? Math.max(...values) : 0;
|
|
73
|
+
|
|
74
|
+
// Calculate trend by comparing first half vs second half average
|
|
75
|
+
let trend: 'improving' | 'stable' | 'degrading' = 'stable';
|
|
76
|
+
if (values.length >= 10) {
|
|
77
|
+
const mid = Math.floor(values.length / 2);
|
|
78
|
+
const firstHalfAvg = values.slice(0, mid).reduce((a, b) => a + b, 0) / mid;
|
|
79
|
+
const secondHalfAvg = values.slice(mid).reduce((a, b) => a + b, 0) / (values.length - mid);
|
|
80
|
+
const diff = ((secondHalfAvg - firstHalfAvg) / firstHalfAvg) * 100;
|
|
81
|
+
|
|
82
|
+
if (diff < -10)
|
|
83
|
+
trend = 'improving'; // Response time decreased (faster)
|
|
84
|
+
else if (diff > 10) trend = 'degrading'; // Response time increased (slower)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const result: Omit<ResponseTimesResponse, 'cached'> = {
|
|
88
|
+
dataPoints: dataPoints.slice(0, 288), // Limit to 288 points
|
|
89
|
+
monitorId,
|
|
90
|
+
stats: {
|
|
91
|
+
avg: Math.round(avg),
|
|
92
|
+
min: Math.round(min),
|
|
93
|
+
max: Math.round(max),
|
|
94
|
+
trend,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Cache the result
|
|
99
|
+
if (kv) {
|
|
100
|
+
await kv.put(cacheKey, JSON.stringify(result), { expirationTtl: CACHE_TTL });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return new Response(JSON.stringify({ ...result, cached: false }), {
|
|
104
|
+
headers: { 'Content-Type': 'application/json' },
|
|
105
|
+
});
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error('Error fetching response times:', error);
|
|
108
|
+
return new Response(
|
|
109
|
+
JSON.stringify({
|
|
110
|
+
error: 'Failed to fetch response times',
|
|
111
|
+
dataPoints: [],
|
|
112
|
+
monitorId,
|
|
113
|
+
stats: { avg: 0, min: 0, max: 0, trend: 'stable' },
|
|
114
|
+
}),
|
|
115
|
+
{
|
|
116
|
+
status: 500,
|
|
117
|
+
headers: { 'Content-Type': 'application/json' },
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/infrastructure/uptime - Fetch Gatus uptime monitors
|
|
3
|
+
* Proxies to Gatus API with caching
|
|
4
|
+
*/
|
|
5
|
+
import type { APIRoute } from 'astro';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
fetchAllGatusStatuses,
|
|
9
|
+
calculateUptimeFromResults,
|
|
10
|
+
} from '../../../lib/infrastructure/gatus';
|
|
11
|
+
|
|
12
|
+
interface Env {
|
|
13
|
+
PLATFORM_CACHE?: KVNamespace;
|
|
14
|
+
GATUS_API_URL?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface UptimeMonitor {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
url: string;
|
|
21
|
+
status: 'up' | 'down' | 'paused' | 'unknown';
|
|
22
|
+
uptimeRatio: number;
|
|
23
|
+
responseTime: number;
|
|
24
|
+
lastCheckAt: number;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const CACHE_KEY = 'UPTIME_MONITORS';
|
|
29
|
+
const CACHE_TTL = 300; // 5 minutes
|
|
30
|
+
|
|
31
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
32
|
+
const env = locals.runtime?.env as Env | undefined;
|
|
33
|
+
const kv = env?.PLATFORM_CACHE;
|
|
34
|
+
const gatusUrl = env?.GATUS_API_URL || 'https://status.littlebearapps.com';
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// Check cache first
|
|
38
|
+
if (kv) {
|
|
39
|
+
const cached = await kv.get(CACHE_KEY, 'json');
|
|
40
|
+
if (cached) {
|
|
41
|
+
return new Response(JSON.stringify({ monitors: cached, cached: true }), {
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Fetch all statuses from Gatus
|
|
48
|
+
const allStatuses = await fetchAllGatusStatuses(gatusUrl);
|
|
49
|
+
|
|
50
|
+
// Filter to HTTP monitors (websites, apps, certificates)
|
|
51
|
+
const UPTIME_GROUPS = new Set(['websites', 'apps', 'certificates', 'resources']);
|
|
52
|
+
const websiteEndpoints = allStatuses.filter((ep) => UPTIME_GROUPS.has(ep.group));
|
|
53
|
+
|
|
54
|
+
const monitors: UptimeMonitor[] = websiteEndpoints.map((ep) => {
|
|
55
|
+
const latestResult = ep.results.length > 0 ? ep.results[ep.results.length - 1] : null;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
id: ep.key,
|
|
59
|
+
name: ep.name,
|
|
60
|
+
url: latestResult?.hostname ? `https://${latestResult.hostname}` : `https://${ep.name}`,
|
|
61
|
+
status: latestResult ? (latestResult.success ? 'up' : 'down') : 'unknown',
|
|
62
|
+
uptimeRatio: calculateUptimeFromResults(ep.results),
|
|
63
|
+
responseTime: latestResult ? latestResult.duration / 1_000_000 : 0, // ns → ms
|
|
64
|
+
lastCheckAt: latestResult
|
|
65
|
+
? Math.floor(new Date(latestResult.timestamp).getTime() / 1000)
|
|
66
|
+
: 0,
|
|
67
|
+
createdAt: 0, // Not available from Gatus
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Cache the result
|
|
72
|
+
if (kv) {
|
|
73
|
+
await kv.put(CACHE_KEY, JSON.stringify(monitors), { expirationTtl: CACHE_TTL });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return new Response(JSON.stringify({ monitors, cached: false }), {
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
});
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error('Error fetching uptime monitors:', error);
|
|
81
|
+
return new Response(
|
|
82
|
+
JSON.stringify({ error: 'Failed to fetch uptime monitors', monitors: [] }),
|
|
83
|
+
{
|
|
84
|
+
status: 500,
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test endpoint to verify Service Auth APIs work with valid tokens
|
|
3
|
+
* This endpoint uses the stored service token secrets to test the batch endpoint
|
|
4
|
+
*
|
|
5
|
+
* Usage: GET https://admin.littlebearapps.com/api/test/service-auth
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { APIContext } from 'astro';
|
|
9
|
+
|
|
10
|
+
export async function GET({ locals }: APIContext): Promise<Response> {
|
|
11
|
+
const env = locals.runtime.env as any;
|
|
12
|
+
|
|
13
|
+
// Get service token credentials from environment
|
|
14
|
+
const batchTokenId = env.BATCH_SERVICE_TOKEN_ID;
|
|
15
|
+
const batchTokenSecret = env.BATCH_SERVICE_TOKEN_SECRET;
|
|
16
|
+
const scannerTokenId = env.SCANNER_SERVICE_TOKEN_ID;
|
|
17
|
+
const scannerTokenSecret = env.SCANNER_SERVICE_TOKEN_SECRET;
|
|
18
|
+
|
|
19
|
+
const results = {
|
|
20
|
+
timestamp: new Date().toISOString(),
|
|
21
|
+
credentials: {
|
|
22
|
+
batchTokenId: batchTokenId ? `${batchTokenId.substring(0, 8)}...` : 'NOT SET',
|
|
23
|
+
scannerTokenId: scannerTokenId ? `${scannerTokenId.substring(0, 8)}...` : 'NOT SET',
|
|
24
|
+
adminTokenId: env.ADMIN_SERVICE_TOKEN_ID
|
|
25
|
+
? `${env.ADMIN_SERVICE_TOKEN_ID.substring(0, 8)}...`
|
|
26
|
+
: 'NOT SET',
|
|
27
|
+
hasSecrets: {
|
|
28
|
+
batch: !!batchTokenSecret,
|
|
29
|
+
scanner: !!scannerTokenSecret,
|
|
30
|
+
admin: !!env.ADMIN_SERVICE_TOKEN_SECRET,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
tests: [] as any[],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Test 1: Subtlety Batch API
|
|
37
|
+
try {
|
|
38
|
+
const batchResponse = await fetch(
|
|
39
|
+
'https://admin.littlebearapps.com/api/brand-copilot/subtlety/batch',
|
|
40
|
+
{
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: {
|
|
43
|
+
'CF-Access-Client-Id': batchTokenId,
|
|
44
|
+
'CF-Access-Client-Secret': batchTokenSecret,
|
|
45
|
+
'Content-Type': 'application/json',
|
|
46
|
+
},
|
|
47
|
+
body: JSON.stringify({ limit: 1, forceRecalculate: false }),
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const batchData = await batchResponse.json().catch(() => ({ error: 'Failed to parse JSON' }));
|
|
52
|
+
|
|
53
|
+
results.tests.push({
|
|
54
|
+
name: 'Subtlety Batch API',
|
|
55
|
+
endpoint: '/api/brand-copilot/subtlety/batch',
|
|
56
|
+
token: 'batch-processing',
|
|
57
|
+
status: batchResponse.status,
|
|
58
|
+
statusText: batchResponse.statusText,
|
|
59
|
+
success: batchResponse.status === 200,
|
|
60
|
+
response: batchData,
|
|
61
|
+
});
|
|
62
|
+
} catch (error: any) {
|
|
63
|
+
results.tests.push({
|
|
64
|
+
name: 'Subtlety Batch API',
|
|
65
|
+
endpoint: '/api/brand-copilot/subtlety/batch',
|
|
66
|
+
token: 'batch-processing',
|
|
67
|
+
error: error.message,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Test 2: Scanner API
|
|
72
|
+
try {
|
|
73
|
+
const scannerResponse = await fetch(
|
|
74
|
+
'https://admin.littlebearapps.com/api/brand-copilot/scanner',
|
|
75
|
+
{
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: {
|
|
78
|
+
'CF-Access-Client-Id': scannerTokenId,
|
|
79
|
+
'CF-Access-Client-Secret': scannerTokenSecret,
|
|
80
|
+
'Content-Type': 'application/json',
|
|
81
|
+
},
|
|
82
|
+
body: JSON.stringify({ platforms: [], source: 'test' }),
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const scannerData = await scannerResponse
|
|
87
|
+
.json()
|
|
88
|
+
.catch(() => ({ error: 'Failed to parse JSON' }));
|
|
89
|
+
|
|
90
|
+
results.tests.push({
|
|
91
|
+
name: 'Scanner API',
|
|
92
|
+
endpoint: '/api/brand-copilot/scanner',
|
|
93
|
+
token: 'scanner-cron-worker',
|
|
94
|
+
status: scannerResponse.status,
|
|
95
|
+
statusText: scannerResponse.statusText,
|
|
96
|
+
success: scannerResponse.status === 200,
|
|
97
|
+
response: scannerData,
|
|
98
|
+
});
|
|
99
|
+
} catch (error: any) {
|
|
100
|
+
results.tests.push({
|
|
101
|
+
name: 'Scanner API',
|
|
102
|
+
endpoint: '/api/brand-copilot/scanner',
|
|
103
|
+
token: 'scanner-cron-worker',
|
|
104
|
+
error: error.message,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Test 3: Semantic Backfill API (using admin token)
|
|
109
|
+
const adminTokenId = env.ADMIN_SERVICE_TOKEN_ID;
|
|
110
|
+
const adminTokenSecret = env.ADMIN_SERVICE_TOKEN_SECRET;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const backfillResponse = await fetch(
|
|
114
|
+
'https://admin.littlebearapps.com/api/admin/semantic/backfill',
|
|
115
|
+
{
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: {
|
|
118
|
+
'CF-Access-Client-Id': adminTokenId,
|
|
119
|
+
'CF-Access-Client-Secret': adminTokenSecret,
|
|
120
|
+
'Content-Type': 'application/json',
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify({ limit: 1, maxDuration: 1000 }),
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const backfillData = await backfillResponse
|
|
127
|
+
.json()
|
|
128
|
+
.catch(() => ({ error: 'Failed to parse JSON' }));
|
|
129
|
+
|
|
130
|
+
results.tests.push({
|
|
131
|
+
name: 'Semantic Backfill API',
|
|
132
|
+
endpoint: '/api/admin/semantic/backfill',
|
|
133
|
+
token: 'admin-tools',
|
|
134
|
+
status: backfillResponse.status,
|
|
135
|
+
statusText: backfillResponse.statusText,
|
|
136
|
+
success: backfillResponse.status === 200,
|
|
137
|
+
response: backfillData,
|
|
138
|
+
});
|
|
139
|
+
} catch (error: any) {
|
|
140
|
+
results.tests.push({
|
|
141
|
+
name: 'Semantic Backfill API',
|
|
142
|
+
endpoint: '/api/admin/semantic/backfill',
|
|
143
|
+
token: 'admin-tools',
|
|
144
|
+
error: error.message,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Calculate summary
|
|
149
|
+
const summary = {
|
|
150
|
+
total: results.tests.length,
|
|
151
|
+
passed: results.tests.filter((t) => t.success).length,
|
|
152
|
+
failed: results.tests.filter((t) => !t.success).length,
|
|
153
|
+
passRate: Math.round(
|
|
154
|
+
(results.tests.filter((t) => t.success).length / results.tests.length) * 100
|
|
155
|
+
),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
return new Response(
|
|
159
|
+
JSON.stringify(
|
|
160
|
+
{
|
|
161
|
+
summary,
|
|
162
|
+
results,
|
|
163
|
+
message:
|
|
164
|
+
summary.passRate === 100
|
|
165
|
+
? '✅ All Service Auth APIs working correctly with service tokens!'
|
|
166
|
+
: `⚠️ ${summary.failed} of ${summary.total} tests failed`,
|
|
167
|
+
},
|
|
168
|
+
null,
|
|
169
|
+
2
|
|
170
|
+
),
|
|
171
|
+
{
|
|
172
|
+
status: 200,
|
|
173
|
+
headers: {
|
|
174
|
+
'Content-Type': 'application/json',
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
}
|