This commit is contained in:
Nathan 2026-02-17 13:07:29 -05:00
commit 13eed0aaac
3 changed files with 65 additions and 13 deletions

View file

@ -1671,8 +1671,8 @@ admin.post('/fix-negative-balances', async ({ headers, status }) => {
}
})
// CSV export of shipped projects for YSWS
admin.get('/export/shipped-csv', async ({ headers, status }) => {
// CSV export of projects under review for YSWS
admin.get('/export/review-csv', async ({ headers, status }) => {
try {
const user = await requireReviewer(headers as Record<string, string>)
if (!user) {
@ -1690,7 +1690,7 @@ admin.get('/export/shipped-csv', async ({ headers, status }) => {
.from(projectsTable)
.innerJoin(usersTable, eq(projectsTable.userId, usersTable.id))
.where(and(
eq(projectsTable.status, 'shipped'),
or(eq(projectsTable.status, 'waiting_for_review'), eq(projectsTable.status, 'pending_admin_approval')),
or(eq(projectsTable.deleted, 0), isNull(projectsTable.deleted))
))
.orderBy(desc(projectsTable.updatedAt))
@ -1717,16 +1717,16 @@ admin.get('/export/shipped-csv', async ({ headers, status }) => {
return new Response(rows.join('\n'), {
headers: {
'Content-Type': 'text/csv; charset=utf-8',
'Content-Disposition': 'attachment; filename="scraps-shipped-projects.csv"'
'Content-Disposition': 'attachment; filename="scraps-review-projects.csv"'
}
})
} catch (err) {
console.error(err)
return status(500, { error: 'Failed to export CSV' })
return status(500, { error: 'Failed to export review CSV' })
}
})
admin.get('/export/ysws-json', async ({ headers, status }) => {
admin.get('/export/review-json', async ({ headers, status }) => {
try {
const user = await requireReviewer(headers as Record<string, string>)
if (!user) {
@ -1744,7 +1744,7 @@ admin.get('/export/ysws-json', async ({ headers, status }) => {
.from(projectsTable)
.innerJoin(usersTable, eq(projectsTable.userId, usersTable.id))
.where(and(
eq(projectsTable.status, 'shipped'),
or(eq(projectsTable.status, 'waiting_for_review'), eq(projectsTable.status, 'pending_admin_approval')),
or(eq(projectsTable.deleted, 0), isNull(projectsTable.deleted))
))
.orderBy(desc(projectsTable.updatedAt))
@ -1764,7 +1764,7 @@ admin.get('/export/ysws-json', async ({ headers, status }) => {
})
} catch (err) {
console.error(err)
return status(500, { error: 'Failed to export YSWS JSON' })
return status(500, { error: 'Failed to export review JSON' })
}
})

View file

@ -612,15 +612,17 @@ shop.post('/items/:id/upgrade-probability', async ({ params, headers }) => {
const item = items[0]
if (item.count <= 0) {
return { error: 'Item is out of stock' }
}
try {
const result = await db.transaction(async (tx) => {
// Lock the user row to serialize spend operations and prevent race conditions
await tx.execute(sql`SELECT 1 FROM users WHERE id = ${user.id} FOR UPDATE`)
// Lock the item row and check stock atomically
const stockCheck = await tx.execute(sql`SELECT count FROM shop_items WHERE id = ${itemId} FOR UPDATE`)
if (!stockCheck.rows[0] || (stockCheck.rows[0] as { count: number }).count <= 0) {
throw { type: 'out_of_stock' }
}
const boostResult = await tx
.select({
boostPercent: sql<number>`COALESCE(SUM(${refineryOrdersTable.boostAmount}), 0)`
@ -697,6 +699,9 @@ shop.post('/items/:id/upgrade-probability', async ({ params, headers }) => {
return result
} catch (e) {
const err = e as { type?: string; balance?: number; cost?: number }
if (err.type === 'out_of_stock') {
return { error: 'Item is out of stock' }
}
if (err.type === 'max_probability') {
return { error: 'Already at maximum probability' }
}

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { Users, FolderKanban, Clock, Scale, Hourglass, ShieldAlert, Coins, XCircle } from '@lucide/svelte';
import { Users, FolderKanban, Clock, Scale, Hourglass, ShieldAlert, Coins, XCircle, Download } from '@lucide/svelte';
import { getUser } from '$lib/auth-client';
import { API_URL } from '$lib/config';
import { t } from '$lib/i18n';
@ -146,6 +146,24 @@
}
}
async function downloadExport(endpoint: string, filename: string) {
try {
const res = await fetch(`${API_URL}/admin/export/${endpoint}`, {
credentials: 'include'
});
if (!res.ok) return;
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
} catch (e) {
console.error('Export failed:', e);
}
}
onMount(async () => {
const user = await getUser();
if (!user || (user.role !== 'admin' && user.role !== 'reviewer')) {
@ -281,6 +299,35 @@
{/if}
</div>
<div class="mx-auto max-w-4xl px-6 pb-12 md:px-12">
<h2 class="mb-4 text-2xl font-bold">exports</h2>
<div class="rounded-2xl border-4 border-black p-6">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h3 class="flex items-center gap-2 text-lg font-bold">
<Download size={20} />
YSWS review export
</h3>
<p class="text-sm text-gray-500">download projects under review for YSWS fraud checking</p>
</div>
<div class="flex gap-3">
<button
onclick={() => downloadExport('review-csv', 'scraps-review-projects.csv')}
class="cursor-pointer rounded-full border-4 border-black px-4 py-2 font-bold transition-all duration-200 hover:border-dashed"
>
CSV
</button>
<button
onclick={() => downloadExport('review-json', 'scraps-review.json')}
class="cursor-pointer rounded-full border-4 border-black px-4 py-2 font-bold transition-all duration-200 hover:border-dashed"
>
JSON
</button>
</div>
</div>
</div>
</div>
{#if isAdmin}
<div class="mx-auto max-w-4xl px-6 pb-24 md:px-12">
<h2 class="mb-4 text-2xl font-bold">admin actions</h2>