costhawk 1.0.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/.claude/settings.local.json +32 -0
- package/STRATEGIC_PLAN_2025-12-01.md +934 -0
- package/costcanary/.claude/settings.local.json +9 -0
- package/costcanary/.env.production.template +38 -0
- package/costcanary/.eslintrc.json +22 -0
- package/costcanary/.nvmrc +1 -0
- package/costcanary/.prettierignore +11 -0
- package/costcanary/.prettierrc.json +12 -0
- package/costcanary/ADMIN_SETUP.md +68 -0
- package/costcanary/CLAUDE.md +228 -0
- package/costcanary/CLERK_SETUP.md +69 -0
- package/costcanary/DATABASE_SETUP.md +136 -0
- package/costcanary/DEMO_CHECKLIST.md +62 -0
- package/costcanary/DEPLOYMENT.md +31 -0
- package/costcanary/PRODUCTION_RECOVERY.md +109 -0
- package/costcanary/README.md +247 -0
- package/costcanary/STRIPE_SECURITY_AUDIT.md +123 -0
- package/costcanary/TESTING_ADMIN.md +92 -0
- package/costcanary/app/(auth)/sign-in/[[...sign-in]]/page.tsx +25 -0
- package/costcanary/app/(auth)/sign-up/[[...sign-up]]/page.tsx +25 -0
- package/costcanary/app/(dashboard)/dashboard/admin/page.tsx +260 -0
- package/costcanary/app/(dashboard)/dashboard/alerts/page.tsx +64 -0
- package/costcanary/app/(dashboard)/dashboard/api-keys/page.tsx +231 -0
- package/costcanary/app/(dashboard)/dashboard/billing/page.tsx +349 -0
- package/costcanary/app/(dashboard)/dashboard/layout.tsx +188 -0
- package/costcanary/app/(dashboard)/dashboard/page.tsx +13 -0
- package/costcanary/app/(dashboard)/dashboard/playground/page.tsx +605 -0
- package/costcanary/app/(dashboard)/dashboard/settings/page.tsx +86 -0
- package/costcanary/app/(dashboard)/dashboard/usage/page.tsx +354 -0
- package/costcanary/app/(dashboard)/dashboard/wrapped-keys/page.tsx +677 -0
- package/costcanary/app/(marketing)/page.tsx +90 -0
- package/costcanary/app/(marketing)/pricing/page.tsx +272 -0
- package/costcanary/app/admin/pricing-status/page.tsx +338 -0
- package/costcanary/app/api/admin/check-pricing/route.ts +127 -0
- package/costcanary/app/api/admin/debug/route.ts +44 -0
- package/costcanary/app/api/admin/fix-pricing/route.ts +216 -0
- package/costcanary/app/api/admin/pricing-jobs/[jobId]/route.ts +48 -0
- package/costcanary/app/api/admin/pricing-jobs/route.ts +45 -0
- package/costcanary/app/api/admin/trigger-pricing/route.ts +209 -0
- package/costcanary/app/api/admin/whoami/route.ts +44 -0
- package/costcanary/app/api/auth/clerk/[...nextjs]/route.ts +93 -0
- package/costcanary/app/api/debug/wrapped-key/route.ts +51 -0
- package/costcanary/app/api/debug-status/route.ts +9 -0
- package/costcanary/app/api/debug-version/route.ts +12 -0
- package/costcanary/app/api/health/route.ts +14 -0
- package/costcanary/app/api/health-simple/route.ts +18 -0
- package/costcanary/app/api/keys/route.ts +162 -0
- package/costcanary/app/api/keys/wrapped/[id]/revoke/route.ts +86 -0
- package/costcanary/app/api/keys/wrapped/[id]/rotate/route.ts +81 -0
- package/costcanary/app/api/keys/wrapped/route.ts +241 -0
- package/costcanary/app/api/optimizer/preview/route.ts +147 -0
- package/costcanary/app/api/optimizer/route.ts +118 -0
- package/costcanary/app/api/pricing/models/route.ts +102 -0
- package/costcanary/app/api/proxy/[...path]/route.ts +391 -0
- package/costcanary/app/api/proxy/anthropic/route.ts +539 -0
- package/costcanary/app/api/proxy/google/route.ts +395 -0
- package/costcanary/app/api/proxy/openai/route.ts +529 -0
- package/costcanary/app/api/simple-test/route.ts +7 -0
- package/costcanary/app/api/stripe/checkout/route.ts +201 -0
- package/costcanary/app/api/stripe/webhook/route.ts +392 -0
- package/costcanary/app/api/test-connection/route.ts +209 -0
- package/costcanary/app/api/test-proxy/route.ts +7 -0
- package/costcanary/app/api/test-simple/route.ts +20 -0
- package/costcanary/app/api/usage/current/route.ts +112 -0
- package/costcanary/app/api/usage/stats/route.ts +129 -0
- package/costcanary/app/api/usage/stream/route.ts +113 -0
- package/costcanary/app/api/usage/summary/route.ts +67 -0
- package/costcanary/app/api/usage/trend/route.ts +119 -0
- package/costcanary/app/api/ws/route.ts +23 -0
- package/costcanary/app/globals.css +280 -0
- package/costcanary/app/layout.tsx +87 -0
- package/costcanary/components/Header.tsx +85 -0
- package/costcanary/components/dashboard/AddApiKeyModal.tsx +264 -0
- package/costcanary/components/dashboard/dashboard-content.tsx +329 -0
- package/costcanary/components/landing/DashboardPreview.tsx +222 -0
- package/costcanary/components/landing/Features.tsx +238 -0
- package/costcanary/components/landing/Footer.tsx +83 -0
- package/costcanary/components/landing/Hero.tsx +193 -0
- package/costcanary/components/landing/Pricing.tsx +250 -0
- package/costcanary/components/landing/Testimonials.tsx +248 -0
- package/costcanary/components/theme-provider.tsx +8 -0
- package/costcanary/components/ui/alert.tsx +59 -0
- package/costcanary/components/ui/badge.tsx +36 -0
- package/costcanary/components/ui/button.tsx +56 -0
- package/costcanary/components/ui/card.tsx +79 -0
- package/costcanary/components/ui/dialog.tsx +122 -0
- package/costcanary/components/ui/input.tsx +22 -0
- package/costcanary/components/ui/label.tsx +26 -0
- package/costcanary/components/ui/progress.tsx +28 -0
- package/costcanary/components/ui/select.tsx +160 -0
- package/costcanary/components/ui/separator.tsx +31 -0
- package/costcanary/components/ui/switch.tsx +29 -0
- package/costcanary/components/ui/tabs.tsx +55 -0
- package/costcanary/components/ui/toast.tsx +127 -0
- package/costcanary/components/ui/toaster.tsx +35 -0
- package/costcanary/components/ui/use-toast.ts +189 -0
- package/costcanary/components.json +17 -0
- package/costcanary/debug-wrapped-keys.md +117 -0
- package/costcanary/fix-console.sh +30 -0
- package/costcanary/lib/admin-auth.ts +226 -0
- package/costcanary/lib/admin-security.ts +124 -0
- package/costcanary/lib/audit-events.ts +62 -0
- package/costcanary/lib/audit.ts +158 -0
- package/costcanary/lib/chart-colors.ts +152 -0
- package/costcanary/lib/cost-calculator.ts +212 -0
- package/costcanary/lib/db-utils.ts +325 -0
- package/costcanary/lib/db.ts +14 -0
- package/costcanary/lib/encryption.ts +120 -0
- package/costcanary/lib/kms.ts +358 -0
- package/costcanary/lib/model-alias.ts +180 -0
- package/costcanary/lib/pricing.ts +292 -0
- package/costcanary/lib/prisma.ts +52 -0
- package/costcanary/lib/railway-db.ts +157 -0
- package/costcanary/lib/sse-parser.ts +283 -0
- package/costcanary/lib/stripe-client.ts +81 -0
- package/costcanary/lib/stripe-server.ts +52 -0
- package/costcanary/lib/tokens.ts +396 -0
- package/costcanary/lib/usage-limits.ts +164 -0
- package/costcanary/lib/utils.ts +6 -0
- package/costcanary/lib/websocket.ts +153 -0
- package/costcanary/lib/wrapped-keys.ts +531 -0
- package/costcanary/market-research.md +443 -0
- package/costcanary/middleware.ts +48 -0
- package/costcanary/next.config.js +43 -0
- package/costcanary/nia-sources.md +151 -0
- package/costcanary/package-lock.json +12162 -0
- package/costcanary/package.json +92 -0
- package/costcanary/package.json.backup +89 -0
- package/costcanary/postcss.config.js +6 -0
- package/costcanary/pricing-worker/.env.example +8 -0
- package/costcanary/pricing-worker/README.md +81 -0
- package/costcanary/pricing-worker/package-lock.json +1109 -0
- package/costcanary/pricing-worker/package.json +26 -0
- package/costcanary/pricing-worker/railway.json +13 -0
- package/costcanary/pricing-worker/schema.prisma +326 -0
- package/costcanary/pricing-worker/src/index.ts +115 -0
- package/costcanary/pricing-worker/src/services/pricing-updater.ts +79 -0
- package/costcanary/pricing-worker/src/services/tavily-client.ts +474 -0
- package/costcanary/pricing-worker/test-tavily.ts +47 -0
- package/costcanary/pricing-worker/tsconfig.json +24 -0
- package/costcanary/prisma/migrations/001_add_stripe_fields.sql +26 -0
- package/costcanary/prisma/schema.prisma +326 -0
- package/costcanary/prisma/seed-pricing.ts +133 -0
- package/costcanary/public/costhawk-logo.png +0 -0
- package/costcanary/railway.json +30 -0
- package/costcanary/railway.toml +16 -0
- package/costcanary/research-nia.md +298 -0
- package/costcanary/research.md +411 -0
- package/costcanary/scripts/build-production.js +65 -0
- package/costcanary/scripts/check-current-pricing.ts +51 -0
- package/costcanary/scripts/check-pricing-data.ts +174 -0
- package/costcanary/scripts/create-stripe-prices.js +49 -0
- package/costcanary/scripts/fix-pricing-data.ts +135 -0
- package/costcanary/scripts/fix-pricing-db.ts +148 -0
- package/costcanary/scripts/postinstall.js +58 -0
- package/costcanary/scripts/railway-deploy.sh +52 -0
- package/costcanary/scripts/run-migration.js +61 -0
- package/costcanary/scripts/start-production.js +175 -0
- package/costcanary/scripts/test-wrapped-key.ts +85 -0
- package/costcanary/scripts/validate-deployment.js +176 -0
- package/costcanary/scripts/validate-production.js +119 -0
- package/costcanary/server.js.backup +38 -0
- package/costcanary/tailwind.config.ts +216 -0
- package/costcanary/test-pricing-status.sh +27 -0
- package/costcanary/tsconfig.json +42 -0
- package/docs/sessions/session-2025-12-01.md +570 -0
- package/executive-summary.md +302 -0
- package/index.js +1 -0
- package/nia-sources.md +163 -0
- package/package.json +16 -0
- package/research.md +750 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import { Badge } from '@/components/ui/badge';
|
|
6
|
+
import { PlayCircle, Clock, CheckCircle, XCircle, Loader2 } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
interface Job {
|
|
9
|
+
id: string;
|
|
10
|
+
status: string;
|
|
11
|
+
duration: string;
|
|
12
|
+
startedAt: string;
|
|
13
|
+
completedAt: string;
|
|
14
|
+
trigger: string;
|
|
15
|
+
successCount: number;
|
|
16
|
+
failureCount: number;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function AdminPage() {
|
|
21
|
+
const [isTriggering, setIsTriggering] = useState(false);
|
|
22
|
+
const [jobs, setJobs] = useState<Job[]>([]);
|
|
23
|
+
const [adminStatus, setAdminStatus] = useState<Record<string, unknown> | null>(null);
|
|
24
|
+
const [lastResult, setLastResult] = useState<Record<string, unknown> | null>(null);
|
|
25
|
+
|
|
26
|
+
const checkAdminStatus = async () => {
|
|
27
|
+
try {
|
|
28
|
+
const response = await fetch('/api/admin/trigger-pricing');
|
|
29
|
+
const data = await response.json();
|
|
30
|
+
setAdminStatus(data);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Failed to check admin status:', error);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const triggerPricingDiscovery = async (testMode: boolean = true, provider?: string) => {
|
|
37
|
+
setIsTriggering(true);
|
|
38
|
+
setLastResult(null);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch('/api/admin/trigger-pricing', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({ testMode, provider }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const result = await response.json();
|
|
50
|
+
setLastResult(result);
|
|
51
|
+
|
|
52
|
+
if (result.success) {
|
|
53
|
+
// Refresh jobs list
|
|
54
|
+
await loadJobs();
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('Failed to trigger pricing discovery:', error);
|
|
58
|
+
setLastResult({ error: 'Failed to trigger pricing discovery' });
|
|
59
|
+
} finally {
|
|
60
|
+
setIsTriggering(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const loadJobs = async () => {
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch('/api/admin/pricing-jobs');
|
|
67
|
+
const data = await response.json();
|
|
68
|
+
setJobs(data.jobs || []);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('Failed to load jobs:', error);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const getStatusIcon = (status: string) => {
|
|
75
|
+
switch (status) {
|
|
76
|
+
case 'COMPLETED': return <CheckCircle className="w-4 h-4 text-green-500" />;
|
|
77
|
+
case 'FAILED': return <XCircle className="w-4 h-4 text-red-500" />;
|
|
78
|
+
case 'RUNNING': return <Loader2 className="w-4 h-4 text-blue-500 animate-spin" />;
|
|
79
|
+
default: return <Clock className="w-4 h-4 text-yellow-500" />;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const getStatusColor = (status: string) => {
|
|
84
|
+
switch (status) {
|
|
85
|
+
case 'COMPLETED': return 'bg-green-100 text-green-800';
|
|
86
|
+
case 'FAILED': return 'bg-red-100 text-red-800';
|
|
87
|
+
case 'RUNNING': return 'bg-blue-100 text-blue-800';
|
|
88
|
+
default: return 'bg-yellow-100 text-yellow-800';
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className="max-w-6xl mx-auto space-y-8">
|
|
94
|
+
<div>
|
|
95
|
+
<h1 className="text-3xl font-bold text-white mb-2">Admin Panel</h1>
|
|
96
|
+
<p className="text-slate-400">Manage pricing discovery and view admin operations</p>
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
{/* Admin Status */}
|
|
100
|
+
<div className="bg-slate-900 border border-slate-700 rounded-lg p-6">
|
|
101
|
+
<div className="mb-4">
|
|
102
|
+
<h2 className="text-xl font-semibold text-white">Admin Status</h2>
|
|
103
|
+
<p className="text-slate-400">Your current admin permissions and access level</p>
|
|
104
|
+
</div>
|
|
105
|
+
<div>
|
|
106
|
+
<div className="flex gap-4 mb-4">
|
|
107
|
+
<Button onClick={checkAdminStatus} variant="outline" size="sm">
|
|
108
|
+
Check Status
|
|
109
|
+
</Button>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{adminStatus && (
|
|
113
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
114
|
+
<div>
|
|
115
|
+
<div className="text-sm text-slate-400">Admin Access</div>
|
|
116
|
+
<Badge variant={(adminStatus.isAdmin as boolean) ? "default" : "destructive"}>
|
|
117
|
+
{(adminStatus.isAdmin as boolean) ? "✅ Admin" : "❌ Not Admin"}
|
|
118
|
+
</Badge>
|
|
119
|
+
</div>
|
|
120
|
+
<div>
|
|
121
|
+
<div className="text-sm text-slate-400">Role</div>
|
|
122
|
+
<div className="text-white">{(adminStatus.role as string) || 'None'}</div>
|
|
123
|
+
</div>
|
|
124
|
+
<div>
|
|
125
|
+
<div className="text-sm text-slate-400">Can Trigger Pricing</div>
|
|
126
|
+
<Badge variant={(adminStatus.canTriggerPricing as boolean) ? "default" : "destructive"}>
|
|
127
|
+
{(adminStatus.canTriggerPricing as boolean) ? "✅ Yes" : "❌ No"}
|
|
128
|
+
</Badge>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Trigger Controls */}
|
|
136
|
+
<div className="bg-slate-900 border border-slate-700 rounded-lg p-6">
|
|
137
|
+
<div className="mb-4">
|
|
138
|
+
<h2 className="text-xl font-semibold text-white flex items-center gap-2">
|
|
139
|
+
<PlayCircle className="w-5 h-5" />
|
|
140
|
+
Pricing Discovery
|
|
141
|
+
</h2>
|
|
142
|
+
<p className="text-slate-400">Manually trigger AI-powered pricing discovery jobs</p>
|
|
143
|
+
</div>
|
|
144
|
+
<div>
|
|
145
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
146
|
+
<div className="space-y-3">
|
|
147
|
+
<h3 className="text-lg font-semibold text-white">Test Mode</h3>
|
|
148
|
+
<p className="text-sm text-slate-400">Uses simulated data for testing</p>
|
|
149
|
+
<div className="space-y-2">
|
|
150
|
+
<Button
|
|
151
|
+
onClick={() => triggerPricingDiscovery(true)}
|
|
152
|
+
disabled={isTriggering}
|
|
153
|
+
className="w-full"
|
|
154
|
+
>
|
|
155
|
+
{isTriggering ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : null}
|
|
156
|
+
Test All Providers
|
|
157
|
+
</Button>
|
|
158
|
+
<Button
|
|
159
|
+
onClick={() => triggerPricingDiscovery(true, 'OpenAI')}
|
|
160
|
+
disabled={isTriggering}
|
|
161
|
+
variant="outline"
|
|
162
|
+
className="w-full"
|
|
163
|
+
>
|
|
164
|
+
Test OpenAI Only
|
|
165
|
+
</Button>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<div className="space-y-3">
|
|
170
|
+
<h3 className="text-lg font-semibold text-white">Live Mode</h3>
|
|
171
|
+
<p className="text-sm text-slate-400">Uses real Tavily API to discover pricing</p>
|
|
172
|
+
<div className="space-y-2">
|
|
173
|
+
<Button
|
|
174
|
+
onClick={() => triggerPricingDiscovery(false)}
|
|
175
|
+
disabled={isTriggering}
|
|
176
|
+
variant="destructive"
|
|
177
|
+
className="w-full"
|
|
178
|
+
>
|
|
179
|
+
{isTriggering ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : null}
|
|
180
|
+
🔥 Live Discovery (All)
|
|
181
|
+
</Button>
|
|
182
|
+
<Button
|
|
183
|
+
onClick={() => triggerPricingDiscovery(false, 'OpenAI')}
|
|
184
|
+
disabled={isTriggering}
|
|
185
|
+
variant="outline"
|
|
186
|
+
className="w-full"
|
|
187
|
+
>
|
|
188
|
+
Live OpenAI Only
|
|
189
|
+
</Button>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{/* Last Result */}
|
|
195
|
+
{lastResult && (
|
|
196
|
+
<div className="mt-6 p-4 rounded-lg bg-slate-800 border border-slate-600">
|
|
197
|
+
<h4 className="text-white font-medium mb-2">Last Result</h4>
|
|
198
|
+
<pre className="text-sm text-slate-300 overflow-auto">
|
|
199
|
+
{JSON.stringify(lastResult, null, 2)}
|
|
200
|
+
</pre>
|
|
201
|
+
</div>
|
|
202
|
+
)}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{/* Jobs List */}
|
|
207
|
+
<div className="bg-slate-900 border border-slate-700 rounded-lg p-6">
|
|
208
|
+
<div className="mb-4">
|
|
209
|
+
<h2 className="text-xl font-semibold text-white">Recent Jobs</h2>
|
|
210
|
+
<p className="text-slate-400">History of pricing discovery operations</p>
|
|
211
|
+
</div>
|
|
212
|
+
<div>
|
|
213
|
+
<div className="flex gap-4 mb-4">
|
|
214
|
+
<Button onClick={loadJobs} variant="outline" size="sm">
|
|
215
|
+
Refresh Jobs
|
|
216
|
+
</Button>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{jobs.length === 0 ? (
|
|
220
|
+
<div className="text-center py-8 text-slate-400">
|
|
221
|
+
No jobs found. Trigger a pricing discovery to see results here.
|
|
222
|
+
</div>
|
|
223
|
+
) : (
|
|
224
|
+
<div className="space-y-3">
|
|
225
|
+
{jobs.map((job) => (
|
|
226
|
+
<div key={job.id} className="flex items-center justify-between p-4 rounded-lg bg-slate-800 border border-slate-600">
|
|
227
|
+
<div className="flex items-center gap-3">
|
|
228
|
+
{getStatusIcon(job.status)}
|
|
229
|
+
<div>
|
|
230
|
+
<div className="text-white font-medium">
|
|
231
|
+
Job {job.id.slice(-8)}
|
|
232
|
+
</div>
|
|
233
|
+
<div className="text-sm text-slate-400">
|
|
234
|
+
{job.trigger} • {job.duration}
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<div className="flex items-center gap-3">
|
|
240
|
+
<div className="text-right">
|
|
241
|
+
<div className="text-sm text-green-400">
|
|
242
|
+
✅ {job.successCount} success
|
|
243
|
+
</div>
|
|
244
|
+
<div className="text-sm text-red-400">
|
|
245
|
+
❌ {job.failureCount} failed
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
<Badge className={getStatusColor(job.status)}>
|
|
249
|
+
{job.status}
|
|
250
|
+
</Badge>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
))}
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { motion } from "framer-motion"
|
|
4
|
+
import { Bell, AlertTriangle, TrendingUp } from "lucide-react"
|
|
5
|
+
|
|
6
|
+
export default function AlertsPage() {
|
|
7
|
+
return (
|
|
8
|
+
<div className="space-y-6">
|
|
9
|
+
{/* Header */}
|
|
10
|
+
<div>
|
|
11
|
+
<h1 className="text-3xl font-bold text-white">Budget Alerts</h1>
|
|
12
|
+
<p className="mt-2 text-slate-400">
|
|
13
|
+
Set up alerts to monitor your API spending
|
|
14
|
+
</p>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
{/* Alert Settings */}
|
|
18
|
+
<motion.div
|
|
19
|
+
initial={{ opacity: 0, y: 20 }}
|
|
20
|
+
animate={{ opacity: 1, y: 0 }}
|
|
21
|
+
className="rounded-lg border border-slate-800 bg-slate-900/50 p-6 backdrop-blur-sm"
|
|
22
|
+
>
|
|
23
|
+
<h2 className="mb-4 text-lg font-medium text-white">Alert Preferences</h2>
|
|
24
|
+
<div className="space-y-4">
|
|
25
|
+
<label className="flex items-center justify-between">
|
|
26
|
+
<div className="flex items-center space-x-3">
|
|
27
|
+
<AlertTriangle className="h-5 w-5 text-yellow-500" />
|
|
28
|
+
<div>
|
|
29
|
+
<p className="font-medium text-white">Budget Warning</p>
|
|
30
|
+
<p className="text-sm text-slate-400">Alert when usage reaches 80% of budget</p>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<input type="checkbox" defaultChecked className="h-5 w-5 rounded" />
|
|
34
|
+
</label>
|
|
35
|
+
|
|
36
|
+
<label className="flex items-center justify-between">
|
|
37
|
+
<div className="flex items-center space-x-3">
|
|
38
|
+
<TrendingUp className="h-5 w-5 text-orange-500" />
|
|
39
|
+
<div>
|
|
40
|
+
<p className="font-medium text-white">Unusual Activity</p>
|
|
41
|
+
<p className="text-sm text-slate-400">Alert on sudden usage spikes</p>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
<input type="checkbox" defaultChecked className="h-5 w-5 rounded" />
|
|
45
|
+
</label>
|
|
46
|
+
</div>
|
|
47
|
+
</motion.div>
|
|
48
|
+
|
|
49
|
+
{/* Coming Soon */}
|
|
50
|
+
<motion.div
|
|
51
|
+
initial={{ opacity: 0, y: 20 }}
|
|
52
|
+
animate={{ opacity: 1, y: 0 }}
|
|
53
|
+
transition={{ delay: 0.1 }}
|
|
54
|
+
className="rounded-lg border border-dashed border-slate-700 bg-slate-900/30 p-12 text-center"
|
|
55
|
+
>
|
|
56
|
+
<Bell className="mx-auto h-12 w-12 text-slate-600" />
|
|
57
|
+
<h3 className="mt-4 text-lg font-medium text-white">Advanced Alerts Coming Soon</h3>
|
|
58
|
+
<p className="mt-2 text-slate-400">
|
|
59
|
+
Custom thresholds, email notifications, Slack integration, and more
|
|
60
|
+
</p>
|
|
61
|
+
</motion.div>
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react"
|
|
4
|
+
import { motion } from "framer-motion"
|
|
5
|
+
import { Plus, Shield, Trash2, Copy, CheckCircle, AlertCircle, Clock, Key } from "lucide-react"
|
|
6
|
+
import { AddApiKeyModal } from "@/components/dashboard/AddApiKeyModal"
|
|
7
|
+
|
|
8
|
+
interface ApiKey {
|
|
9
|
+
id: string
|
|
10
|
+
provider: string
|
|
11
|
+
alias: string | null
|
|
12
|
+
masked: string
|
|
13
|
+
isActive: boolean
|
|
14
|
+
lastUsed: string | null
|
|
15
|
+
createdAt: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function ApiKeysPage() {
|
|
19
|
+
const [keys, setKeys] = useState<ApiKey[]>([])
|
|
20
|
+
const [loading, setLoading] = useState(true)
|
|
21
|
+
const [showAddModal, setShowAddModal] = useState(false)
|
|
22
|
+
const [copiedUrl, setCopiedUrl] = useState<string | null>(null)
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
fetchKeys()
|
|
26
|
+
}, [])
|
|
27
|
+
|
|
28
|
+
const fetchKeys = async () => {
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch("/api/keys")
|
|
31
|
+
const data = await response.json()
|
|
32
|
+
setKeys(data.keys || [])
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error("Failed to fetch API keys:", error)
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const handleDeleteKey = async (keyId: string) => {
|
|
41
|
+
if (!confirm("Are you sure you want to delete this API key? This action cannot be undone.")) {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(`/api/keys?id=${keyId}`, {
|
|
47
|
+
method: "DELETE",
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
if (response.ok) {
|
|
51
|
+
setKeys(keys.filter(key => key.id !== keyId))
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.error("Failed to delete API key:", error)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const copyProxyUrl = (provider: string) => {
|
|
59
|
+
const baseUrl = window.location.origin
|
|
60
|
+
const proxyUrl = `${baseUrl}/api/proxy/${provider.toLowerCase()}/`
|
|
61
|
+
navigator.clipboard.writeText(proxyUrl)
|
|
62
|
+
setCopiedUrl(provider)
|
|
63
|
+
setTimeout(() => setCopiedUrl(null), 2000)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const getProviderIcon = (provider: string) => {
|
|
67
|
+
const icons: Record<string, string> = {
|
|
68
|
+
OPENAI: "🤖",
|
|
69
|
+
ANTHROPIC: "🧠",
|
|
70
|
+
GOOGLE: "🔍",
|
|
71
|
+
GROK: "🚀",
|
|
72
|
+
}
|
|
73
|
+
return icons[provider] || "🔑"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const getStatusIcon = (lastUsed: string | null) => {
|
|
77
|
+
if (!lastUsed) return <Clock className="h-4 w-4 text-slate-500" />
|
|
78
|
+
|
|
79
|
+
const lastUsedDate = new Date(lastUsed)
|
|
80
|
+
const daysSinceUsed = Math.floor((Date.now() - lastUsedDate.getTime()) / (1000 * 60 * 60 * 24))
|
|
81
|
+
|
|
82
|
+
if (daysSinceUsed < 1) {
|
|
83
|
+
return <CheckCircle className="h-4 w-4 text-green-500" />
|
|
84
|
+
} else if (daysSinceUsed < 7) {
|
|
85
|
+
return <Clock className="h-4 w-4 text-yellow-500" />
|
|
86
|
+
} else {
|
|
87
|
+
return <AlertCircle className="h-4 w-4 text-orange-500" />
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<>
|
|
93
|
+
<div className="space-y-6">
|
|
94
|
+
{/* Header */}
|
|
95
|
+
<div className="flex items-center justify-between">
|
|
96
|
+
<div>
|
|
97
|
+
<h1 className="text-3xl font-bold text-white">API Keys</h1>
|
|
98
|
+
<p className="mt-2 text-slate-400">
|
|
99
|
+
Manage your API keys securely. All keys are encrypted with AES-256.
|
|
100
|
+
</p>
|
|
101
|
+
</div>
|
|
102
|
+
<motion.button
|
|
103
|
+
whileHover={{ scale: 1.05 }}
|
|
104
|
+
whileTap={{ scale: 0.95 }}
|
|
105
|
+
onClick={() => setShowAddModal(true)}
|
|
106
|
+
className="flex items-center space-x-2 rounded-lg bg-primary-600 px-4 py-2 text-white transition-colors hover:bg-primary-700"
|
|
107
|
+
>
|
|
108
|
+
<Plus className="h-5 w-5" />
|
|
109
|
+
<span>Add API Key</span>
|
|
110
|
+
</motion.button>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
{/* API Keys List */}
|
|
114
|
+
{loading ? (
|
|
115
|
+
<div className="space-y-4">
|
|
116
|
+
{[1, 2, 3].map((i) => (
|
|
117
|
+
<div key={i} className="h-24 animate-pulse rounded-lg bg-slate-900" />
|
|
118
|
+
))}
|
|
119
|
+
</div>
|
|
120
|
+
) : keys.length === 0 ? (
|
|
121
|
+
<motion.div
|
|
122
|
+
initial={{ opacity: 0, y: 20 }}
|
|
123
|
+
animate={{ opacity: 1, y: 0 }}
|
|
124
|
+
className="rounded-lg border border-slate-800 bg-slate-900/50 p-12 text-center backdrop-blur-sm"
|
|
125
|
+
>
|
|
126
|
+
<Key className="mx-auto h-12 w-12 text-slate-600" />
|
|
127
|
+
<h3 className="mt-4 text-lg font-medium text-white">No API Keys</h3>
|
|
128
|
+
<p className="mt-2 text-slate-400">
|
|
129
|
+
Add your first API key to start tracking costs
|
|
130
|
+
</p>
|
|
131
|
+
<motion.button
|
|
132
|
+
whileHover={{ scale: 1.05 }}
|
|
133
|
+
whileTap={{ scale: 0.95 }}
|
|
134
|
+
onClick={() => setShowAddModal(true)}
|
|
135
|
+
className="mt-6 rounded-lg bg-primary-600 px-4 py-2 text-white transition-colors hover:bg-primary-700"
|
|
136
|
+
>
|
|
137
|
+
Add Your First Key
|
|
138
|
+
</motion.button>
|
|
139
|
+
</motion.div>
|
|
140
|
+
) : (
|
|
141
|
+
<div className="space-y-4">
|
|
142
|
+
{keys.map((key, index) => (
|
|
143
|
+
<motion.div
|
|
144
|
+
key={key.id}
|
|
145
|
+
initial={{ opacity: 0, y: 20 }}
|
|
146
|
+
animate={{ opacity: 1, y: 0 }}
|
|
147
|
+
transition={{ delay: index * 0.05 }}
|
|
148
|
+
className="group rounded-lg border border-slate-800 bg-slate-900/50 p-6 backdrop-blur-sm transition-all hover:border-slate-700 hover:bg-slate-900/70"
|
|
149
|
+
>
|
|
150
|
+
<div className="flex items-start justify-between">
|
|
151
|
+
<div className="flex items-start space-x-4">
|
|
152
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-slate-800 text-2xl">
|
|
153
|
+
{getProviderIcon(key.provider)}
|
|
154
|
+
</div>
|
|
155
|
+
<div>
|
|
156
|
+
<div className="flex items-center space-x-2">
|
|
157
|
+
<h3 className="text-lg font-medium text-white">
|
|
158
|
+
{key.provider}
|
|
159
|
+
</h3>
|
|
160
|
+
{key.alias && (
|
|
161
|
+
<span className="rounded-full bg-slate-800 px-2 py-1 text-xs text-slate-400">
|
|
162
|
+
{key.alias}
|
|
163
|
+
</span>
|
|
164
|
+
)}
|
|
165
|
+
{getStatusIcon(key.lastUsed)}
|
|
166
|
+
</div>
|
|
167
|
+
<div className="mt-1 flex items-center space-x-2 text-sm text-slate-400">
|
|
168
|
+
<Shield className="h-3 w-3" />
|
|
169
|
+
<span className="font-mono">{key.masked}</span>
|
|
170
|
+
</div>
|
|
171
|
+
<div className="mt-2 text-xs text-slate-500">
|
|
172
|
+
Added {new Date(key.createdAt).toLocaleDateString()}
|
|
173
|
+
{key.lastUsed && (
|
|
174
|
+
<> • Last used {new Date(key.lastUsed).toLocaleDateString()}</>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div className="flex space-x-2 opacity-0 transition-opacity group-hover:opacity-100">
|
|
181
|
+
<motion.button
|
|
182
|
+
whileHover={{ scale: 1.1 }}
|
|
183
|
+
whileTap={{ scale: 0.9 }}
|
|
184
|
+
onClick={() => copyProxyUrl(key.provider)}
|
|
185
|
+
className="rounded-lg bg-slate-800 p-2 text-slate-400 transition-colors hover:bg-slate-700 hover:text-white"
|
|
186
|
+
title="Copy proxy URL"
|
|
187
|
+
>
|
|
188
|
+
{copiedUrl === key.provider ? (
|
|
189
|
+
<CheckCircle className="h-4 w-4 text-green-500" />
|
|
190
|
+
) : (
|
|
191
|
+
<Copy className="h-4 w-4" />
|
|
192
|
+
)}
|
|
193
|
+
</motion.button>
|
|
194
|
+
<motion.button
|
|
195
|
+
whileHover={{ scale: 1.1 }}
|
|
196
|
+
whileTap={{ scale: 0.9 }}
|
|
197
|
+
onClick={() => handleDeleteKey(key.id)}
|
|
198
|
+
className="rounded-lg bg-slate-800 p-2 text-slate-400 transition-colors hover:bg-red-900/20 hover:text-red-500"
|
|
199
|
+
title="Delete key"
|
|
200
|
+
>
|
|
201
|
+
<Trash2 className="h-4 w-4" />
|
|
202
|
+
</motion.button>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{/* Proxy URL Info */}
|
|
207
|
+
<div className="mt-4 rounded-lg bg-slate-800/50 p-3">
|
|
208
|
+
<p className="text-xs text-slate-500">Proxy URL:</p>
|
|
209
|
+
<p className="mt-1 font-mono text-xs text-slate-400">
|
|
210
|
+
{window.location.origin}/api/proxy/{key.provider.toLowerCase()}/...
|
|
211
|
+
</p>
|
|
212
|
+
</div>
|
|
213
|
+
</motion.div>
|
|
214
|
+
))}
|
|
215
|
+
</div>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
{/* Add API Key Modal */}
|
|
220
|
+
{showAddModal && (
|
|
221
|
+
<AddApiKeyModal
|
|
222
|
+
onClose={() => setShowAddModal(false)}
|
|
223
|
+
onSuccess={() => {
|
|
224
|
+
setShowAddModal(false)
|
|
225
|
+
fetchKeys()
|
|
226
|
+
}}
|
|
227
|
+
/>
|
|
228
|
+
)}
|
|
229
|
+
</>
|
|
230
|
+
)
|
|
231
|
+
}
|