fix: build speed, observer AI details, round tracker empty state
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled

- Disable typedRoutes and skip TS in build (run tsc separately) — build
  drops from ~9min to ~36s
- Expand optimizePackageImports for sonner, date-fns, recharts, motion, zod
- Docker: mount .next/cache as build cache for faster rebuilds
- Observer filtering panel: fix AI reasoning extraction (nested under rule ID)
  and show confidence, quality score, spam risk, override reason
- Round User Tracker: show empty state message instead of disappearing when
  selected round has no passed projects yet

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 17:30:11 +01:00
parent 0d94ee1fe8
commit 22731e7978
4 changed files with 91 additions and 24 deletions

View File

@@ -23,9 +23,9 @@ COPY . .
# Generate Prisma client # Generate Prisma client
RUN npx prisma generate RUN npx prisma generate
# Build Next.js # Build Next.js — mount .next/cache as a Docker build cache for faster rebuilds
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build RUN --mount=type=cache,target=/app/.next/cache npm run build
# Production image, copy all the files and run next # Production image, copy all the files and run next
FROM base AS runner FROM base AS runner

View File

@@ -2,10 +2,21 @@ import type { NextConfig } from 'next'
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: 'standalone', output: 'standalone',
typedRoutes: true,
serverExternalPackages: ['@prisma/client', 'minio'], serverExternalPackages: ['@prisma/client', 'minio'],
typescript: {
// We run tsc --noEmit separately before each push
ignoreBuildErrors: true,
},
experimental: { experimental: {
optimizePackageImports: ['lucide-react'], optimizePackageImports: [
'lucide-react',
'sonner',
'date-fns',
'recharts',
'motion/react',
'zod',
'@radix-ui/react-icons',
],
}, },
images: { images: {
remotePatterns: [ remotePatterns: [

View File

@@ -57,23 +57,21 @@ export function RoundUserTracker({ editionId }: RoundUserTrackerProps) {
const { rounds, byCategory } = data const { rounds, byCategory } = data
const effectiveRoundId = data.selectedRoundId const effectiveRoundId = data.selectedRoundId
// Don't render if no rounds or no data // Don't render if no rounds at all
if (!effectiveRoundId || rounds.length === 0) return null if (rounds.length === 0) return null
const totalProjects = byCategory.reduce((sum, c) => sum + c.total, 0) const totalProjects = byCategory.reduce((sum, c) => sum + c.total, 0)
const totalActivated = byCategory.reduce((sum, c) => sum + c.accountsSet, 0) const totalActivated = byCategory.reduce((sum, c) => sum + c.accountsSet, 0)
const totalPending = byCategory.reduce((sum, c) => sum + c.accountsNotSet, 0) const totalPending = byCategory.reduce((sum, c) => sum + c.accountsNotSet, 0)
if (totalProjects === 0) return null const selectedRound = effectiveRoundId ? rounds.find(r => r.id === effectiveRoundId) : undefined
const selectedRound = rounds.find(r => r.id === effectiveRoundId)
const handleSendReminder = async (target: string, opts: { category?: 'STARTUP' | 'BUSINESS_CONCEPT' }) => { const handleSendReminder = async (target: string, opts: { category?: 'STARTUP' | 'BUSINESS_CONCEPT' }) => {
setSendingTarget(target) setSendingTarget(target)
try { try {
await sendReminders.mutateAsync({ await sendReminders.mutateAsync({
editionId, editionId,
roundId: effectiveRoundId, roundId: effectiveRoundId!,
category: opts.category, category: opts.category,
}) })
} finally { } finally {
@@ -89,13 +87,15 @@ export function RoundUserTracker({ editionId }: RoundUserTrackerProps) {
<Users className="h-4 w-4 text-brand-blue" /> <Users className="h-4 w-4 text-brand-blue" />
Round User Tracker Round User Tracker
</CardTitle> </CardTitle>
{totalProjects > 0 && (
<Badge variant="outline" className="text-xs shrink-0"> <Badge variant="outline" className="text-xs shrink-0">
{totalActivated}/{totalProjects} activated {totalActivated}/{totalProjects} activated
</Badge> </Badge>
)}
</div> </div>
{/* Round selector */} {/* Round selector */}
<Select <Select
value={effectiveRoundId} value={effectiveRoundId ?? ''}
onValueChange={(val) => setSelectedRoundId(val)} onValueChange={(val) => setSelectedRoundId(val)}
> >
<SelectTrigger className="h-8 text-xs mt-2"> <SelectTrigger className="h-8 text-xs mt-2">
@@ -114,6 +114,15 @@ export function RoundUserTracker({ editionId }: RoundUserTrackerProps) {
</Select> </Select>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{totalProjects === 0 ? (
<div className="text-center py-4">
<Users className="h-8 w-8 text-muted-foreground/30 mx-auto mb-2" />
<p className="text-sm text-muted-foreground">
No projects have passed {selectedRound?.name ?? 'this round'} yet
</p>
</div>
) : (
<>
{/* Subtitle showing round context */} {/* Subtitle showing round context */}
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Projects that passed <span className="font-medium">{selectedRound?.name ?? 'this round'}</span> account activation status Projects that passed <span className="font-medium">{selectedRound?.name ?? 'this round'}</span> account activation status
@@ -192,6 +201,8 @@ export function RoundUserTracker({ editionId }: RoundUserTrackerProps) {
)} )}
</div> </div>
</div> </div>
</>
)}
</CardContent> </CardContent>
</Card> </Card>
) )

View File

@@ -19,6 +19,29 @@ import {
import { Filter, ChevronDown, ChevronUp, ChevronLeft, ChevronRight } from 'lucide-react' import { Filter, ChevronDown, ChevronUp, ChevronLeft, ChevronRight } from 'lucide-react'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
type AIScreeningData = {
meetsCriteria?: boolean
confidence?: number
reasoning?: string
qualityScore?: number
spamRisk?: boolean
}
function parseAIData(json: unknown): AIScreeningData | null {
if (!json || typeof json !== 'object') return null
const obj = json as Record<string, unknown>
// aiScreeningJson is nested under rule ID: { [ruleId]: { outcome, confidence, ... } }
if (!('outcome' in obj) && !('reasoning' in obj)) {
const keys = Object.keys(obj)
if (keys.length > 0) {
const inner = obj[keys[0]]
if (inner && typeof inner === 'object') return inner as AIScreeningData
}
return null
}
return obj as unknown as AIScreeningData
}
export function FilteringPanel({ roundId }: { roundId: string }) { export function FilteringPanel({ roundId }: { roundId: string }) {
const [outcomeFilter, setOutcomeFilter] = useState<string>('ALL') const [outcomeFilter, setOutcomeFilter] = useState<string>('ALL')
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
@@ -199,17 +222,39 @@ export function FilteringPanel({ roundId }: { roundId: string }) {
</div> </div>
</div> </div>
</button> </button>
{expandedId === r.id && ( {expandedId === r.id && (() => {
<div className="px-4 pb-3 pt-0"> const ai = parseAIData(r.aiScreeningJson)
<div className="rounded bg-muted/50 p-3 text-xs leading-relaxed text-muted-foreground"> return (
{(() => { <div className="px-4 pb-3 pt-0 space-y-2">
const screening = r.aiScreeningJson as Record<string, unknown> | null {ai?.confidence != null && (
const reasoning = (screening?.reasoning ?? screening?.explanation ?? r.overrideReason ?? 'No details available') as string <div className="flex items-center gap-3 text-xs">
return reasoning {ai.confidence != null && (
})()} <span className="text-muted-foreground">
</div> Confidence: <strong>{Math.round(ai.confidence * 100)}%</strong>
</span>
)}
{ai.qualityScore != null && (
<span className="text-muted-foreground">
Quality: <strong>{ai.qualityScore}/10</strong>
</span>
)}
{ai.spamRisk && (
<Badge variant="destructive" className="text-[10px] px-1.5 py-0">Spam Risk</Badge>
)}
</div> </div>
)} )}
<div className="rounded bg-muted/50 border p-3 text-xs leading-relaxed text-muted-foreground whitespace-pre-wrap">
{ai?.reasoning || 'No AI reasoning available'}
</div>
{r.overrideReason && (
<div className="rounded bg-amber-50 border border-amber-200 p-3 text-xs">
<span className="font-medium text-amber-800">Override: </span>
{r.overrideReason}
</div>
)}
</div>
)
})()}
</div> </div>
))} ))}
</div> </div>