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
93 changes: 3 additions & 90 deletions sw-dash/src/app/api/admin/ship_certifications/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { bust } from '@/lib/cache'
import { calc } from '@/lib/payouts'
import { withParams } from '@/lib/api'
import { create as createYsws } from '@/lib/ysws'
import { updateStreakOnReview } from '@/lib/streaks'

interface InternalNote {
id: string
Expand Down Expand Up @@ -304,96 +305,8 @@ export const PATCH = withParams(PERMS.certs_edit)(async ({ user, req, params, ip
}

if (verdict) {
const getESTComponents = (date: Date) => {
const parts = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
hourCycle: 'h23',
}).formatToParts(date)
const getVal = (type: string) => parseInt(parts.find((p) => p.type === type)?.value || '0')

return {
y: getVal('year'),
m: getVal('month') - 1,
d: getVal('day'),
h: getVal('hour'),
}
}

const now = new Date()
const { y, m, d } = getESTComponents(now)

const cand1 = new Date(Date.UTC(y, m, d, 5, 0, 0, 0)) // 5 AM UTC
const cand2 = new Date(Date.UTC(y, m, d, 4, 0, 0, 0)) // 4 AM UTC

const check1 = getESTComponents(cand1)

let startOfTodayUTC = cand1
if (check1.h !== 0) {
startOfTodayUTC = cand2
}

const todayCount = await prisma.shipCert.count({
where: {
reviewerId: certifierId !== undefined ? certifierId : user.id,
status: { in: ['approved', 'rejected'] },
reviewCompletedAt: {
gte: startOfTodayUTC,
},
},
})

if (todayCount >= 7) {
const userIdToUpdate = certifierId !== undefined ? certifierId : user.id
const currentUser = await prisma.user.findUnique({
where: { id: userIdToUpdate },
select: { streak: true, lastReviewDate: true },
})

if (currentUser) {
let newStreak = currentUser.streak
let shouldUpdate = false

const todayNormalized = new Date(Date.UTC(y, m, d))
const yesterdayNormalized = new Date(todayNormalized)
yesterdayNormalized.setDate(yesterdayNormalized.getDate() - 1)

let lastReviewNormalized = null
if (currentUser.lastReviewDate) {
const ld = currentUser.lastReviewDate

lastReviewNormalized = new Date(
Date.UTC(ld.getUTCFullYear(), ld.getUTCMonth(), ld.getUTCDate())
)
}

if (
!lastReviewNormalized ||
lastReviewNormalized.getTime() < yesterdayNormalized.getTime()
) {
newStreak = 1
shouldUpdate = true
} else if (lastReviewNormalized.getTime() === yesterdayNormalized.getTime()) {
newStreak += 1
shouldUpdate = true
} else if (lastReviewNormalized.getTime() === todayNormalized.getTime()) {
shouldUpdate = false
}

if (shouldUpdate) {
await prisma.user.update({
where: { id: userIdToUpdate },
data: {
streak: newStreak,
lastReviewDate: todayNormalized,
},
})
}
}
}
const userIdToUpdate = certifierId !== undefined ? certifierId : user.id
await updateStreakOnReview(userIdToUpdate)
}

if (
Expand Down
42 changes: 42 additions & 0 deletions sw-dash/src/app/api/admin/streak/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export const dynamic = 'force-dynamic'

import { NextResponse } from 'next/server'
import { prisma } from '@/lib/db'
import { can, PERMS } from '@/lib/perms'
import { getStreakInfo } from '@/lib/streaks'

export async function GET(req: Request) {
const { searchParams } = new URL(req.url)
const authHeader = req.headers.get('Authorization')

if (!authHeader || !authHeader.startsWith('Bearer ')) {
return NextResponse.json({ error: 'need a Bearer key' }, { status: 401 })
}

const key = authHeader.replace('Bearer ', '')
const user = await prisma.user.findUnique({
where: { swApiKey: key },
select: { id: true, role: true, username: true },
})

if (!user || !can(user.role, PERMS.certs_view)) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const slackId = searchParams.get('slack_id')
if (!slackId) {
return NextResponse.json({ error: 'slack_id is required' }, { status: 400 })
}

try {
const info = await getStreakInfo(slackId)

if (!info) {
return NextResponse.json({ error: 'user not found' }, { status: 404 })
}

return NextResponse.json(info)
} catch {
return NextResponse.json({ error: 'streak API exploded' }, { status: 500 })
}
}
133 changes: 133 additions & 0 deletions sw-dash/src/app/api/cron/recalc-streaks/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/db'

export async function GET(req: NextRequest) {
const key = req.headers.get('authorization')?.replace('Bearer ', '')

if (key !== process.env.CRON_SECRET && process.env.NODE_ENV === 'production') {
return NextResponse.json({ error: 'nope' }, { status: 401 })
}

try {
const getESTDate = (date: Date) => {
const parts = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
year: 'numeric',
month: 'numeric',
day: 'numeric',
}).formatToParts(date)
const get = (type: string) => parseInt(parts.find((p) => p.type === type)?.value || '0')
return { y: get('year'), m: get('month') - 1, d: get('day') }
}

const { y, m, d } = getESTDate(new Date())
const todayNorm = new Date(Date.UTC(y, m, d))

const reviewers = await prisma.user.findMany({
where: {
shipCerts: {
some: {
status: { in: ['approved', 'rejected'] },
reviewCompletedAt: { not: null },
},
},
},
select: { id: true, username: true, streak: true },
})

const results: { id: number; username: string; oldStreak: number; newStreak: number }[] = []

for (const reviewer of reviewers) {
const rows = await prisma.$queryRaw<{ reviewDate: Date }[]>`
SELECT DISTINCT DATE(reviewCompletedAt) as reviewDate
FROM ship_certs
WHERE reviewerId = ${reviewer.id}
AND status IN ('approved', 'rejected')
AND reviewCompletedAt IS NOT NULL
AND spotRemoved = false
ORDER BY reviewDate DESC
`

if (rows.length === 0) {
if (reviewer.streak !== 0) {
await prisma.user.update({
where: { id: reviewer.id },
data: { streak: 0, lastReviewDate: null },
})
results.push({
id: reviewer.id,
username: reviewer.username,
oldStreak: reviewer.streak,
newStreak: 0,
})
}
continue
}

let streak = 0
let checkDate = new Date(todayNorm)
let lastReviewDate: Date | null = null

const reviewDates = new Set(
rows.map((r) => {
const rd = new Date(r.reviewDate)
return new Date(
Date.UTC(rd.getUTCFullYear(), rd.getUTCMonth(), rd.getUTCDate())
).getTime()
})
)

for (let i = 0; i < 365; i++) {
if (reviewDates.has(checkDate.getTime())) {
streak++
if (!lastReviewDate) lastReviewDate = new Date(checkDate)
checkDate = new Date(checkDate)
checkDate.setDate(checkDate.getDate() - 1)
} else if (i === 0) {
checkDate.setDate(checkDate.getDate() - 1)
if (reviewDates.has(checkDate.getTime())) {
streak++
lastReviewDate = new Date(checkDate)
checkDate = new Date(checkDate)
checkDate.setDate(checkDate.getDate() - 1)
} else {
break
}
} else {
break
}
}

if (streak !== reviewer.streak) {
await prisma.user.update({
where: { id: reviewer.id },
data: {
streak,
lastReviewDate,
},
})
}

results.push({
id: reviewer.id,
username: reviewer.username,
oldStreak: reviewer.streak,
newStreak: streak,
})
}

const changed = results.filter((r) => r.oldStreak !== r.newStreak)

return NextResponse.json({
ok: true,
total: results.length,
changed: changed.length,
updates: changed,
})
} catch (e: any) {
return NextResponse.json(
{ error: 'recalc exploded :sob: ', message: e.message },
{ status: 500 }
)
}
}
100 changes: 100 additions & 0 deletions sw-dash/src/lib/streaks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { prisma } from '@/lib/db'

