Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions sw-dash/src/app/admin/payouts/logs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ export default async function Logs() {
<th className="text-left p-4 text-amber-400 font-mono text-sm">project</th>
<th className="text-left p-4 text-amber-400 font-mono text-sm">reviewer</th>
<th className="text-left p-4 text-amber-400 font-mono text-sm">type</th>
<th className="text-right p-4 text-amber-400 font-mono text-sm">rate</th>
<th className="text-right p-4 text-amber-400 font-mono text-sm">multi</th>
<th className="text-right p-4 text-amber-400 font-mono text-sm">base rate</th>
<th className="text-right p-4 text-amber-400 font-mono text-sm">eff. multi</th>
<th className="text-right p-4 text-amber-400 font-mono text-sm">when</th>
<th className="text-right p-4 text-amber-400 font-mono text-sm">cookies</th>
</tr>
Expand Down
108 changes: 77 additions & 31 deletions sw-dash/src/app/admin/ship_certifications/mystats/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,42 +225,88 @@ export default async function Stats() {
</div>
</div>

<div className="bg-gradient-to-br from-zinc-900/90 to-black/90 border-4 border-purple-900/40 rounded-3xl p-4 md:p-6 shadow-xl">
<h2 className="text-purple-400 font-mono text-base md:text-lg mb-4">Bounty Rates</h2>
<div className="space-y-2">
{Object.entries(RATES)
.sort((a, b) => b[1] - a[1])
.map(([type, bounty]) => (
<div
key={type}
className="flex justify-between text-sm font-mono py-1 border-b border-purple-900/20 last:border-0"
>
<span className="text-gray-300">{type}</span>
<span className="text-purple-300 font-bold">{bounty} 🍪</span>
</div>
))}
<div className="bg-gradient-to-br from-zinc-900/90 to-black/90 border-4 border-purple-900/40 rounded-3xl p-4 md:p-6 shadow-xl space-y-6">
<div className="flex items-center justify-between border-b border-purple-900/30 pb-4">
<h2 className="text-purple-400 font-mono text-xl md:text-2xl font-bold">
Dynamic Payouts
</h2>
</div>
<div className="bg-purple-900/20 rounded-xl p-3 mt-4">
<div className="text-purple-300 font-mono text-xs font-bold mb-2">Multipliers</div>
<div className="space-y-1 text-xs font-mono">
<div className="flex justify-between">
<span className="text-gray-400">1st on lb:</span>
<span className="text-purple-300">1.75x bounty</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">2nd on lb:</span>
<span className="text-purple-300">1.5x bounty</span>
</div>
<div className="flex justify-between">
<span className="text-gray-400">3rd on lb:</span>
<span className="text-purple-300">1.25x bounty</span>

<div className="space-y-4">
<div className="space-y-2">
<h3 className="text-gray-500 font-mono text-xs font-bold uppercase tracking-wider">
Base Rates
</h3>
<div className="space-y-1">
<div className="flex justify-between items-center p-2 rounded hover:bg-white/5 transition-colors">
<span className="text-gray-300 font-mono text-sm">Desktop, Mobile, Other</span>
<span className="text-purple-400 font-mono font-bold text-lg">1.5 🍪</span>
</div>
<div className="flex justify-between items-center p-2 rounded hover:bg-white/5 transition-colors">
<span className="text-gray-300 font-mono text-sm">CLI, Games, Hardware</span>
<span className="text-purple-400 font-mono font-bold text-lg">1.0 🍪</span>
</div>
<div className="flex justify-between items-center p-2 rounded hover:bg-white/5 transition-colors">
<span className="text-gray-300 font-mono text-sm">Web Apps, Bots</span>
<span className="text-purple-400 font-mono font-bold text-lg">0.6 🍪</span>
</div>
</div>
<div className="flex justify-between">
<span className="text-gray-400">4th+ on lb:</span>
<span className="text-purple-300">1x bounty</span>
</div>

<div className="space-y-2 pt-2 border-t border-purple-900/20">
<h3 className="text-gray-500 font-mono text-xs font-bold uppercase tracking-wider">
Multipliers
</h3>
<div className="space-y-1">
<div className="flex justify-between items-center p-2 rounded hover:bg-white/5 transition-colors">
<div className="flex flex-col">
<span className="text-gray-300 font-mono text-sm font-bold">
First Review
</span>
<span className="text-gray-500 font-mono text-xs">
1.5x for your 1st review of the day
</span>
</div>
<span className="text-purple-400 font-mono font-bold">1.5x</span>
</div>
<div className="flex justify-between items-center p-2 rounded hover:bg-white/5 transition-colors">
<div className="flex flex-col">
<span className="text-gray-300 font-mono text-sm font-bold">
Old Projects
</span>
<span className="text-gray-500 font-mono text-xs">
1.5x if &gt;7 days, 1.2x if &gt;24h
</span>
</div>
<span className="text-purple-400 font-mono font-bold">1.5x</span>
</div>
<div className="flex justify-between items-center p-2 rounded hover:bg-white/5 transition-colors">
<div className="flex flex-col">
<span className="text-gray-300 font-mono text-sm font-bold">Daily Grind</span>
<span className="text-gray-500 font-mono text-xs">
1.2x after 7 reviews, 1.3x after 15
</span>
</div>
<span className="text-purple-400 font-mono font-bold">1.3x</span>
</div>
<div className="flex justify-between items-center p-2 rounded hover:bg-white/5 transition-colors">
<div className="flex flex-col">
<span className="text-gray-300 font-mono text-sm font-bold">Weekly Rank</span>
<span className="text-gray-500 font-mono text-xs">
1.75x (1st), 1.5x (2nd), 1.25x (3rd)
</span>
</div>
<span className="text-purple-400 font-mono font-bold">1.75x</span>
</div>
</div>
</div>
</div>

<div className="text-center pt-2 border-t border-purple-900/20">
<p className="text-gray-500 font-mono text-xs">
Base × Multipliers = Total.{' '}
</p>
</div>
</div>
</div>

Expand Down
13 changes: 11 additions & 2 deletions sw-dash/src/app/api/admin/ship_certifications/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,9 +235,18 @@ export const PATCH = withParams(PERMS.certs_edit)(async ({ user, req, params, ip
}

if (verdict.toLowerCase() === 'approved' || verdict.toLowerCase() === 'rejected') {
const payout = await calc(user.id, cert.projectType, cert.customBounty)
const payout = await calc({
userId: user.id,
projectType: cert.projectType,
verdict: verdict.toLowerCase() as 'approved' | 'rejected',
certCreatedAt: cert.createdAt,
customBounty: cert.customBounty,
Comment on lines +238 to +243

This comment was marked as outdated.

Comment on lines 236 to +243
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: A race condition exists where concurrent review certifications can both receive the same daily bonus multiplier, as the payout is calculated before the review completion is saved to the database.
Severity: MEDIUM

Suggested Fix

Wrap the critical section of the PATCH endpoint, from reading the review count with getDailyMulti() to updating the shipCert record, within a prisma.$transaction(). This will ensure the read and write operations are atomic, preventing other requests from reading stale data.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: sw-dash/src/app/api/admin/ship_certifications/[id]/route.ts#L236-L243

Potential issue: The payout multiplier is calculated by `calc()` before the database
update commits the review's `reviewCompletedAt` timestamp. If two certification requests
are processed concurrently, both calls to `getDailyMulti()` could query the database
before either review is marked as complete. This would cause both requests to
incorrectly receive the same tiered bonus, such as the "first review of the day" 1.5x
multiplier, leading to users earning duplicate rewards. The issue is present for
multiple bonus thresholds.

})
updateData.cookiesEarned = payout.cookies
updateData.payoutMulti = payout.multi

const effectiveMulti =
payout.base > 0 ? (payout.cookies - (payout.customBounty || 0)) / payout.base : 0
updateData.payoutMulti = Number(effectiveMulti.toFixed(2))
}
}

Expand Down
222 changes: 111 additions & 111 deletions sw-dash/src/components/admin/chart.tsx
Original file line number Diff line number Diff line change
@@ -1,128 +1,128 @@
'use client'

import {
LineChart,
Line,
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
CartesianGrid,
LineChart,
Line,
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
CartesianGrid,
} from 'recharts'

interface Props {
data: Array<{ date?: string; label?: string; value: number;[key: string]: any }>
type?: 'line' | 'bar'
dataKey?: string
xKey?: string
yLabel?: string
xLabel?: string
valueLabel?: string
color?: string
data: Array<{ date?: string; label?: string; value: number; [key: string]: any }>
type?: 'line' | 'bar'
dataKey?: string
xKey?: string
yLabel?: string
xLabel?: string
valueLabel?: string
color?: string
}

export function Chart({
data,
type = 'line',
dataKey = 'value',
xKey = 'date',
yLabel = 'Value',
xLabel = 'Date',
valueLabel = 'Value',
color = '#f59e0b',
data,
type = 'line',
dataKey = 'value',
xKey = 'date',
yLabel = 'Value',
xLabel = 'Date',
valueLabel = 'Value',
color = '#f59e0b',
}: Props) {
const formatDate = (dateStr: string) => {
const date = new Date(dateStr)
const month = date.toLocaleString('en-US', { month: 'short' })
const day = date.getDate()
return `${month} ${day}`
}
const formatDate = (dateStr: string) => {
const date = new Date(dateStr)
const month = date.toLocaleString('en-US', { month: 'short' })
const day = date.getDate()
return `${month} ${day}`
}

const CustomTooltip = ({ active, payload }: any) => {
if (!active || !payload || !payload.length) return null
const CustomTooltip = ({ active, payload }: any) => {
if (!active || !payload || !payload.length) return null

const date = payload[0].payload[xKey]
const value = payload[0].value
const date = payload[0].payload[xKey]
const value = payload[0].value

return (
<div className="bg-zinc-900 border-2 border-amber-600 rounded px-3 py-2 shadow-xl">
<p className="text-amber-400 text-xs font-mono mb-1 font-bold">
{date ? formatDate(date) : ''}
</p>
<p className="text-amber-100 text-base font-mono font-bold">
{valueLabel}: {value}
</p>
</div>
)
}
return (
<div className="bg-zinc-900 border-2 border-amber-600 rounded px-3 py-2 shadow-xl">
<p className="text-amber-400 text-xs font-mono mb-1 font-bold">
{date ? formatDate(date) : ''}
</p>
<p className="text-amber-100 text-base font-mono font-bold">
{valueLabel}: {value}
</p>
</div>
)
}

const formatXAxis = (value: string) => {
return formatDate(value)
}
const formatXAxis = (value: string) => {
return formatDate(value)
}

return (
<ResponsiveContainer width="100%" height={300}>
{type === 'line' ? (
<LineChart data={data} margin={{ top: 10, right: 20, bottom: 60, left: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#78716c20" />
<XAxis
dataKey={xKey}
stroke="#78716c"
style={{ fontSize: '10px' }}
tickFormatter={formatXAxis}
interval={0}
angle={-45}
textAnchor="end"
height={60}
/>
<YAxis
stroke="#78716c"
style={{ fontSize: '11px' }}
label={{
value: yLabel,
angle: -90,
position: 'insideLeft',
fill: '#78716c',
fontSize: 12,
}}
/>
<Tooltip content={<CustomTooltip />} cursor={false} />
return (
<ResponsiveContainer width="100%" height={300}>
{type === 'line' ? (
<LineChart data={data} margin={{ top: 10, right: 20, bottom: 60, left: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#78716c20" />
<XAxis
dataKey={xKey}
stroke="#78716c"
style={{ fontSize: '10px' }}
tickFormatter={formatXAxis}
interval={0}
angle={-45}
textAnchor="end"
height={60}
/>
<YAxis
stroke="#78716c"
style={{ fontSize: '11px' }}
label={{
value: yLabel,
angle: -90,
position: 'insideLeft',
fill: '#78716c',
fontSize: 12,
}}
/>
<Tooltip content={<CustomTooltip />} cursor={false} />
<Line type="monotone" dataKey={dataKey} stroke={color} strokeWidth={2} dot={false} />
</LineChart>
) : (
<BarChart data={data} margin={{ top: 10, right: 20, bottom: 60, left: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#78716c20" />
<XAxis
dataKey={xKey}
stroke="#78716c"
style={{ fontSize: '10px' }}
tickFormatter={formatXAxis}
interval={0}
angle={-45}
textAnchor="end"
height={60}
/>
<YAxis
stroke="#78716c"
style={{ fontSize: '11px' }}
label={{
value: yLabel,
angle: -90,
position: 'insideLeft',
fill: '#78716c',
fontSize: 12,
}}
/>
<Tooltip content={<CustomTooltip />} cursor={false} />
<Bar
dataKey={dataKey}
fill={color}
activeBar={{ fill: color, opacity: 0.8, strokeWidth: 2, stroke: color }}
/>
</BarChart>
)}
</ResponsiveContainer>
)
</LineChart>
) : (
<BarChart data={data} margin={{ top: 10, right: 20, bottom: 60, left: 20 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#78716c20" />
<XAxis
dataKey={xKey}
stroke="#78716c"
style={{ fontSize: '10px' }}
tickFormatter={formatXAxis}
interval={0}
angle={-45}
textAnchor="end"
height={60}
/>
<YAxis
stroke="#78716c"
style={{ fontSize: '11px' }}
label={{
value: yLabel,
angle: -90,
position: 'insideLeft',
fill: '#78716c',
fontSize: 12,
}}
/>
<Tooltip content={<CustomTooltip />} cursor={false} />
<Bar
dataKey={dataKey}
fill={color}
activeBar={{ fill: color, opacity: 0.8, strokeWidth: 2, stroke: color }}
/>
</BarChart>
)}
</ResponsiveContainer>
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is half your PR editing indentation?

Copy link
Contributor Author

@ObayM ObayM Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just pnpm pretty
@EricZil wanted this in the last 2 PRs :)

}
Loading