@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
package/dist/upgrade.js
CHANGED
|
@@ -14,7 +14,7 @@ import { resolve, dirname, join } from 'node:path';
|
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import Handlebars from 'handlebars';
|
|
16
16
|
import pc from 'picocolors';
|
|
17
|
-
import { getFilesForTier, SDK_VERSION, isMigrationFile, isTierUpgradeOrSame } from './templates.js';
|
|
17
|
+
import { getFilesForTier, SDK_VERSION, isMigrationFile, isTierUpgradeOrSame, detectCollisions } from './templates.js';
|
|
18
18
|
import { readManifest, writeManifest, buildManifest, hashContent, MANIFEST_FILENAME, } from './manifest.js';
|
|
19
19
|
import { findHighestMigration, getMigrationNumber, planMigrations } from './migrations.js';
|
|
20
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -46,7 +46,11 @@ export async function upgrade(projectDir, options = {}) {
|
|
|
46
46
|
}
|
|
47
47
|
if (manifest.sdkVersion === SDK_VERSION && manifest.tier === targetTier) {
|
|
48
48
|
console.log(pc.green(` Already up to date (SDK ${SDK_VERSION}, tier ${targetTier}).`));
|
|
49
|
-
return { created: [], updated: [], skipped: [], removed: [], migrations: [] };
|
|
49
|
+
return { created: [], updated: [], skipped: [], removed: [], migrations: [], excluded: [] };
|
|
50
|
+
}
|
|
51
|
+
const collisions = detectCollisions(targetTier);
|
|
52
|
+
if (collisions.length > 0) {
|
|
53
|
+
throw new Error(`Template naming collisions detected:\n${collisions.map((c) => ` - ${c}`).join('\n')}`);
|
|
50
54
|
}
|
|
51
55
|
const templatesDir = getTemplatesDir();
|
|
52
56
|
const files = getFilesForTier(targetTier);
|
|
@@ -59,12 +63,14 @@ export async function upgrade(projectDir, options = {}) {
|
|
|
59
63
|
defaultAssignee: manifest.context.defaultAssignee,
|
|
60
64
|
sdkVersion: SDK_VERSION,
|
|
61
65
|
};
|
|
66
|
+
const excludeSet = new Set(manifest.excludeFromUpgrade ?? []);
|
|
62
67
|
const result = {
|
|
63
68
|
created: [],
|
|
64
69
|
updated: [],
|
|
65
70
|
skipped: [],
|
|
66
71
|
removed: [],
|
|
67
72
|
migrations: [],
|
|
73
|
+
excluded: [],
|
|
68
74
|
};
|
|
69
75
|
// Separate regular files from migrations
|
|
70
76
|
const regularFiles = files.filter((f) => !isMigrationFile(f));
|
|
@@ -75,6 +81,16 @@ export async function upgrade(projectDir, options = {}) {
|
|
|
75
81
|
const srcPath = join(templatesDir, file.src);
|
|
76
82
|
const destRelative = renderString(file.dest, context);
|
|
77
83
|
const destPath = join(projectDir, destRelative);
|
|
84
|
+
if (excludeSet.has(destRelative)) {
|
|
85
|
+
console.log(` ${pc.dim('exclude')} ${destRelative} ${pc.dim('(in excludeFromUpgrade)')}`);
|
|
86
|
+
result.excluded.push(destRelative);
|
|
87
|
+
// Preserve existing hash in new manifest if file is on disk
|
|
88
|
+
if (existsSync(destPath)) {
|
|
89
|
+
const diskContent = readFileSync(destPath, 'utf-8');
|
|
90
|
+
newFileHashes[destRelative] = hashContent(diskContent);
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
78
94
|
if (!existsSync(srcPath))
|
|
79
95
|
continue;
|
|
80
96
|
const raw = readFileSync(srcPath, 'utf-8');
|
|
@@ -174,6 +190,9 @@ export async function upgrade(projectDir, options = {}) {
|
|
|
174
190
|
// --- Write updated manifest ---
|
|
175
191
|
if (!options.dryRun) {
|
|
176
192
|
const newManifest = buildManifest(SDK_VERSION, targetTier, manifest.context, newFileHashes, highestScaffoldMig);
|
|
193
|
+
if (manifest.excludeFromUpgrade && manifest.excludeFromUpgrade.length > 0) {
|
|
194
|
+
newManifest.excludeFromUpgrade = manifest.excludeFromUpgrade;
|
|
195
|
+
}
|
|
177
196
|
writeManifest(projectDir, newManifest);
|
|
178
197
|
}
|
|
179
198
|
return result;
|
package/package.json
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotificationDropdown.tsx
|
|
3
|
+
*
|
|
4
|
+
* Dropdown panel showing recent notifications with mark-as-read actions.
|
|
5
|
+
*
|
|
6
|
+
* @module dashboard/components/notifications/NotificationDropdown
|
|
7
|
+
* @created 2026-02-03
|
|
8
|
+
* @task task-303.2
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { NotificationItem } from './NotificationItem';
|
|
12
|
+
import type { Notification } from '../../lib/notifications/types';
|
|
13
|
+
|
|
14
|
+
export interface NotificationDropdownProps {
|
|
15
|
+
notifications: Notification[];
|
|
16
|
+
readIds: Set<string>;
|
|
17
|
+
isOpen: boolean;
|
|
18
|
+
onMarkRead: (id: string) => void;
|
|
19
|
+
onMarkAllRead: () => void;
|
|
20
|
+
onClose: () => void;
|
|
21
|
+
loading?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function NotificationDropdown({
|
|
25
|
+
notifications,
|
|
26
|
+
readIds,
|
|
27
|
+
isOpen,
|
|
28
|
+
onMarkRead,
|
|
29
|
+
onMarkAllRead,
|
|
30
|
+
_onClose,
|
|
31
|
+
loading = false,
|
|
32
|
+
}: NotificationDropdownProps): JSX.Element | null {
|
|
33
|
+
if (!isOpen) return null;
|
|
34
|
+
|
|
35
|
+
const hasUnread = notifications.some((n) => !readIds.has(n.id));
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
className="absolute right-0 mt-2 w-80 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-60 overflow-hidden animate-slide-down"
|
|
40
|
+
role="menu"
|
|
41
|
+
aria-orientation="vertical"
|
|
42
|
+
aria-label="Notifications"
|
|
43
|
+
>
|
|
44
|
+
{/* Header */}
|
|
45
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
|
46
|
+
<h3 className="text-sm font-semibold text-gray-900 dark:text-white">Notifications</h3>
|
|
47
|
+
{hasUnread ? (
|
|
48
|
+
<button
|
|
49
|
+
onClick={onMarkAllRead}
|
|
50
|
+
className="text-xs text-blue-600 dark:text-blue-400 hover:underline focus:outline-none"
|
|
51
|
+
>
|
|
52
|
+
Mark all as read
|
|
53
|
+
</button>
|
|
54
|
+
) : (
|
|
55
|
+
<span className="text-xs text-gray-400">All read</span>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{/* Notification list */}
|
|
60
|
+
<div className="max-h-80 overflow-y-auto">
|
|
61
|
+
{loading ? (
|
|
62
|
+
<div className="px-4 py-8 text-center">
|
|
63
|
+
<svg
|
|
64
|
+
className="animate-spin h-6 w-6 mx-auto text-blue-500"
|
|
65
|
+
fill="none"
|
|
66
|
+
viewBox="0 0 24 24"
|
|
67
|
+
>
|
|
68
|
+
<circle
|
|
69
|
+
className="opacity-25"
|
|
70
|
+
cx="12"
|
|
71
|
+
cy="12"
|
|
72
|
+
r="10"
|
|
73
|
+
stroke="currentColor"
|
|
74
|
+
strokeWidth="4"
|
|
75
|
+
/>
|
|
76
|
+
<path
|
|
77
|
+
className="opacity-75"
|
|
78
|
+
fill="currentColor"
|
|
79
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
|
80
|
+
/>
|
|
81
|
+
</svg>
|
|
82
|
+
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">Loading...</p>
|
|
83
|
+
</div>
|
|
84
|
+
) : notifications.length === 0 ? (
|
|
85
|
+
<div className="px-4 py-8 text-center">
|
|
86
|
+
<svg
|
|
87
|
+
className="mx-auto h-8 w-8 text-gray-400"
|
|
88
|
+
fill="none"
|
|
89
|
+
stroke="currentColor"
|
|
90
|
+
viewBox="0 0 24 24"
|
|
91
|
+
>
|
|
92
|
+
<path
|
|
93
|
+
strokeLinecap="round"
|
|
94
|
+
strokeLinejoin="round"
|
|
95
|
+
strokeWidth={2}
|
|
96
|
+
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
|
|
97
|
+
/>
|
|
98
|
+
</svg>
|
|
99
|
+
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">No notifications</p>
|
|
100
|
+
<p className="text-xs text-gray-400 dark:text-gray-500">You're all caught up!</p>
|
|
101
|
+
</div>
|
|
102
|
+
) : (
|
|
103
|
+
<div className="group">
|
|
104
|
+
{notifications.map((notification) => (
|
|
105
|
+
<NotificationItem
|
|
106
|
+
key={notification.id}
|
|
107
|
+
notification={notification}
|
|
108
|
+
isRead={readIds.has(notification.id)}
|
|
109
|
+
onMarkRead={onMarkRead}
|
|
110
|
+
compact
|
|
111
|
+
/>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Footer */}
|
|
118
|
+
<div className="px-4 py-3 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
|
119
|
+
<a
|
|
120
|
+
href="/notifications"
|
|
121
|
+
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
|
|
122
|
+
>
|
|
123
|
+
View all notifications
|
|
124
|
+
</a>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export default NotificationDropdown;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotificationItem.tsx
|
|
3
|
+
*
|
|
4
|
+
* Individual notification rendering with category-based styling,
|
|
5
|
+
* priority colours, and project badges.
|
|
6
|
+
*
|
|
7
|
+
* @module dashboard/components/notifications/NotificationItem
|
|
8
|
+
* @created 2026-02-03
|
|
9
|
+
* @updated 2026-02-04 - Added priority colours and project badges
|
|
10
|
+
* @task task-303.2, task-314
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Notification, NotificationCategory, NotificationPriority } from '../../lib/notifications/types';
|
|
14
|
+
|
|
15
|
+
export interface NotificationItemProps {
|
|
16
|
+
notification: Notification;
|
|
17
|
+
isRead: boolean;
|
|
18
|
+
onMarkRead?: (id: string) => void;
|
|
19
|
+
onDismiss?: (id: string) => void;
|
|
20
|
+
compact?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Category-based icon and colour configuration */
|
|
24
|
+
const categoryConfig: Record<
|
|
25
|
+
NotificationCategory,
|
|
26
|
+
{ icon: string; bgClass: string; iconClass: string }
|
|
27
|
+
> = {
|
|
28
|
+
error: {
|
|
29
|
+
icon: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z',
|
|
30
|
+
bgClass: 'bg-red-100 dark:bg-red-900/30',
|
|
31
|
+
iconClass: 'text-red-600 dark:text-red-400',
|
|
32
|
+
},
|
|
33
|
+
warning: {
|
|
34
|
+
icon: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z',
|
|
35
|
+
bgClass: 'bg-yellow-100 dark:bg-yellow-900/30',
|
|
36
|
+
iconClass: 'text-yellow-600 dark:text-yellow-400',
|
|
37
|
+
},
|
|
38
|
+
info: {
|
|
39
|
+
icon: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
40
|
+
bgClass: 'bg-blue-100 dark:bg-blue-900/30',
|
|
41
|
+
iconClass: 'text-blue-600 dark:text-blue-400',
|
|
42
|
+
},
|
|
43
|
+
success: {
|
|
44
|
+
icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
|
|
45
|
+
bgClass: 'bg-green-100 dark:bg-green-900/30',
|
|
46
|
+
iconClass: 'text-green-600 dark:text-green-400',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/** Priority-based colour configuration */
|
|
51
|
+
const priorityConfig: Record<
|
|
52
|
+
NotificationPriority,
|
|
53
|
+
{ borderClass: string; badgeClass: string; label: string }
|
|
54
|
+
> = {
|
|
55
|
+
critical: {
|
|
56
|
+
borderClass: 'border-l-4 border-l-red-500',
|
|
57
|
+
badgeClass: 'bg-red-100 text-red-700 dark:bg-red-900/50 dark:text-red-300',
|
|
58
|
+
label: 'Critical',
|
|
59
|
+
},
|
|
60
|
+
high: {
|
|
61
|
+
borderClass: 'border-l-4 border-l-orange-500',
|
|
62
|
+
badgeClass: 'bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-300',
|
|
63
|
+
label: 'High',
|
|
64
|
+
},
|
|
65
|
+
medium: {
|
|
66
|
+
borderClass: 'border-l-4 border-l-yellow-500',
|
|
67
|
+
badgeClass: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/50 dark:text-yellow-300',
|
|
68
|
+
label: 'Medium',
|
|
69
|
+
},
|
|
70
|
+
low: {
|
|
71
|
+
borderClass: 'border-l-4 border-l-blue-400',
|
|
72
|
+
badgeClass: 'bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300',
|
|
73
|
+
label: 'Low',
|
|
74
|
+
},
|
|
75
|
+
info: {
|
|
76
|
+
borderClass: 'border-l-4 border-l-gray-300 dark:border-l-gray-600',
|
|
77
|
+
badgeClass: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400',
|
|
78
|
+
label: 'Info',
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Project display configuration */
|
|
83
|
+
const projectConfig: Record<string, { label: string; bgClass: string }> = {
|
|
84
|
+
platform: {
|
|
85
|
+
label: 'Platform',
|
|
86
|
+
bgClass: 'bg-purple-100 text-purple-700 dark:bg-purple-900/50 dark:text-purple-300',
|
|
87
|
+
},
|
|
88
|
+
'brand-copilot': {
|
|
89
|
+
label: 'Brand Copilot',
|
|
90
|
+
bgClass: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:text-indigo-300',
|
|
91
|
+
},
|
|
92
|
+
scout: {
|
|
93
|
+
label: 'Scout',
|
|
94
|
+
bgClass: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300',
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Source display configuration */
|
|
99
|
+
const sourceConfig: Record<string, { label: string; icon: string }> = {
|
|
100
|
+
'error-collector': { label: 'Error', icon: 'M12 9v2m0 4h.01' },
|
|
101
|
+
'pattern-discovery': { label: 'Pattern', icon: 'M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2z' },
|
|
102
|
+
'circuit-breaker': { label: 'Circuit', icon: 'M13 10V3L4 14h7v7l9-11h-7z' },
|
|
103
|
+
usage: { label: 'Usage', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' },
|
|
104
|
+
gatus: { label: 'Monitoring', icon: 'M5.636 18.364a9 9 0 010-12.728m12.728 0a9 9 0 010 12.728m-9.9-2.829a5 5 0 010-7.07m7.072 0a5 5 0 010 7.07M13 12a1 1 0 11-2 0 1 1 0 012 0z' },
|
|
105
|
+
system: { label: 'System', icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z' },
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Format relative time string */
|
|
109
|
+
function formatRelativeTime(timestamp: number): string {
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
const seconds = Math.floor((now - timestamp * 1000) / 1000);
|
|
112
|
+
|
|
113
|
+
if (seconds < 60) return 'Just now';
|
|
114
|
+
if (seconds < 3600) {
|
|
115
|
+
const mins = Math.floor(seconds / 60);
|
|
116
|
+
return `${mins}m ago`;
|
|
117
|
+
}
|
|
118
|
+
if (seconds < 86400) {
|
|
119
|
+
const hours = Math.floor(seconds / 3600);
|
|
120
|
+
return `${hours}h ago`;
|
|
121
|
+
}
|
|
122
|
+
const days = Math.floor(seconds / 86400);
|
|
123
|
+
if (days === 1) return 'Yesterday';
|
|
124
|
+
if (days < 7) return `${days}d ago`;
|
|
125
|
+
return new Date(timestamp * 1000).toLocaleDateString('en-AU', {
|
|
126
|
+
day: 'numeric',
|
|
127
|
+
month: 'short',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function NotificationItem({
|
|
132
|
+
notification,
|
|
133
|
+
isRead,
|
|
134
|
+
onMarkRead,
|
|
135
|
+
onDismiss,
|
|
136
|
+
compact = false,
|
|
137
|
+
}: NotificationItemProps): JSX.Element {
|
|
138
|
+
const catConfig = categoryConfig[notification.category] || categoryConfig.info;
|
|
139
|
+
const prioConfig = priorityConfig[notification.priority] || priorityConfig.info;
|
|
140
|
+
const projConfig = notification.project ? projectConfig[notification.project] : null;
|
|
141
|
+
const srcConfig = sourceConfig[notification.source] || sourceConfig.system;
|
|
142
|
+
|
|
143
|
+
const handleClick = (): void => {
|
|
144
|
+
if (!isRead && onMarkRead) {
|
|
145
|
+
onMarkRead(notification.id);
|
|
146
|
+
}
|
|
147
|
+
if (notification.action_url) {
|
|
148
|
+
window.location.href = notification.action_url;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const handleDismiss = (e: React.MouseEvent): void => {
|
|
153
|
+
e.stopPropagation();
|
|
154
|
+
onDismiss?.(notification.id);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const baseClasses = isRead
|
|
158
|
+
? 'hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
|
159
|
+
: 'bg-blue-50 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-900/30';
|
|
160
|
+
|
|
161
|
+
// Show priority border for critical/high only
|
|
162
|
+
const priorityBorder = ['critical', 'high'].includes(notification.priority)
|
|
163
|
+
? prioConfig.borderClass
|
|
164
|
+
: '';
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<div
|
|
168
|
+
className={`notification-item group flex items-start gap-3 px-4 py-3 cursor-pointer border-b border-gray-100 dark:border-gray-700 transition-colors ${baseClasses} ${priorityBorder}`}
|
|
169
|
+
onClick={handleClick}
|
|
170
|
+
role="button"
|
|
171
|
+
tabIndex={0}
|
|
172
|
+
onKeyDown={(e) => e.key === 'Enter' && handleClick()}
|
|
173
|
+
>
|
|
174
|
+
{/* Category icon */}
|
|
175
|
+
<div
|
|
176
|
+
className={`flex-shrink-0 w-8 h-8 rounded-full ${catConfig.bgClass} flex items-center justify-center`}
|
|
177
|
+
>
|
|
178
|
+
<svg
|
|
179
|
+
className={`w-4 h-4 ${catConfig.iconClass}`}
|
|
180
|
+
fill="none"
|
|
181
|
+
stroke="currentColor"
|
|
182
|
+
viewBox="0 0 24 24"
|
|
183
|
+
>
|
|
184
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={catConfig.icon} />
|
|
185
|
+
</svg>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
{/* Content */}
|
|
189
|
+
<div className="flex-1 min-w-0">
|
|
190
|
+
{/* Title row with priority badge */}
|
|
191
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
192
|
+
<p
|
|
193
|
+
className={`text-sm ${isRead ? 'text-gray-700 dark:text-gray-300' : 'font-medium text-gray-900 dark:text-white'}`}
|
|
194
|
+
>
|
|
195
|
+
{notification.title}
|
|
196
|
+
</p>
|
|
197
|
+
{/* Priority badge for critical/high */}
|
|
198
|
+
{['critical', 'high'].includes(notification.priority) && (
|
|
199
|
+
<span className={`px-1.5 py-0.5 text-xs font-medium rounded ${prioConfig.badgeClass}`}>
|
|
200
|
+
{prioConfig.label}
|
|
201
|
+
</span>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
{/* Description */}
|
|
206
|
+
{notification.description && !compact && (
|
|
207
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 truncate mt-0.5">
|
|
208
|
+
{notification.description}
|
|
209
|
+
</p>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{/* Metadata row: time, project, source */}
|
|
213
|
+
<div className="flex items-center gap-2 mt-1 flex-wrap">
|
|
214
|
+
<span className="text-xs text-gray-400 dark:text-gray-500">
|
|
215
|
+
{formatRelativeTime(notification.created_at)}
|
|
216
|
+
</span>
|
|
217
|
+
|
|
218
|
+
{/* Project badge */}
|
|
219
|
+
{projConfig && (
|
|
220
|
+
<span className={`px-1.5 py-0.5 text-xs rounded ${projConfig.bgClass}`}>
|
|
221
|
+
{projConfig.label}
|
|
222
|
+
</span>
|
|
223
|
+
)}
|
|
224
|
+
|
|
225
|
+
{/* Source indicator */}
|
|
226
|
+
<span className="text-xs text-gray-400 dark:text-gray-500 flex items-center gap-1">
|
|
227
|
+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
228
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={srcConfig.icon} />
|
|
229
|
+
</svg>
|
|
230
|
+
{srcConfig.label}
|
|
231
|
+
</span>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
{/* Unread indicator */}
|
|
236
|
+
{!isRead && <div className="flex-shrink-0 w-2 h-2 mt-2 bg-blue-500 rounded-full" />}
|
|
237
|
+
|
|
238
|
+
{/* Dismiss button (optional) */}
|
|
239
|
+
{onDismiss && (
|
|
240
|
+
<button
|
|
241
|
+
onClick={handleDismiss}
|
|
242
|
+
className="flex-shrink-0 p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
243
|
+
aria-label="Dismiss notification"
|
|
244
|
+
>
|
|
245
|
+
<svg
|
|
246
|
+
className="w-4 h-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
|
247
|
+
fill="none"
|
|
248
|
+
stroke="currentColor"
|
|
249
|
+
viewBox="0 0 24 24"
|
|
250
|
+
>
|
|
251
|
+
<path
|
|
252
|
+
strokeLinecap="round"
|
|
253
|
+
strokeLinejoin="round"
|
|
254
|
+
strokeWidth={2}
|
|
255
|
+
d="M6 18L18 6M6 6l12 12"
|
|
256
|
+
/>
|
|
257
|
+
</svg>
|
|
258
|
+
</button>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export default NotificationItem;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PatternInfoButton Component
|
|
3
|
+
* Info icon button that toggles a panel explaining how pattern discovery works
|
|
4
|
+
*/
|
|
5
|
+
import { useState, useRef, useEffect } from 'react';
|
|
6
|
+
|
|
7
|
+
export function PatternInfoButton() {
|
|
8
|
+
const [open, setOpen] = useState(false);
|
|
9
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!open) return;
|
|
13
|
+
|
|
14
|
+
function handleClickOutside(event: MouseEvent) {
|
|
15
|
+
if (panelRef.current && !panelRef.current.contains(event.target as Node)) {
|
|
16
|
+
setOpen(false);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
21
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
22
|
+
}, [open]);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="relative" ref={panelRef}>
|
|
26
|
+
<button
|
|
27
|
+
onClick={() => setOpen(!open)}
|
|
28
|
+
className="inline-flex items-center justify-center w-9 h-9 rounded-full bg-blue-100 hover:bg-blue-200 dark:bg-blue-900/40 dark:hover:bg-blue-900/60 text-blue-600 dark:text-blue-400 transition-colors"
|
|
29
|
+
title="How Pattern Discovery Works"
|
|
30
|
+
aria-label="How Pattern Discovery Works"
|
|
31
|
+
>
|
|
32
|
+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
33
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
34
|
+
</svg>
|
|
35
|
+
</button>
|
|
36
|
+
|
|
37
|
+
{open && (
|
|
38
|
+
<div className="absolute right-0 top-12 z-50 w-96 bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-800 rounded-lg p-4 shadow-lg">
|
|
39
|
+
<h3 className="font-semibold text-blue-800 dark:text-blue-300 mb-2">
|
|
40
|
+
How Pattern Discovery Works
|
|
41
|
+
</h3>
|
|
42
|
+
<ol className="text-sm text-blue-700 dark:text-blue-400 space-y-1.5 list-decimal list-inside">
|
|
43
|
+
<li>Daily cron job (2am UTC) analyses unclassified errors in D1</li>
|
|
44
|
+
<li>Similar errors are clustered and sent to AI (DeepSeek) for pattern suggestions</li>
|
|
45
|
+
<li>AI suggests patterns using a constrained DSL (contains, startsWith, statusCode, regex)</li>
|
|
46
|
+
<li>Human reviews and approves/rejects suggestions with backtest validation</li>
|
|
47
|
+
<li>Approved patterns are cached in KV and loaded by error-collector at runtime</li>
|
|
48
|
+
<li>System patterns are always active (built into error-collector code)</li>
|
|
49
|
+
</ol>
|
|
50
|
+
<button
|
|
51
|
+
onClick={() => setOpen(false)}
|
|
52
|
+
className="mt-3 text-xs text-blue-600 dark:text-blue-400 hover:underline"
|
|
53
|
+
>
|
|
54
|
+
Close
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|