@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,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
+ };
@@ -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
+ }