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,338 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
5
|
+
import { Badge } from '@/components/ui/badge';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { RefreshCw, CheckCircle, XCircle, Clock, AlertCircle } from 'lucide-react';
|
|
8
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
9
|
+
|
|
10
|
+
interface PricingJob {
|
|
11
|
+
id: string;
|
|
12
|
+
status: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
completedAt?: string;
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
|
+
error?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ActivePricing {
|
|
20
|
+
id: string;
|
|
21
|
+
provider: string;
|
|
22
|
+
isActive: boolean;
|
|
23
|
+
modelCount: number;
|
|
24
|
+
createdAt: string;
|
|
25
|
+
note?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface SamplePrice {
|
|
29
|
+
provider: string;
|
|
30
|
+
model: string;
|
|
31
|
+
inputPer1k: number;
|
|
32
|
+
outputPer1k: number;
|
|
33
|
+
createdAt: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface PricingData {
|
|
37
|
+
recentJobs: PricingJob[];
|
|
38
|
+
activePricing: ActivePricing[];
|
|
39
|
+
counts: {
|
|
40
|
+
totalJobs: number;
|
|
41
|
+
completedJobs: number;
|
|
42
|
+
failedJobs: number;
|
|
43
|
+
totalVersions: number;
|
|
44
|
+
activeVersions: number;
|
|
45
|
+
totalModels: number;
|
|
46
|
+
totalAliases: number;
|
|
47
|
+
};
|
|
48
|
+
recentUpdates: {
|
|
49
|
+
last24Hours: number;
|
|
50
|
+
};
|
|
51
|
+
samplePricing: SamplePrice[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default function PricingStatusPage() {
|
|
55
|
+
const [data, setData] = useState<PricingData | null>(null);
|
|
56
|
+
const [loading, setLoading] = useState(true);
|
|
57
|
+
const [error, setError] = useState<string | null>(null);
|
|
58
|
+
|
|
59
|
+
const fetchData = async () => {
|
|
60
|
+
setLoading(true);
|
|
61
|
+
setError(null);
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch('/api/admin/check-pricing');
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
66
|
+
}
|
|
67
|
+
const result = await response.json();
|
|
68
|
+
if (result.success) {
|
|
69
|
+
setData(result.data);
|
|
70
|
+
} else {
|
|
71
|
+
throw new Error(result.error || 'Failed to fetch data');
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
setError(err instanceof Error ? err.message : 'Failed to fetch pricing data');
|
|
75
|
+
} finally {
|
|
76
|
+
setLoading(false);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
fetchData();
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
const getStatusIcon = (status: string) => {
|
|
85
|
+
switch (status) {
|
|
86
|
+
case 'COMPLETED':
|
|
87
|
+
return <CheckCircle className="h-4 w-4 text-green-500" />;
|
|
88
|
+
case 'FAILED':
|
|
89
|
+
return <XCircle className="h-4 w-4 text-red-500" />;
|
|
90
|
+
case 'RUNNING':
|
|
91
|
+
return <Clock className="h-4 w-4 text-blue-500 animate-spin" />;
|
|
92
|
+
case 'PARTIAL':
|
|
93
|
+
return <AlertCircle className="h-4 w-4 text-yellow-500" />;
|
|
94
|
+
default:
|
|
95
|
+
return <Clock className="h-4 w-4 text-gray-500" />;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const getStatusBadge = (status: string) => {
|
|
100
|
+
const variants: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
|
|
101
|
+
COMPLETED: 'default',
|
|
102
|
+
FAILED: 'destructive',
|
|
103
|
+
RUNNING: 'secondary',
|
|
104
|
+
PARTIAL: 'outline',
|
|
105
|
+
PENDING: 'outline'
|
|
106
|
+
};
|
|
107
|
+
return (
|
|
108
|
+
<Badge variant={variants[status] || 'outline'} className="flex items-center gap-1">
|
|
109
|
+
{getStatusIcon(status)}
|
|
110
|
+
{status}
|
|
111
|
+
</Badge>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (loading) {
|
|
116
|
+
return (
|
|
117
|
+
<div className="flex items-center justify-center min-h-screen">
|
|
118
|
+
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900"></div>
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (error) {
|
|
124
|
+
return (
|
|
125
|
+
<div className="container mx-auto p-6">
|
|
126
|
+
<Card className="border-red-200 bg-red-50">
|
|
127
|
+
<CardHeader>
|
|
128
|
+
<CardTitle className="text-red-800">Error Loading Pricing Data</CardTitle>
|
|
129
|
+
</CardHeader>
|
|
130
|
+
<CardContent>
|
|
131
|
+
<p className="text-red-600">{error}</p>
|
|
132
|
+
<Button onClick={fetchData} className="mt-4">
|
|
133
|
+
<RefreshCw className="mr-2 h-4 w-4" />
|
|
134
|
+
Retry
|
|
135
|
+
</Button>
|
|
136
|
+
</CardContent>
|
|
137
|
+
</Card>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!data) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<div className="container mx-auto p-6 space-y-6">
|
|
148
|
+
<div className="flex justify-between items-center">
|
|
149
|
+
<h1 className="text-3xl font-bold">Pricing Data Status</h1>
|
|
150
|
+
<Button onClick={fetchData} variant="outline">
|
|
151
|
+
<RefreshCw className="mr-2 h-4 w-4" />
|
|
152
|
+
Refresh
|
|
153
|
+
</Button>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Summary Cards */}
|
|
157
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
158
|
+
<Card>
|
|
159
|
+
<CardHeader className="pb-2">
|
|
160
|
+
<CardTitle className="text-sm font-medium">Total Jobs</CardTitle>
|
|
161
|
+
</CardHeader>
|
|
162
|
+
<CardContent>
|
|
163
|
+
<div className="text-2xl font-bold">{data.counts.totalJobs}</div>
|
|
164
|
+
<div className="text-xs text-muted-foreground">
|
|
165
|
+
{data.counts.completedJobs} completed, {data.counts.failedJobs} failed
|
|
166
|
+
</div>
|
|
167
|
+
</CardContent>
|
|
168
|
+
</Card>
|
|
169
|
+
|
|
170
|
+
<Card>
|
|
171
|
+
<CardHeader className="pb-2">
|
|
172
|
+
<CardTitle className="text-sm font-medium">Pricing Versions</CardTitle>
|
|
173
|
+
</CardHeader>
|
|
174
|
+
<CardContent>
|
|
175
|
+
<div className="text-2xl font-bold">{data.counts.totalVersions}</div>
|
|
176
|
+
<div className="text-xs text-muted-foreground">
|
|
177
|
+
{data.counts.activeVersions} active
|
|
178
|
+
</div>
|
|
179
|
+
</CardContent>
|
|
180
|
+
</Card>
|
|
181
|
+
|
|
182
|
+
<Card>
|
|
183
|
+
<CardHeader className="pb-2">
|
|
184
|
+
<CardTitle className="text-sm font-medium">Model Prices</CardTitle>
|
|
185
|
+
</CardHeader>
|
|
186
|
+
<CardContent>
|
|
187
|
+
<div className="text-2xl font-bold">{data.counts.totalModels}</div>
|
|
188
|
+
<div className="text-xs text-muted-foreground">
|
|
189
|
+
{data.counts.totalAliases} aliases
|
|
190
|
+
</div>
|
|
191
|
+
</CardContent>
|
|
192
|
+
</Card>
|
|
193
|
+
|
|
194
|
+
<Card>
|
|
195
|
+
<CardHeader className="pb-2">
|
|
196
|
+
<CardTitle className="text-sm font-medium">Recent Updates</CardTitle>
|
|
197
|
+
</CardHeader>
|
|
198
|
+
<CardContent>
|
|
199
|
+
<div className="text-2xl font-bold">{data.recentUpdates.last24Hours}</div>
|
|
200
|
+
<div className="text-xs text-muted-foreground">
|
|
201
|
+
in last 24 hours
|
|
202
|
+
</div>
|
|
203
|
+
</CardContent>
|
|
204
|
+
</Card>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
{/* Recent Jobs */}
|
|
208
|
+
<Card>
|
|
209
|
+
<CardHeader>
|
|
210
|
+
<CardTitle>Recent Discovery Jobs</CardTitle>
|
|
211
|
+
<CardDescription>Last 5 pricing discovery jobs</CardDescription>
|
|
212
|
+
</CardHeader>
|
|
213
|
+
<CardContent>
|
|
214
|
+
{data.recentJobs.length === 0 ? (
|
|
215
|
+
<p className="text-muted-foreground">No jobs found</p>
|
|
216
|
+
) : (
|
|
217
|
+
<div className="space-y-4">
|
|
218
|
+
{data.recentJobs.map((job) => (
|
|
219
|
+
<div key={job.id} className="flex items-center justify-between border-b pb-2">
|
|
220
|
+
<div className="flex items-center gap-4">
|
|
221
|
+
{getStatusBadge(job.status)}
|
|
222
|
+
<div>
|
|
223
|
+
<p className="text-sm font-medium">Job {job.id.slice(0, 8)}...</p>
|
|
224
|
+
<p className="text-xs text-muted-foreground">
|
|
225
|
+
Created {formatDistanceToNow(new Date(job.createdAt))} ago
|
|
226
|
+
</p>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
<div className="text-right">
|
|
230
|
+
{job.completedAt && (
|
|
231
|
+
<p className="text-xs text-muted-foreground">
|
|
232
|
+
Completed {formatDistanceToNow(new Date(job.completedAt))} ago
|
|
233
|
+
</p>
|
|
234
|
+
)}
|
|
235
|
+
{job.error && (
|
|
236
|
+
<p className="text-xs text-red-500">Error: {job.error}</p>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
))}
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
</CardContent>
|
|
244
|
+
</Card>
|
|
245
|
+
|
|
246
|
+
{/* Active Pricing by Provider */}
|
|
247
|
+
<Card>
|
|
248
|
+
<CardHeader>
|
|
249
|
+
<CardTitle>Active Pricing Versions</CardTitle>
|
|
250
|
+
<CardDescription>Currently active pricing data by provider</CardDescription>
|
|
251
|
+
</CardHeader>
|
|
252
|
+
<CardContent>
|
|
253
|
+
{data.activePricing.length === 0 ? (
|
|
254
|
+
<p className="text-muted-foreground">No active pricing versions</p>
|
|
255
|
+
) : (
|
|
256
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
257
|
+
{data.activePricing.map((pricing) => (
|
|
258
|
+
<div key={pricing.id} className="border rounded-lg p-4">
|
|
259
|
+
<div className="flex items-center justify-between mb-2">
|
|
260
|
+
<Badge variant="outline">{pricing.provider}</Badge>
|
|
261
|
+
{pricing.isActive && (
|
|
262
|
+
<Badge variant="default" className="bg-green-500">Active</Badge>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
<p className="text-sm font-medium">{pricing.modelCount} models</p>
|
|
266
|
+
<p className="text-xs text-muted-foreground">
|
|
267
|
+
Updated {formatDistanceToNow(new Date(pricing.createdAt))} ago
|
|
268
|
+
</p>
|
|
269
|
+
{pricing.note && (
|
|
270
|
+
<p className="text-xs mt-2 italic">{pricing.note}</p>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
</CardContent>
|
|
277
|
+
</Card>
|
|
278
|
+
|
|
279
|
+
{/* Current Model Pricing */}
|
|
280
|
+
<Card>
|
|
281
|
+
<CardHeader>
|
|
282
|
+
<CardTitle>Current Model Pricing</CardTitle>
|
|
283
|
+
<CardDescription>Active model prices discovered from provider APIs</CardDescription>
|
|
284
|
+
</CardHeader>
|
|
285
|
+
<CardContent>
|
|
286
|
+
{data.samplePricing.length === 0 ? (
|
|
287
|
+
<p className="text-muted-foreground">No pricing data found</p>
|
|
288
|
+
) : (
|
|
289
|
+
<div className="overflow-x-auto rounded-lg border">
|
|
290
|
+
<table className="min-w-full divide-y divide-border">
|
|
291
|
+
<thead className="bg-muted/50">
|
|
292
|
+
<tr>
|
|
293
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
294
|
+
Provider
|
|
295
|
+
</th>
|
|
296
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
297
|
+
Model
|
|
298
|
+
</th>
|
|
299
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
300
|
+
Input (per 1K)
|
|
301
|
+
</th>
|
|
302
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
303
|
+
Output (per 1K)
|
|
304
|
+
</th>
|
|
305
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
306
|
+
Added
|
|
307
|
+
</th>
|
|
308
|
+
</tr>
|
|
309
|
+
</thead>
|
|
310
|
+
<tbody className="divide-y divide-border">
|
|
311
|
+
{data.samplePricing.map((price, idx) => (
|
|
312
|
+
<tr key={idx} className="hover:bg-muted/50 transition-colors">
|
|
313
|
+
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
314
|
+
<Badge variant="outline">{price.provider}</Badge>
|
|
315
|
+
</td>
|
|
316
|
+
<td className="px-6 py-4 whitespace-nowrap text-sm font-mono">
|
|
317
|
+
{price.model}
|
|
318
|
+
</td>
|
|
319
|
+
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
|
|
320
|
+
${price.inputPer1k.toFixed(4)}
|
|
321
|
+
</td>
|
|
322
|
+
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
|
|
323
|
+
${price.outputPer1k.toFixed(4)}
|
|
324
|
+
</td>
|
|
325
|
+
<td className="px-6 py-4 whitespace-nowrap text-sm text-muted-foreground">
|
|
326
|
+
{formatDistanceToNow(new Date(price.createdAt))} ago
|
|
327
|
+
</td>
|
|
328
|
+
</tr>
|
|
329
|
+
))}
|
|
330
|
+
</tbody>
|
|
331
|
+
</table>
|
|
332
|
+
</div>
|
|
333
|
+
)}
|
|
334
|
+
</CardContent>
|
|
335
|
+
</Card>
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/prisma';
|
|
3
|
+
import { requireAdminPermission } from '@/lib/admin-auth';
|
|
4
|
+
|
|
5
|
+
export async function GET(_req: NextRequest) {
|
|
6
|
+
try {
|
|
7
|
+
// Check admin permission - use view_sensitive_data as pricing is sensitive
|
|
8
|
+
const admin = await requireAdminPermission('view_sensitive_data');
|
|
9
|
+
|
|
10
|
+
// Get recent jobs
|
|
11
|
+
const recentJobs = await prisma.pricingDiscoveryJob.findMany({
|
|
12
|
+
orderBy: { createdAt: 'desc' },
|
|
13
|
+
take: 5,
|
|
14
|
+
select: {
|
|
15
|
+
id: true,
|
|
16
|
+
status: true,
|
|
17
|
+
createdAt: true,
|
|
18
|
+
completedAt: true,
|
|
19
|
+
metadata: true,
|
|
20
|
+
error: true
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Get active pricing versions
|
|
25
|
+
const activePricing = await prisma.pricingVersion.findMany({
|
|
26
|
+
where: { isActive: true },
|
|
27
|
+
include: {
|
|
28
|
+
_count: {
|
|
29
|
+
select: { models: true }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Get counts
|
|
35
|
+
const counts = {
|
|
36
|
+
totalJobs: await prisma.pricingDiscoveryJob.count(),
|
|
37
|
+
completedJobs: await prisma.pricingDiscoveryJob.count({
|
|
38
|
+
where: { status: 'COMPLETED' }
|
|
39
|
+
}),
|
|
40
|
+
failedJobs: await prisma.pricingDiscoveryJob.count({
|
|
41
|
+
where: { status: 'FAILED' }
|
|
42
|
+
}),
|
|
43
|
+
totalVersions: await prisma.pricingVersion.count(),
|
|
44
|
+
activeVersions: await prisma.pricingVersion.count({
|
|
45
|
+
where: { isActive: true }
|
|
46
|
+
}),
|
|
47
|
+
totalModels: await prisma.pricingModelPrice.count(),
|
|
48
|
+
totalAliases: await prisma.providerModelAlias.count()
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Check for recent updates (last 24 hours)
|
|
52
|
+
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
53
|
+
const recentVersions = await prisma.pricingVersion.count({
|
|
54
|
+
where: {
|
|
55
|
+
createdAt: { gte: oneDayAgo }
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Get all pricing data
|
|
60
|
+
const samplePricing = await prisma.pricingModelPrice.findMany({
|
|
61
|
+
take: 50, // Increased from 10 to show all models
|
|
62
|
+
orderBy: [
|
|
63
|
+
{ provider: 'asc' },
|
|
64
|
+
{ canonicalModel: 'asc' }
|
|
65
|
+
],
|
|
66
|
+
select: {
|
|
67
|
+
provider: true,
|
|
68
|
+
canonicalModel: true,
|
|
69
|
+
unitInputPer1k: true,
|
|
70
|
+
unitOutputPer1k: true,
|
|
71
|
+
createdAt: true
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return NextResponse.json({
|
|
76
|
+
success: true,
|
|
77
|
+
admin: {
|
|
78
|
+
userId: admin.clerkId,
|
|
79
|
+
role: admin.role
|
|
80
|
+
},
|
|
81
|
+
data: {
|
|
82
|
+
recentJobs,
|
|
83
|
+
activePricing: activePricing.map((v: any) => ({
|
|
84
|
+
id: v.id,
|
|
85
|
+
provider: v.provider,
|
|
86
|
+
isActive: v.isActive,
|
|
87
|
+
modelCount: v._count.models,
|
|
88
|
+
createdAt: v.createdAt,
|
|
89
|
+
note: v.note
|
|
90
|
+
})),
|
|
91
|
+
counts,
|
|
92
|
+
recentUpdates: {
|
|
93
|
+
last24Hours: recentVersions
|
|
94
|
+
},
|
|
95
|
+
samplePricing: samplePricing.map((p: any) => ({
|
|
96
|
+
provider: p.provider,
|
|
97
|
+
model: p.canonicalModel,
|
|
98
|
+
inputPer1k: Number(p.unitInputPer1k),
|
|
99
|
+
outputPer1k: Number(p.unitOutputPer1k),
|
|
100
|
+
createdAt: p.createdAt
|
|
101
|
+
}))
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Log error for debugging
|
|
107
|
+
// console.error('Check pricing error:', error);
|
|
108
|
+
|
|
109
|
+
if (error instanceof Error && error.message.includes('Admin access required')) {
|
|
110
|
+
return NextResponse.json({
|
|
111
|
+
error: 'Admin access required'
|
|
112
|
+
}, { status: 403 });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (error instanceof Error && error.message.includes('Permission required')) {
|
|
116
|
+
return NextResponse.json({
|
|
117
|
+
error: 'Insufficient permissions',
|
|
118
|
+
message: 'You need admin privileges to view pricing data'
|
|
119
|
+
}, { status: 403 });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return NextResponse.json({
|
|
123
|
+
error: 'Failed to check pricing data',
|
|
124
|
+
details: error instanceof Error ? error.message : 'Unknown error'
|
|
125
|
+
}, { status: 500 });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { auth, clerkClient } from '@clerk/nextjs/server';
|
|
3
|
+
|
|
4
|
+
// Force dynamic rendering for this route
|
|
5
|
+
export const dynamic = 'force-dynamic';
|
|
6
|
+
|
|
7
|
+
export async function GET() {
|
|
8
|
+
try {
|
|
9
|
+
const { userId } = await auth();
|
|
10
|
+
|
|
11
|
+
if (!userId) {
|
|
12
|
+
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const adminOrgId = process.env.CLERK_ADMIN_ORGANIZATION_ID;
|
|
16
|
+
|
|
17
|
+
// Get all user's organization memberships
|
|
18
|
+
const client = await clerkClient();
|
|
19
|
+
const memberships = await client.users.getOrganizationMembershipList({
|
|
20
|
+
userId
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const debug = {
|
|
24
|
+
userId,
|
|
25
|
+
adminOrgIdFromEnv: adminOrgId,
|
|
26
|
+
totalMemberships: memberships.data?.length || 0,
|
|
27
|
+
memberships: memberships.data?.map((membership: any) => ({
|
|
28
|
+
organizationId: membership.organization.id,
|
|
29
|
+
organizationName: membership.organization.name,
|
|
30
|
+
role: membership.role,
|
|
31
|
+
isAdminOrg: membership.organization.id === adminOrgId
|
|
32
|
+
})) || []
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return NextResponse.json(debug);
|
|
36
|
+
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('Debug error:', error);
|
|
39
|
+
return NextResponse.json({
|
|
40
|
+
error: 'Debug failed',
|
|
41
|
+
details: error instanceof Error ? error.message : 'Unknown error'
|
|
42
|
+
}, { status: 500 });
|
|
43
|
+
}
|
|
44
|
+
}
|