@littlebearapps/platform-admin-sdk 2.1.0 → 2.3.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/check-upgrade.d.ts +29 -0
- package/dist/check-upgrade.js +97 -0
- package/dist/index.js +59 -4
- package/dist/manifest.d.ts +2 -0
- package/dist/scaffold.js +5 -1
- package/dist/templates.d.ts +6 -1
- package/dist/templates.js +141 -3
- package/dist/upgrade.d.ts +1 -0
- package/dist/upgrade.js +21 -2
- 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,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POST /api/patterns/:id/reject - Proxy to pattern-discovery /suggestions/:id?action=reject
|
|
3
|
+
* Rejects a pending pattern suggestion
|
|
4
|
+
*/
|
|
5
|
+
import type { APIRoute } from 'astro';
|
|
6
|
+
|
|
7
|
+
interface Env {
|
|
8
|
+
PATTERN_DISCOVERY_API?: Fetcher;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const POST: APIRoute = async ({ locals, params, url }) => {
|
|
12
|
+
const api = (locals.runtime?.env as Env)?.PATTERN_DISCOVERY_API;
|
|
13
|
+
|
|
14
|
+
if (!api) {
|
|
15
|
+
return new Response(JSON.stringify({ error: 'Pattern Discovery API not configured' }), {
|
|
16
|
+
status: 503,
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { id } = params;
|
|
22
|
+
if (!id) {
|
|
23
|
+
return new Response(JSON.stringify({ error: 'Missing suggestion ID' }), {
|
|
24
|
+
status: 400,
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const by = url.searchParams.get('by') || 'dashboard';
|
|
31
|
+
const reason = url.searchParams.get('reason') || 'Rejected via dashboard';
|
|
32
|
+
|
|
33
|
+
const response = await api.fetch(
|
|
34
|
+
`https://pattern-discovery.littlebearapps.workers.dev/suggestions/${id}?action=reject&by=${by}&reason=${encodeURIComponent(reason)}`,
|
|
35
|
+
{ method: 'POST' }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const data = await response.json();
|
|
39
|
+
return new Response(JSON.stringify(data), {
|
|
40
|
+
status: response.status,
|
|
41
|
+
headers: { 'Content-Type': 'application/json' },
|
|
42
|
+
});
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('Error rejecting suggestion:', error);
|
|
45
|
+
return new Response(JSON.stringify({ error: 'Failed to reject suggestion' }), {
|
|
46
|
+
status: 500,
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/reports/digests/stats - Proxy to error-collector /digests/stats
|
|
3
|
+
*/
|
|
4
|
+
import type { APIRoute } from 'astro';
|
|
5
|
+
|
|
6
|
+
interface Env {
|
|
7
|
+
ERROR_COLLECTOR_API?: Fetcher;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const GET: APIRoute = async ({ locals }) => {
|
|
11
|
+
const api = (locals.runtime?.env as Env)?.ERROR_COLLECTOR_API;
|
|
12
|
+
|
|
13
|
+
if (!api) {
|
|
14
|
+
return new Response(JSON.stringify({ error: 'Error Collector API not configured' }), {
|
|
15
|
+
status: 503,
|
|
16
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const response = await api.fetch(
|
|
22
|
+
'https://error-collector.littlebearapps.workers.dev/digests/stats',
|
|
23
|
+
{ method: 'GET' }
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
return new Response(JSON.stringify(data), {
|
|
28
|
+
status: response.status,
|
|
29
|
+
headers: { 'Content-Type': 'application/json' },
|
|
30
|
+
});
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Error fetching digest stats:', error);
|
|
33
|
+
return new Response(JSON.stringify({ error: 'Failed to fetch digest stats' }), {
|
|
34
|
+
status: 500,
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/reports/digests - Proxy to error-collector /digests
|
|
3
|
+
*/
|
|
4
|
+
import type { APIRoute } from 'astro';
|
|
5
|
+
|
|
6
|
+
interface Env {
|
|
7
|
+
ERROR_COLLECTOR_API?: Fetcher;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const GET: APIRoute = async ({ request, locals }) => {
|
|
11
|
+
const api = (locals.runtime?.env as Env)?.ERROR_COLLECTOR_API;
|
|
12
|
+
|
|
13
|
+
if (!api) {
|
|
14
|
+
return new Response(JSON.stringify({ error: 'Error Collector API not configured' }), {
|
|
15
|
+
status: 503,
|
|
16
|
+
headers: { 'Content-Type': 'application/json' },
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const url = new URL(request.url);
|
|
22
|
+
const response = await api.fetch(
|
|
23
|
+
`https://error-collector.littlebearapps.workers.dev/digests${url.search}`,
|
|
24
|
+
{ method: 'GET' }
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const data = await response.json();
|
|
28
|
+
return new Response(JSON.stringify(data), {
|
|
29
|
+
status: response.status,
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
});
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Error fetching digests:', error);
|
|
34
|
+
return new Response(JSON.stringify({ error: 'Failed to fetch digests' }), {
|
|
35
|
+
status: 500,
|
|
36
|
+
headers: { 'Content-Type': 'application/json' },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reindex API Proxy
|
|
3
|
+
*
|
|
4
|
+
* Proxies reindex requests to platform-search worker.
|
|
5
|
+
*
|
|
6
|
+
* @module dashboard/pages/api/search/reindex
|
|
7
|
+
* @created 2026-02-03
|
|
8
|
+
* @task task-303.3
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { APIRoute } from 'astro';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* POST /api/search/reindex/:type - Reindex all documents of a content type
|
|
15
|
+
*/
|
|
16
|
+
export const POST: APIRoute = async ({ params, locals }) => {
|
|
17
|
+
const api = (locals.runtime?.env as { SEARCH_API?: Fetcher })?.SEARCH_API;
|
|
18
|
+
const { type } = params;
|
|
19
|
+
|
|
20
|
+
if (!api) {
|
|
21
|
+
return new Response(JSON.stringify({ error: 'Search API not available' }), {
|
|
22
|
+
status: 503,
|
|
23
|
+
headers: { 'Content-Type': 'application/json' },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!type) {
|
|
28
|
+
return new Response(JSON.stringify({ error: 'Content type required' }), {
|
|
29
|
+
status: 400,
|
|
30
|
+
headers: { 'Content-Type': 'application/json' },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const response = await api.fetch(
|
|
36
|
+
new Request(
|
|
37
|
+
`https://platform-search.littlebearapps.workers.dev/search/reindex/${encodeURIComponent(type)}`,
|
|
38
|
+
{
|
|
39
|
+
method: 'POST',
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const data = await response.json();
|
|
45
|
+
return new Response(JSON.stringify(data), {
|
|
46
|
+
status: response.status,
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Reindex error:', error);
|
|
51
|
+
return new Response(JSON.stringify({ error: 'Failed to reindex content type' }), {
|
|
52
|
+
status: 500,
|
|
53
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Reports API Endpoint
|
|
3
|
+
*
|
|
4
|
+
* Serves HTML test reports from KV storage
|
|
5
|
+
* Reports are stored by platform-ingest-tester worker
|
|
6
|
+
* TTL: 7 days (passes) or 90 days (failures)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const GET = async ({ params, locals }: any) => {
|
|
10
|
+
const { id: runId } = params;
|
|
11
|
+
const env = locals.runtime.env;
|
|
12
|
+
|
|
13
|
+
if (!runId) {
|
|
14
|
+
return new Response(
|
|
15
|
+
JSON.stringify({
|
|
16
|
+
error: 'Missing run ID',
|
|
17
|
+
message: 'Run ID is required',
|
|
18
|
+
}),
|
|
19
|
+
{
|
|
20
|
+
status: 400,
|
|
21
|
+
headers: { 'Content-Type': 'application/json' },
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// First, get the run from D1 to find the KV key
|
|
28
|
+
const run = await env.PLATFORM_DB.prepare(
|
|
29
|
+
`SELECT id, project, kv_report_key, timestamp, pass_rate
|
|
30
|
+
FROM runs
|
|
31
|
+
WHERE id = ?`
|
|
32
|
+
)
|
|
33
|
+
.bind(runId)
|
|
34
|
+
.first();
|
|
35
|
+
|
|
36
|
+
if (!run) {
|
|
37
|
+
return new Response(
|
|
38
|
+
JSON.stringify({
|
|
39
|
+
error: 'Run not found',
|
|
40
|
+
message: `No test run found with ID: ${runId}`,
|
|
41
|
+
}),
|
|
42
|
+
{
|
|
43
|
+
status: 404,
|
|
44
|
+
headers: { 'Content-Type': 'application/json' },
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!run.kv_report_key) {
|
|
50
|
+
return new Response(
|
|
51
|
+
JSON.stringify({
|
|
52
|
+
error: 'Report not available',
|
|
53
|
+
message: 'HTML report was not uploaded for this run',
|
|
54
|
+
}),
|
|
55
|
+
{
|
|
56
|
+
status: 404,
|
|
57
|
+
headers: { 'Content-Type': 'application/json' },
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Fetch HTML report from KV
|
|
63
|
+
const htmlReport = await env.PLATFORM_CACHE.get(run.kv_report_key);
|
|
64
|
+
|
|
65
|
+
if (!htmlReport) {
|
|
66
|
+
return new Response(
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
error: 'Report expired',
|
|
69
|
+
message: 'HTML report has expired or been deleted (TTL: 7d for passes, 90d for failures)',
|
|
70
|
+
}),
|
|
71
|
+
{
|
|
72
|
+
status: 410, // Gone
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Return HTML report
|
|
79
|
+
return new Response(htmlReport, {
|
|
80
|
+
headers: {
|
|
81
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
82
|
+
'Cache-Control': 'public, max-age=3600', // Cache for 1 hour
|
|
83
|
+
'X-Run-ID': runId,
|
|
84
|
+
'X-Project': run.project as string,
|
|
85
|
+
'X-Timestamp': run.timestamp as string,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
} catch (error: any) {
|
|
89
|
+
console.error('Failed to fetch test report:', error);
|
|
90
|
+
|
|
91
|
+
return new Response(
|
|
92
|
+
JSON.stringify({
|
|
93
|
+
error: 'Internal server error',
|
|
94
|
+
message: error.message,
|
|
95
|
+
}),
|
|
96
|
+
{
|
|
97
|
+
status: 500,
|
|
98
|
+
headers: { 'Content-Type': 'application/json' },
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
@@ -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>
|