@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,365 @@
|
|
|
1
|
+
---
|
|
2
|
+
import DashboardLayout from '../layouts/DashboardLayout.astro';
|
|
3
|
+
|
|
4
|
+
interface FeedbackEvent {
|
|
5
|
+
feedbackId: string;
|
|
6
|
+
repo: string;
|
|
7
|
+
issueNumber: number;
|
|
8
|
+
category: string;
|
|
9
|
+
sentiment: string | null;
|
|
10
|
+
relatedErrorCorrelation: string | null;
|
|
11
|
+
correlationConfidence: number;
|
|
12
|
+
correlationMethod: 'rule-based' | 'ml' | 'manual';
|
|
13
|
+
userContactEncrypted: string | null;
|
|
14
|
+
receivedAt: string;
|
|
15
|
+
updatedAt: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const runtime = (Astro.locals as App.Locals | undefined)?.runtime;
|
|
19
|
+
const db = runtime?.env?.PLATFORM_DB;
|
|
20
|
+
const kv = runtime?.env?.PLATFORM_CACHE;
|
|
21
|
+
|
|
22
|
+
let recentFeedback: FeedbackEvent[] = [];
|
|
23
|
+
let correlatedFeedback: FeedbackEvent[] = [];
|
|
24
|
+
let feedbackByCategory: Record<string, number> = {
|
|
25
|
+
'feature-request': 0,
|
|
26
|
+
'bug-report': 0,
|
|
27
|
+
'ux-improvement': 0,
|
|
28
|
+
other: 0,
|
|
29
|
+
};
|
|
30
|
+
let totalFeedback = 0;
|
|
31
|
+
|
|
32
|
+
if (db && kv) {
|
|
33
|
+
// Try KV cache first for recent feedback
|
|
34
|
+
const cachedRecent = await kv.get('feedback:recent:all');
|
|
35
|
+
if (cachedRecent) {
|
|
36
|
+
recentFeedback = JSON.parse(cachedRecent);
|
|
37
|
+
} else {
|
|
38
|
+
// Cache miss - query D1
|
|
39
|
+
const result = await db
|
|
40
|
+
.prepare(
|
|
41
|
+
`SELECT
|
|
42
|
+
feedback_id as feedbackId,
|
|
43
|
+
repo,
|
|
44
|
+
issue_number as issueNumber,
|
|
45
|
+
category,
|
|
46
|
+
sentiment,
|
|
47
|
+
related_error_correlation as relatedErrorCorrelation,
|
|
48
|
+
correlation_confidence as correlationConfidence,
|
|
49
|
+
correlation_method as correlationMethod,
|
|
50
|
+
user_contact_encrypted as userContactEncrypted,
|
|
51
|
+
received_at as receivedAt,
|
|
52
|
+
updated_at as updatedAt
|
|
53
|
+
FROM feedback_events
|
|
54
|
+
ORDER BY received_at DESC
|
|
55
|
+
LIMIT 20`
|
|
56
|
+
)
|
|
57
|
+
.all<FeedbackEvent>();
|
|
58
|
+
|
|
59
|
+
recentFeedback = result.results ?? [];
|
|
60
|
+
|
|
61
|
+
// Cache for 15 minutes
|
|
62
|
+
await kv.put('feedback:recent:all', JSON.stringify(recentFeedback), {
|
|
63
|
+
expirationTtl: 900,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Get correlated feedback (linked to errors)
|
|
68
|
+
const cachedCorrelated = await kv.get('feedback:correlated:all');
|
|
69
|
+
if (cachedCorrelated) {
|
|
70
|
+
correlatedFeedback = JSON.parse(cachedCorrelated);
|
|
71
|
+
} else {
|
|
72
|
+
const result = await db
|
|
73
|
+
.prepare(
|
|
74
|
+
`SELECT
|
|
75
|
+
feedback_id as feedbackId,
|
|
76
|
+
repo,
|
|
77
|
+
issue_number as issueNumber,
|
|
78
|
+
category,
|
|
79
|
+
sentiment,
|
|
80
|
+
related_error_correlation as relatedErrorCorrelation,
|
|
81
|
+
correlation_confidence as correlationConfidence,
|
|
82
|
+
correlation_method as correlationMethod,
|
|
83
|
+
user_contact_encrypted as userContactEncrypted,
|
|
84
|
+
received_at as receivedAt,
|
|
85
|
+
updated_at as updatedAt
|
|
86
|
+
FROM feedback_events
|
|
87
|
+
WHERE related_error_correlation IS NOT NULL
|
|
88
|
+
ORDER BY correlation_confidence DESC, received_at DESC
|
|
89
|
+
LIMIT 20`
|
|
90
|
+
)
|
|
91
|
+
.all<FeedbackEvent>();
|
|
92
|
+
|
|
93
|
+
correlatedFeedback = result.results ?? [];
|
|
94
|
+
|
|
95
|
+
await kv.put('feedback:correlated:all', JSON.stringify(correlatedFeedback), {
|
|
96
|
+
expirationTtl: 900,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Get counts by category
|
|
101
|
+
const categoryResult = await db
|
|
102
|
+
.prepare(
|
|
103
|
+
`SELECT category, COUNT(*) as count
|
|
104
|
+
FROM feedback_events
|
|
105
|
+
GROUP BY category`
|
|
106
|
+
)
|
|
107
|
+
.all<{ category: string; count: number }>();
|
|
108
|
+
|
|
109
|
+
for (const row of categoryResult.results ?? []) {
|
|
110
|
+
feedbackByCategory[row.category] = row.count;
|
|
111
|
+
totalFeedback += row.count;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Calculate priority score for each feedback event
|
|
116
|
+
// Higher correlation confidence = higher priority
|
|
117
|
+
function getPriorityScore(feedback: FeedbackEvent): number {
|
|
118
|
+
let score = 0;
|
|
119
|
+
|
|
120
|
+
// Correlation confidence (0-100 points)
|
|
121
|
+
score += feedback.correlationConfidence * 100;
|
|
122
|
+
|
|
123
|
+
// Category weight
|
|
124
|
+
if (feedback.category === 'bug-report') score += 30;
|
|
125
|
+
if (feedback.category === 'feature-request') score += 20;
|
|
126
|
+
if (feedback.category === 'ux-improvement') score += 15;
|
|
127
|
+
|
|
128
|
+
// Sentiment weight (if available)
|
|
129
|
+
if (feedback.sentiment === 'negative') score += 25;
|
|
130
|
+
if (feedback.sentiment === 'positive') score += 5;
|
|
131
|
+
|
|
132
|
+
return Math.round(score);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Sort by priority
|
|
136
|
+
const prioritizedFeedback = [...recentFeedback].sort(
|
|
137
|
+
(a, b) => getPriorityScore(b) - getPriorityScore(a)
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
function formatDate(dateString: string): string {
|
|
141
|
+
const date = new Date(dateString);
|
|
142
|
+
const now = new Date();
|
|
143
|
+
const diffMs = now.getTime() - date.getTime();
|
|
144
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
145
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
146
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
147
|
+
|
|
148
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
149
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
150
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
151
|
+
return date.toLocaleDateString();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getCategoryBadgeColor(category: string): string {
|
|
155
|
+
switch (category) {
|
|
156
|
+
case 'feature-request':
|
|
157
|
+
return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300';
|
|
158
|
+
case 'bug-report':
|
|
159
|
+
return 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300';
|
|
160
|
+
case 'ux-improvement':
|
|
161
|
+
return 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300';
|
|
162
|
+
default:
|
|
163
|
+
return 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function getPriorityBadgeColor(score: number): string {
|
|
168
|
+
if (score >= 120) return 'bg-red-500 text-white';
|
|
169
|
+
if (score >= 80) return 'bg-orange-500 text-white';
|
|
170
|
+
if (score >= 40) return 'bg-yellow-500 text-white';
|
|
171
|
+
return 'bg-green-500 text-white';
|
|
172
|
+
}
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
<DashboardLayout title="Feedback">
|
|
176
|
+
<div class="space-y-6">
|
|
177
|
+
<h2 class="text-3xl font-bold text-gray-900 dark:text-white">User Feedback</h2>
|
|
178
|
+
|
|
179
|
+
<!-- Summary Cards -->
|
|
180
|
+
<div class="grid grid-cols-1 gap-6 md:grid-cols-4">
|
|
181
|
+
<div class="metric-card">
|
|
182
|
+
<div class="metric-title">Total Feedback</div>
|
|
183
|
+
<div class="metric-value">{totalFeedback}</div>
|
|
184
|
+
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">All time</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<div class="metric-card">
|
|
188
|
+
<div class="metric-title">Feature Requests</div>
|
|
189
|
+
<div class="metric-value">{feedbackByCategory['feature-request']}</div>
|
|
190
|
+
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
191
|
+
{((feedbackByCategory['feature-request'] / totalFeedback) * 100).toFixed(0)}%
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
<div class="metric-card">
|
|
196
|
+
<div class="metric-title">Bug Reports</div>
|
|
197
|
+
<div class="metric-value">{feedbackByCategory['bug-report']}</div>
|
|
198
|
+
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
199
|
+
{((feedbackByCategory['bug-report'] / totalFeedback) * 100).toFixed(0)}%
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div class="metric-card">
|
|
204
|
+
<div class="metric-title">Correlated</div>
|
|
205
|
+
<div class="metric-value">{correlatedFeedback.length}</div>
|
|
206
|
+
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">Linked to errors</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<!-- Prioritized Feedback List -->
|
|
211
|
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
212
|
+
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Feedback by Priority</h3>
|
|
213
|
+
|
|
214
|
+
<div class="space-y-3">
|
|
215
|
+
{
|
|
216
|
+
prioritizedFeedback.length === 0 ? (
|
|
217
|
+
<p class="text-gray-600 dark:text-gray-400">No feedback events yet.</p>
|
|
218
|
+
) : (
|
|
219
|
+
prioritizedFeedback.map((feedback) => {
|
|
220
|
+
const priorityScore = getPriorityScore(feedback);
|
|
221
|
+
return (
|
|
222
|
+
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors">
|
|
223
|
+
<div class="flex items-start justify-between">
|
|
224
|
+
<div class="flex-1">
|
|
225
|
+
<div class="flex items-center gap-2 mb-2">
|
|
226
|
+
<span
|
|
227
|
+
class={`px-2 py-1 rounded text-xs font-medium ${getCategoryBadgeColor(feedback.category)}`}
|
|
228
|
+
>
|
|
229
|
+
{feedback.category}
|
|
230
|
+
</span>
|
|
231
|
+
<span
|
|
232
|
+
class={`px-2 py-1 rounded text-xs font-bold ${getPriorityBadgeColor(priorityScore)}`}
|
|
233
|
+
>
|
|
234
|
+
P{priorityScore}
|
|
235
|
+
</span>
|
|
236
|
+
{feedback.relatedErrorCorrelation && (
|
|
237
|
+
<span class="px-2 py-1 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300">
|
|
238
|
+
Correlated ({Math.round(feedback.correlationConfidence * 100)}%)
|
|
239
|
+
</span>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
|
244
|
+
<span class="font-mono">{feedback.repo}</span>
|
|
245
|
+
<span>•</span>
|
|
246
|
+
<a
|
|
247
|
+
href={`https://github.com/littlebearapps/${feedback.repo}/issues/${feedback.issueNumber}`}
|
|
248
|
+
target="_blank"
|
|
249
|
+
rel="noopener noreferrer"
|
|
250
|
+
class="text-blue-600 dark:text-blue-400 hover:underline"
|
|
251
|
+
>
|
|
252
|
+
#{feedback.issueNumber}
|
|
253
|
+
</a>
|
|
254
|
+
<span>•</span>
|
|
255
|
+
<span>{formatDate(feedback.receivedAt)}</span>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
{feedback.relatedErrorCorrelation && (
|
|
259
|
+
<div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
|
260
|
+
<span>Linked to error: </span>
|
|
261
|
+
<span class="font-mono">
|
|
262
|
+
{feedback.relatedErrorCorrelation.substring(0, 8)}...
|
|
263
|
+
</span>
|
|
264
|
+
<span class="ml-2">via {feedback.correlationMethod}</span>
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
|
|
268
|
+
{feedback.userContactEncrypted && (
|
|
269
|
+
<div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
|
270
|
+
<span>User contact provided (encrypted)</span>
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
})
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<!-- Correlated Feedback Section -->
|
|
284
|
+
{
|
|
285
|
+
correlatedFeedback.length > 0 && (
|
|
286
|
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
287
|
+
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
|
288
|
+
High-Confidence Correlations
|
|
289
|
+
</h3>
|
|
290
|
+
|
|
291
|
+
<div class="space-y-3">
|
|
292
|
+
{correlatedFeedback.map((feedback) => (
|
|
293
|
+
<div class="border border-green-200 dark:border-green-700 rounded-lg p-4 bg-green-50 dark:bg-green-900/20">
|
|
294
|
+
<div class="flex items-start justify-between">
|
|
295
|
+
<div class="flex-1">
|
|
296
|
+
<div class="flex items-center gap-2 mb-2">
|
|
297
|
+
<span
|
|
298
|
+
class={`px-2 py-1 rounded text-xs font-medium ${getCategoryBadgeColor(feedback.category)}`}
|
|
299
|
+
>
|
|
300
|
+
{feedback.category}
|
|
301
|
+
</span>
|
|
302
|
+
<span class="px-2 py-1 rounded text-xs font-bold bg-green-600 text-white">
|
|
303
|
+
{Math.round(feedback.correlationConfidence * 100)}% confident
|
|
304
|
+
</span>
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-300">
|
|
308
|
+
<span class="font-mono">{feedback.repo}</span>
|
|
309
|
+
<span>•</span>
|
|
310
|
+
<a
|
|
311
|
+
href={`https://github.com/littlebearapps/${feedback.repo}/issues/${feedback.issueNumber}`}
|
|
312
|
+
target="_blank"
|
|
313
|
+
rel="noopener noreferrer"
|
|
314
|
+
class="text-blue-600 dark:text-blue-400 hover:underline"
|
|
315
|
+
>
|
|
316
|
+
#{feedback.issueNumber}
|
|
317
|
+
</a>
|
|
318
|
+
<span>•</span>
|
|
319
|
+
<span>{formatDate(feedback.receivedAt)}</span>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<div class="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
|
323
|
+
<span>Linked to error: </span>
|
|
324
|
+
<span class="font-mono bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">
|
|
325
|
+
{feedback.relatedErrorCorrelation}
|
|
326
|
+
</span>
|
|
327
|
+
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">
|
|
328
|
+
(via {feedback.correlationMethod})
|
|
329
|
+
</span>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
))}
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
<!-- Category Breakdown -->
|
|
341
|
+
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
|
342
|
+
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-4">Feedback by Category</h3>
|
|
343
|
+
|
|
344
|
+
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
|
345
|
+
{
|
|
346
|
+
Object.entries(feedbackByCategory).map(([category, count]) => (
|
|
347
|
+
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
|
348
|
+
<div class="flex items-center justify-between">
|
|
349
|
+
<span
|
|
350
|
+
class={`px-3 py-1 rounded text-sm font-medium ${getCategoryBadgeColor(category)}`}
|
|
351
|
+
>
|
|
352
|
+
{category}
|
|
353
|
+
</span>
|
|
354
|
+
<span class="text-2xl font-bold text-gray-900 dark:text-white">{count}</span>
|
|
355
|
+
</div>
|
|
356
|
+
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
357
|
+
{totalFeedback > 0 ? ((count / totalFeedback) * 100).toFixed(1) : 0}% of total
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
))
|
|
361
|
+
}
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
</div>
|
|
365
|
+
</DashboardLayout>
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
---
|
|
2
|
+
const runtime = (Astro.locals as App.Locals | undefined)?.runtime;
|
|
3
|
+
const db = runtime?.env?.PLATFORM_DB;
|
|
4
|
+
|
|
5
|
+
let errors24h = 0;
|
|
6
|
+
let mrr = 0;
|
|
7
|
+
let activeUsers = 0;
|
|
8
|
+
let alerts = 0;
|
|
9
|
+
|
|
10
|
+
if (db) {
|
|
11
|
+
// Wrap each query in try/catch - tables may not exist yet
|
|
12
|
+
try {
|
|
13
|
+
const errorRow = await db
|
|
14
|
+
.prepare(
|
|
15
|
+
`SELECT COUNT(*) AS count FROM error_reports
|
|
16
|
+
WHERE timestamp > unixepoch() - 86400`
|
|
17
|
+
)
|
|
18
|
+
.first<{ count: number }>();
|
|
19
|
+
errors24h = errorRow?.count ?? 0;
|
|
20
|
+
} catch {
|
|
21
|
+
// Table doesn't exist yet
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const mrrRow = await db
|
|
26
|
+
.prepare(
|
|
27
|
+
`SELECT value FROM revenue_metrics
|
|
28
|
+
WHERE metric_type = 'mrr'
|
|
29
|
+
ORDER BY timestamp DESC
|
|
30
|
+
LIMIT 1`
|
|
31
|
+
)
|
|
32
|
+
.first<{ value: number }>();
|
|
33
|
+
mrr = mrrRow?.value ?? 0;
|
|
34
|
+
} catch {
|
|
35
|
+
// Table doesn't exist yet
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const activeUsersRow = await db
|
|
40
|
+
.prepare(
|
|
41
|
+
`SELECT value FROM product_metrics
|
|
42
|
+
WHERE metric_type = 'active_users'
|
|
43
|
+
ORDER BY timestamp DESC
|
|
44
|
+
LIMIT 1`
|
|
45
|
+
)
|
|
46
|
+
.first<{ value: number }>();
|
|
47
|
+
activeUsers = activeUsersRow?.value ?? 0;
|
|
48
|
+
} catch {
|
|
49
|
+
// Table doesn't exist yet
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const alertsRow = await db
|
|
54
|
+
.prepare(
|
|
55
|
+
`SELECT COUNT(*) AS count FROM alerts
|
|
56
|
+
WHERE resolved = 0 AND severity IN ('critical', 'high')`
|
|
57
|
+
)
|
|
58
|
+
.first<{ count: number }>();
|
|
59
|
+
alerts = alertsRow?.count ?? 0;
|
|
60
|
+
} catch {
|
|
61
|
+
// Table doesn't exist yet
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
<!doctype html>
|
|
67
|
+
<html lang="en">
|
|
68
|
+
<head>
|
|
69
|
+
<meta charset="UTF-8" />
|
|
70
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
71
|
+
<title>Platform Kiosk</title>
|
|
72
|
+
<style>
|
|
73
|
+
body {
|
|
74
|
+
margin: 0;
|
|
75
|
+
padding: 2rem;
|
|
76
|
+
background: #111;
|
|
77
|
+
color: #fff;
|
|
78
|
+
font-family:
|
|
79
|
+
'Inter',
|
|
80
|
+
system-ui,
|
|
81
|
+
-apple-system,
|
|
82
|
+
BlinkMacSystemFont,
|
|
83
|
+
'Segoe UI',
|
|
84
|
+
sans-serif;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.kiosk-grid {
|
|
88
|
+
display: grid;
|
|
89
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
90
|
+
gap: 2rem;
|
|
91
|
+
min-height: calc(100vh - 4rem);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.kiosk-tile {
|
|
95
|
+
background: #222;
|
|
96
|
+
border-radius: 1.25rem;
|
|
97
|
+
padding: 3rem;
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
align-items: center;
|
|
102
|
+
text-align: center;
|
|
103
|
+
box-shadow: 0 1.5rem 3rem rgba(0, 0, 0, 0.35);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.kiosk-label {
|
|
107
|
+
font-size: 2rem;
|
|
108
|
+
text-transform: uppercase;
|
|
109
|
+
letter-spacing: 0.08em;
|
|
110
|
+
opacity: 0.7;
|
|
111
|
+
margin-bottom: 1.5rem;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.kiosk-value {
|
|
115
|
+
font-size: clamp(4rem, 8vw, 7rem);
|
|
116
|
+
font-weight: 700;
|
|
117
|
+
line-height: 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.kiosk-unit {
|
|
121
|
+
font-size: 2rem;
|
|
122
|
+
opacity: 0.6;
|
|
123
|
+
margin-top: 1rem;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.status-ok {
|
|
127
|
+
color: #10b981;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.status-warning {
|
|
131
|
+
color: #f59e0b;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.status-error {
|
|
135
|
+
color: #ef4444;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.refresh-indicator {
|
|
139
|
+
position: fixed;
|
|
140
|
+
bottom: 2rem;
|
|
141
|
+
right: 2rem;
|
|
142
|
+
opacity: 0.5;
|
|
143
|
+
font-size: 1rem;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@media (max-width: 900px) {
|
|
147
|
+
body {
|
|
148
|
+
padding: 1.5rem;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.kiosk-grid {
|
|
152
|
+
grid-template-columns: 1fr;
|
|
153
|
+
gap: 1.5rem;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.kiosk-tile {
|
|
157
|
+
padding: 2rem;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.kiosk-label {
|
|
161
|
+
font-size: 1.5rem;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
</style>
|
|
165
|
+
</head>
|
|
166
|
+
<body>
|
|
167
|
+
<div class="kiosk-grid">
|
|
168
|
+
<div class="kiosk-tile">
|
|
169
|
+
<div class="kiosk-label">Errors (24h)</div>
|
|
170
|
+
<div class={`kiosk-value ${errors24h > 10 ? 'status-error' : 'status-ok'}`}>
|
|
171
|
+
{errors24h}
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div class="kiosk-tile">
|
|
176
|
+
<div class="kiosk-label">MRR</div>
|
|
177
|
+
<div class="kiosk-value status-ok">
|
|
178
|
+
${mrr.toFixed(0)}
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<div class="kiosk-tile">
|
|
183
|
+
<div class="kiosk-label">Active Users</div>
|
|
184
|
+
<div class="kiosk-value status-ok">
|
|
185
|
+
{activeUsers.toLocaleString()}
|
|
186
|
+
</div>
|
|
187
|
+
<div class="kiosk-unit">Last 30 Days</div>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<div class="kiosk-tile">
|
|
191
|
+
<div class="kiosk-label">Active Alerts</div>
|
|
192
|
+
<div class={`kiosk-value ${alerts > 0 ? 'status-error' : 'status-ok'}`}>
|
|
193
|
+
{alerts}
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<div class="refresh-indicator">Auto-refresh: 60s</div>
|
|
199
|
+
|
|
200
|
+
<script>
|
|
201
|
+
setTimeout(() => {
|
|
202
|
+
window.location.reload();
|
|
203
|
+
}, 60000);
|
|
204
|
+
</script>
|
|
205
|
+
</body>
|
|
206
|
+
</html>
|