function getESTDate(date: Date) {
const parts = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
year: 'numeric',
month: 'numeric',
day: 'numeric',
}).formatToParts(date)

const get = (type: string) => parseInt(parts.find((p) => p.type === type)?.value || '0')
return { y: get('year'), m: get('month') - 1, d: get('day') }
}

export function computeStreak(streak: number, lastReviewDate: Date | null): number {
if (!lastReviewDate || streak === 0) return 0

const { y, m, d } = getESTDate(new Date())
const todayNorm = new Date(Date.UTC(y, m, d))
const yesterdayNorm = new Date(todayNorm)
yesterdayNorm.setDate(yesterdayNorm.getDate() - 1)

const lastNorm = new Date(
Date.UTC(
lastReviewDate.getUTCFullYear(),
lastReviewDate.getUTCMonth(),
lastReviewDate.getUTCDate()
)
)

if (lastNorm.getTime() >= yesterdayNorm.getTime()) {
return streak
}

return 0
}

export async function updateStreakOnReview(userId: number) {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { streak: true, lastReviewDate: true },
})

if (!user) return

const { y, m, d } = getESTDate(new Date())
const todayNorm = new Date(Date.UTC(y, m, d))
const yesterdayNorm = new Date(todayNorm)
yesterdayNorm.setDate(yesterdayNorm.getDate() - 1)

let lastNorm: Date | null = null
if (user.lastReviewDate) {
const ld = user.lastReviewDate
lastNorm = new Date(Date.UTC(ld.getUTCFullYear(), ld.getUTCMonth(), ld.getUTCDate()))
}

if (lastNorm && lastNorm.getTime() === todayNorm.getTime()) {
return
}

let newStreak: number

if (!lastNorm || lastNorm.getTime() < yesterdayNorm.getTime()) {
newStreak = 1
} else {
newStreak = user.streak + 1
}

await prisma.user.update({
where: { id: userId },
data: {
streak: newStreak,
lastReviewDate: todayNorm,
},
})
}

export async function getStreakInfo(slackId: string) {
const user = await prisma.user.findUnique({
where: { slackId },
select: { streak: true, lastReviewDate: true, slackId: true },
})

if (!user) return null

const liveStreak = computeStreak(user.streak, user.lastReviewDate)

if (liveStreak === 0 && user.streak > 0) {
await prisma.user.update({
where: { slackId },
data: { streak: 0 },
})
}

return {
slack_id: user.slackId,
is_streak_active: liveStreak > 0,
streak_number: liveStreak,
}
}
Loading