fix: score distribution chart bars + add binaryDecision backfill script
All checks were successful
Build and Push Docker Image / build (push) Successful in 11m15s

Chart: fixed bars not rendering by using explicit h-[160px] + min-h-0 on
the bar container so percentage-based heights resolve correctly.

Script: one-off backfill copies the custom "Move to the Next Stage?" boolean
criterion value into binaryDecision for evaluations where it's null.
Run: npx tsx scripts/backfill-binary-decision.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 11:14:02 +01:00
parent ba7f068b1e
commit f200eda692
2 changed files with 87 additions and 3 deletions

View File

@@ -0,0 +1,84 @@
/**
* One-off script: backfill binaryDecision from custom boolean criterion
* "Move to the Next Stage?" for evaluations where binaryDecision is null.
*
* Usage: npx tsx scripts/backfill-binary-decision.ts
*
* What it does:
* 1. Finds all rounds with a boolean criterion labeled "Move to the Next Stage?"
* 2. For evaluations in those rounds where binaryDecision IS NULL,
* copies the boolean value from criterionScoresJson into binaryDecision
*/
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
type CriterionConfig = {
id: string
label: string
type?: string
}
async function main() {
// Find all rounds that have evaluation config with criteria
const rounds = await prisma.round.findMany({
where: { roundType: 'EVALUATION' },
select: { id: true, name: true, configJson: true },
})
let totalUpdated = 0
for (const round of rounds) {
const config = round.configJson as Record<string, unknown> | null
if (!config) continue
const criteria = (config.criteria ?? config.evaluationCriteria ?? []) as CriterionConfig[]
// Find the boolean criterion for "Move to the Next Stage?"
const boolCriterion = criteria.find(
(c) =>
(c.type === 'boolean') &&
c.label?.toLowerCase().includes('move to the next stage'),
)
if (!boolCriterion) continue
console.log(`Round "${round.name}" (${round.id}): found criterion "${boolCriterion.label}" (${boolCriterion.id})`)
// Find evaluations in this round where binaryDecision is null
const evaluations = await prisma.evaluation.findMany({
where: {
assignment: { roundId: round.id },
binaryDecision: null,
status: 'SUBMITTED',
criterionScoresJson: { not: undefined },
},
select: { id: true, criterionScoresJson: true },
})
let updated = 0
for (const ev of evaluations) {
const scores = ev.criterionScoresJson as Record<string, unknown> | null
if (!scores) continue
const value = scores[boolCriterion.id]
if (typeof value !== 'boolean') continue
await prisma.evaluation.update({
where: { id: ev.id },
data: { binaryDecision: value },
})
updated++
}
console.log(` Updated ${updated}/${evaluations.length} evaluations`)
totalUpdated += updated
}
console.log(`\nDone. Total evaluations updated: ${totalUpdated}`)
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect())

View File

@@ -38,13 +38,13 @@ export function ScoreDistribution({ roundId }: ScoreDistributionProps) {
No evaluations submitted yet No evaluations submitted yet
</p> </p>
) : ( ) : (
<div className="flex gap-1 flex-1 min-h-[120px]"> <div className="flex gap-1 h-[160px]">
{dist.globalDistribution.map((bucket) => { {dist.globalDistribution.map((bucket) => {
const heightPct = (bucket.count / maxCount) * 100 const heightPct = (bucket.count / maxCount) * 100
return ( return (
<div key={bucket.score} className="flex-1 flex flex-col items-center gap-1 h-full"> <div key={bucket.score} className="flex-1 flex flex-col items-center gap-1">
<span className="text-[9px] text-muted-foreground">{bucket.count || ''}</span> <span className="text-[9px] text-muted-foreground">{bucket.count || ''}</span>
<div className="w-full flex-1 relative"> <div className="w-full flex-1 relative min-h-0">
<div className={cn( <div className={cn(
'absolute inset-x-0 bottom-0 rounded-t transition-all', 'absolute inset-x-0 bottom-0 rounded-t transition-all',
bucket.score <= 3 ? 'bg-red-400' : bucket.score <= 3 ? 'bg-red-400' :