update logic fix

This commit is contained in:
NotARoomba 2026-02-23 15:35:59 -05:00
parent 3e897fbd26
commit df2039908f
7 changed files with 272 additions and 54 deletions

View file

@ -1,5 +1,3 @@
ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL_1FAEFB6177B4672DEE07F9D3AFC62588CCD2631EDCF22E8CCC1FB35B501C9C86on
# CLAUDE.md
## Architecture

66
backend/dist/index.js vendored
View file

@ -34517,6 +34517,26 @@ admin.get("/users/:id/bonuses", async ({ params, headers }) => {
return { error: "Failed to fetch user bonuses" };
}
});
admin.delete("/bonuses/:id", async ({ params, headers, status: status2 }) => {
try {
const user2 = await requireAdmin(headers);
if (!user2)
return status2(401, { error: "Unauthorized" });
const bonusId = parseInt(params.id);
if (!Number.isInteger(bonusId) || bonusId <= 0) {
return status2(400, { error: "Invalid bonus id" });
}
const bonus = await db.select({ id: userBonusesTable.id }).from(userBonusesTable).where(eq(userBonusesTable.id, bonusId)).limit(1);
if (!bonus[0]) {
return status2(404, { error: "Bonus not found" });
}
await db.delete(userBonusesTable).where(eq(userBonusesTable.id, bonusId));
return { success: true };
} catch (err) {
console.error(err);
return status2(500, { error: "Failed to delete bonus" });
}
});
admin.get("/reviews", async ({ headers, query }) => {
try {
const user2 = await requireReviewer(headers);
@ -34727,7 +34747,6 @@ admin.post("/reviews/:id", async ({ params, body, headers }) => {
}
let scrapsAwarded = 0;
if (action === "approved") {
const hours = hoursOverride ?? project[0].hours ?? 0;
const tier = tierOverride ?? project[0].tier ?? 1;
const { effectiveHours } = await computeEffectiveHoursForProject({
...project[0],
@ -35243,10 +35262,9 @@ admin.post("/shop/items", async ({ headers, body, status: status2 }) => {
category,
count,
baseProbability,
baseUpgradeCost,
costMultiplier,
boostAmount,
rollCostOverride
rollCostOverride,
perRollMultiplier: bodyPerRollMultiplier,
upgradeBudgetMultiplier: bodyUpgradeBudgetMultiplier
} = body;
if (!name?.trim() || !image?.trim() || !description?.trim() || !category?.trim()) {
return status2(400, { error: "All fields are required" });
@ -35262,8 +35280,8 @@ admin.post("/shop/items", async ({ headers, body, status: status2 }) => {
try {
const dollarCost = typeof price === "number" ? price / SCRAPS_PER_DOLLAR : 0;
const pricing = computeItemPricing(dollarCost, baseProbability, count ?? 1);
const perRollMultiplierVal = body?.perRollMultiplier ?? 0.05;
const upgradeBudgetMultiplierVal = body?.upgradeBudgetMultiplier ?? 3;
const perRollMultiplierVal = bodyPerRollMultiplier ?? 0.05;
const upgradeBudgetMultiplierVal = bodyUpgradeBudgetMultiplier ?? 3;
await db.insert(shopItemsTable).values({
name: name.trim(),
image: image.trim(),
@ -35744,10 +35762,12 @@ admin.delete("/orders/:id", async ({ params, headers, body, status: status2 }) =
let alreadyRow = null;
try {
const already = await db.execute(sql`SELECT 1 FROM admin_deleted_orders WHERE original_order_id = ${orderId} AND restored = false LIMIT 1`);
alreadyRow = already.rows?.[0] ?? (Array.isArray(already) ? already[0] : null);
const alreadyResult = already;
alreadyRow = alreadyResult.rows?.[0] ?? (Array.isArray(already) ? already[0] : null);
} catch (err) {
const msg = err?.message ?? "";
const code = err?.code ?? null;
const dbErr = err;
const msg = dbErr?.message ?? "";
const code = dbErr?.code ?? null;
if (msg.includes("does not exist") || code === "42P01") {
await db.execute(sql`
CREATE TABLE IF NOT EXISTS admin_deleted_orders (
@ -35777,7 +35797,8 @@ admin.delete("/orders/:id", async ({ params, headers, body, status: status2 }) =
CREATE INDEX IF NOT EXISTS idx_admin_deleted_orders_original_order_id ON admin_deleted_orders (original_order_id);
`);
const already2 = await db.execute(sql`SELECT 1 FROM admin_deleted_orders WHERE original_order_id = ${orderId} AND restored = false LIMIT 1`);
alreadyRow = already2.rows?.[0] ?? (Array.isArray(already2) ? already2[0] : null);
const already2Result = already2;
alreadyRow = already2Result.rows?.[0] ?? (Array.isArray(already2) ? already2[0] : null);
} else {
throw err;
}
@ -35824,12 +35845,13 @@ admin.delete("/orders/:id", async ({ params, headers, body, status: status2 }) =
return { success: true };
} catch (err) {
try {
console.error("Admin delete order error (stack):", err?.stack ?? err);
console.error("Admin delete order error (name):", err?.name ?? null);
console.error("Admin delete order error (message):", err?.message ?? String(err));
console.error("Admin delete order error (cause):", err?.cause ?? null);
console.error("Admin delete order error (query):", err?.query ?? null);
console.error("Admin delete order error (params):", err?.params ?? null);
const e = err;
console.error("Admin delete order error (stack):", e?.stack ?? err);
console.error("Admin delete order error (name):", e?.name ?? null);
console.error("Admin delete order error (message):", e?.message ?? String(err));
console.error("Admin delete order error (cause):", e?.cause ?? null);
console.error("Admin delete order error (query):", e?.query ?? null);
console.error("Admin delete order error (params):", e?.params ?? null);
try {
console.error("Admin delete order error (full):", JSON.stringify(err, Object.getOwnPropertyNames(err), 2));
} catch {
@ -35840,7 +35862,7 @@ admin.delete("/orders/:id", async ({ params, headers, body, status: status2 }) =
console.error("Original error:", err);
}
return status2(500, {
error: "Failed to delete order: " + (err?.message || String(err))
error: "Failed to delete order: " + (err instanceof Error ? err.message : String(err))
});
}
});
@ -35853,7 +35875,8 @@ admin.post("/orders/:id/restore", async ({ params, headers, status: status2 }) =
if (!Number.isInteger(originalOrderId) || originalOrderId <= 0)
return status2(400, { error: "Invalid order id" });
const archivedRes = await db.execute(sql`SELECT * FROM admin_deleted_orders WHERE original_order_id = ${originalOrderId} AND restored = false LIMIT 1`);
const archived = archivedRes.rows?.[0] ?? (Array.isArray(archivedRes) ? archivedRes[0] : null);
const archivedResult = archivedRes;
const archived = archivedResult.rows?.[0] ?? (Array.isArray(archivedRes) ? archivedRes[0] : null);
if (!archived)
return status2(404, {
error: "Archived order not found or already restored"
@ -36045,7 +36068,8 @@ admin.get("/users/:id/timeline", async ({ params, headers, status: status2 }) =>
type: "bonus",
amount: b.amount,
description: b.reason,
date: b.createdAt.toISOString()
date: b.createdAt.toISOString(),
bonusId: b.id
});
}
for (const o of shopOrders) {
@ -36103,7 +36127,7 @@ admin.get("/users/:id/timeline", async ({ params, headers, status: status2 }) =>
return status2(500, { error: "Failed to fetch user timeline" });
}
});
admin.post("/sync-ysws", async ({ headers, set }) => {
admin.post("/sync-ysws", async ({ headers }) => {
const user2 = await requireAdmin(headers);
if (!user2)
return { error: "Unauthorized" };

View file

@ -595,6 +595,38 @@ admin.get("/users/:id/bonuses", async ({ params, headers }) => {
}
});
// Delete a bonus (admin only)
admin.delete("/bonuses/:id", async ({ params, headers, status }) => {
try {
const user = await requireAdmin(headers as Record<string, string>);
if (!user) return status(401, { error: "Unauthorized" });
const bonusId = parseInt(params.id);
if (!Number.isInteger(bonusId) || bonusId <= 0) {
return status(400, { error: "Invalid bonus id" });
}
const bonus = await db
.select({ id: userBonusesTable.id })
.from(userBonusesTable)
.where(eq(userBonusesTable.id, bonusId))
.limit(1);
if (!bonus[0]) {
return status(404, { error: "Bonus not found" });
}
await db
.delete(userBonusesTable)
.where(eq(userBonusesTable.id, bonusId));
return { success: true };
} catch (err) {
console.error(err);
return status(500, { error: "Failed to delete bonus" });
}
});
// Get projects waiting for review
admin.get("/reviews", async ({ headers, query }) => {
try {
@ -3000,6 +3032,7 @@ admin.get("/users/:id/timeline", async ({ params, headers, status }) => {
itemName?: string;
paid?: boolean;
orderId?: number;
bonusId?: number;
};
const timeline: TimelineEvent[] = [];
@ -3022,6 +3055,7 @@ admin.get("/users/:id/timeline", async ({ params, headers, status }) => {
amount: b.amount,
description: b.reason,
date: b.createdAt.toISOString(),
bonusId: b.id,
});
}

View file

@ -89,6 +89,7 @@
let lastDeletedError = $state<string | null>(null);
let filterItem = $state('');
let filterUser = $state('');
let filterRegion = $state<'' | 'us' | 'intl'>('');
let uniqueItems = $derived(
[...new Map(orders.map((o) => [o.itemName, o.itemName])).values()].sort()
@ -113,6 +114,15 @@
result = result.filter((o) => o.username === filterUser);
}
if (filterRegion) {
result = result.filter((o) => {
const addr = parseShippingAddress(o.shippingAddress);
const country = addr?.country?.toLowerCase().trim() ?? '';
const isUS = country === 'us' || country === 'usa' || country === 'united states' || country === 'united states of america';
return filterRegion === 'us' ? isUS : !isUS;
});
}
if (searchQuery.trim()) {
const q = searchQuery.trim().toLowerCase();
result = result.filter((o) => {
@ -435,6 +445,18 @@
{/each}
</select>
</div>
<div class="flex flex-col">
<label for="filter-region" class="mb-1 text-xs font-bold text-gray-500 uppercase">region</label>
<select
id="filter-region"
bind:value={filterRegion}
class="cursor-pointer rounded-xl border-4 border-black px-3 py-2 font-bold transition-all duration-200 focus:border-dashed focus:outline-none {filterRegion ? 'bg-black text-white' : ''}"
>
<option value="">all regions</option>
<option value="us">US only</option>
<option value="intl">non-US</option>
</select>
</div>
<div class="flex items-end gap-2">
<div class="flex flex-col">
<label for="date-from" class="mb-1 text-xs font-bold text-gray-500 uppercase">from</label>
@ -454,13 +476,14 @@
class="rounded-xl border-4 border-black px-3 py-2 font-bold transition-all duration-200 focus:border-dashed focus:outline-none"
/>
</div>
{#if dateFrom || dateTo || filterItem || filterUser}
{#if dateFrom || dateTo || filterItem || filterUser || filterRegion}
<button
onclick={() => {
dateFrom = '';
dateTo = '';
filterItem = '';
filterUser = '';
filterRegion = '';
}}
class="cursor-pointer rounded-xl border-4 border-black px-3 py-2 font-bold transition-all duration-200 hover:border-dashed"
title="clear filters"

View file

@ -74,6 +74,7 @@
updateDescription: string | null;
aiDescription: string | null;
reviewerNotes: string | null;
scrapsAwarded: number;
}
interface User {
@ -114,6 +115,19 @@
project ? Math.max(0, (hoursOverride ?? project.hours) - deductedHours) : 0
);
const PHI = (1 + Math.sqrt(5)) / 2;
const MULTIPLIER = 10;
const TIER_MULTIPLIERS: Record<number, number> = { 1: 0.8, 2: 1.0, 3: 1.25, 4: 1.5 };
let isUpdate = $derived(project ? project.scrapsAwarded > 0 : false);
let previewTier = $derived(project ? (tierOverride ?? project.tier ?? 1) : 1);
let previewScraps = $derived(
Math.floor(effectiveHours * PHI * MULTIPLIER * (TIER_MULTIPLIERS[previewTier] ?? 1.0))
);
let additionalScraps = $derived(
project ? Math.max(0, previewScraps - project.scrapsAwarded) : previewScraps
);
let hoursOverrideError = $derived(
hoursOverride !== undefined && project && hoursOverride > project.hours
? `Hours override cannot exceed project hours (${formatHours(project.hours)}h)`
@ -522,6 +536,29 @@
</div>
{/if}
{#if isUpdate}
<div class="mt-4 rounded-lg border-2 border-dashed border-blue-500 bg-blue-50 p-4">
<p class="mb-2 flex items-center gap-1.5 text-sm font-bold text-blue-700">
<RefreshCw size={14} />
update — scraps preview
</p>
<p class="mb-2 text-sm text-blue-700">
this is an updated project. previously awarded scraps will be subtracted from the new total.
</p>
<div class="flex flex-wrap gap-3 text-sm font-bold">
<span class="rounded-full border-2 border-blue-600 bg-blue-100 px-3 py-1 text-blue-800">
previously awarded: {project.scrapsAwarded} scraps
</span>
<span class="rounded-full border-2 border-blue-600 bg-blue-100 px-3 py-1 text-blue-800">
new total: {previewScraps} scraps ({formatHours(effectiveHours)}h × tier {previewTier})
</span>
<span class="rounded-full border-2 border-black bg-blue-200 px-3 py-1 text-black">
additional: +{additionalScraps} scraps
</span>
</div>
</div>
{/if}
<div class="mt-4 flex flex-wrap gap-3">
{#if project.githubUrl}
<a

View file

@ -68,6 +68,7 @@
updateDescription: string | null;
aiDescription: string | null;
reviewerNotes: string | null;
scrapsAwarded: number;
}
interface User {
@ -102,6 +103,19 @@
let effectiveHours = $derived(
project ? Math.max(0, (hoursOverride ?? project.hoursOverride ?? project.hours) - deductedHours) : 0
);
const PHI = (1 + Math.sqrt(5)) / 2;
const MULTIPLIER = 10;
const TIER_MULTIPLIERS: Record<number, number> = { 1: 0.8, 2: 1.0, 3: 1.25, 4: 1.5 };
let isUpdate = $derived(project ? project.scrapsAwarded > 0 : false);
let previewTier = $derived(project ? (project.tierOverride ?? project.tier ?? 1) : 1);
let previewScraps = $derived(
Math.floor(effectiveHours * PHI * MULTIPLIER * (TIER_MULTIPLIERS[previewTier] ?? 1.0))
);
let additionalScraps = $derived(
project ? Math.max(0, previewScraps - project.scrapsAwarded) : previewScraps
);
let hoursOverrideError = $derived(
hoursOverride !== undefined && project && hoursOverride > project.hours
? `hours override cannot exceed project hours (${formatHours(project.hours)}h)`
@ -408,6 +422,29 @@
</div>
{/if}
{#if isUpdate}
<div class="mt-4 rounded-lg border-2 border-dashed border-blue-500 bg-blue-50 p-4">
<p class="mb-2 flex items-center gap-1.5 text-sm font-bold text-blue-700">
<RefreshCw size={14} />
update — scraps preview
</p>
<p class="mb-2 text-sm text-blue-700">
this is an updated project. previously awarded scraps will be subtracted from the new total.
</p>
<div class="flex flex-wrap gap-3 text-sm font-bold">
<span class="rounded-full border-2 border-blue-600 bg-blue-100 px-3 py-1 text-blue-800">
previously awarded: {project.scrapsAwarded} scraps
</span>
<span class="rounded-full border-2 border-blue-600 bg-blue-100 px-3 py-1 text-blue-800">
new total: {previewScraps} scraps ({formatHours(effectiveHours)}h × tier {previewTier})
</span>
<span class="rounded-full border-2 border-black bg-blue-200 px-3 py-1 text-black">
additional: +{additionalScraps} scraps
</span>
</div>
</div>
{/if}
<div class="mt-4 flex flex-wrap gap-3">
{#if project.githubUrl}
<a

View file

@ -17,6 +17,7 @@
TrendingUp,
TrendingDown,
Undo2,
Trash2,
ShoppingCart,
Dices,
FileText,
@ -102,6 +103,7 @@
itemName?: string;
paid?: boolean;
orderId?: number;
bonusId?: number;
}
let timeline = $state<TimelineEvent[]>([]);
@ -114,7 +116,8 @@
let timelineLoading = $state(false);
let showTimeline = $state(false);
let undoingOrder = $state<number | null>(null);
let showUndoConfirm = $state<number | null>(null);
let deletingBonus = $state<number | null>(null);
let showDeleteConfirm = $state<{ type: 'order'; id: number } | { type: 'bonus'; id: number } | null>(null);
let showUnshipConfirm = $state<number | null>(null);
let unshipReason = $state('');
let unshipping = $state(false);
@ -284,15 +287,13 @@
async function undoOrder(orderId: number) {
undoingOrder = orderId;
showUndoConfirm = null;
showDeleteConfirm = null;
try {
// Directly call DELETE with a required reason payload.
// The server expects a short reason when deleting/reverting an order.
const refundRes = await fetch(`${API_URL}/admin/orders/${orderId}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ reason: 'reverted via admin UI' })
body: JSON.stringify({ reason: 'reverted via admin timeline' })
});
const refundResult = await refundRes.json();
if (refundResult.error) {
@ -307,6 +308,44 @@
}
}
async function deleteBonus(bonusId: number) {
deletingBonus = bonusId;
showDeleteConfirm = null;
try {
const res = await fetch(`${API_URL}/admin/bonuses/${bonusId}`, {
method: 'DELETE',
credentials: 'include'
});
const result = await res.json();
if (result.error) {
console.error(result.error);
return;
}
await fetchTimeline();
} catch (e) {
console.error('Failed to delete bonus:', e);
} finally {
deletingBonus = null;
}
}
function confirmTimelineDelete(event: TimelineEvent) {
if (event.type === 'bonus' && event.bonusId) {
showDeleteConfirm = { type: 'bonus', id: event.bonusId };
} else if (event.type.startsWith('shop_') && event.orderId) {
showDeleteConfirm = { type: 'order', id: event.orderId };
}
}
function executeTimelineDelete() {
if (!showDeleteConfirm) return;
if (showDeleteConfirm.type === 'bonus') {
deleteBonus(showDeleteConfirm.id);
} else {
undoOrder(showDeleteConfirm.id);
}
}
async function unshipProject(projectId: number) {
unshipping = true;
try {
@ -835,31 +874,19 @@
<p class="truncate text-sm text-gray-700">{event.description}</p>
</div>
<div class="flex shrink-0 items-center gap-2">
{#if event.type.startsWith('shop_') && event.orderId}
{#if showUndoConfirm === event.orderId}
<div class="flex items-center gap-1">
{#if (event.type.startsWith('shop_') && event.orderId) || (event.type === 'bonus' && event.bonusId)}
<button
onclick={() => undoOrder(event.orderId ?? 0)}
disabled={undoingOrder === event.orderId}
class="cursor-pointer rounded-full bg-red-600 px-2 py-1 text-xs font-bold text-white transition-all duration-200 hover:bg-red-700 disabled:opacity-50"
>
{undoingOrder === event.orderId ? '...' : 'confirm'}
</button>
<button
onclick={() => (showUndoConfirm = null)}
class="cursor-pointer rounded-full border-2 border-black px-2 py-1 text-xs font-bold transition-all duration-200 hover:border-dashed"
>
cancel
</button>
</div>
{:else}
<button
onclick={() => (showUndoConfirm = event.orderId ?? null)}
onclick={() => confirmTimelineDelete(event)}
disabled={undoingOrder === event.orderId || deletingBonus === event.bonusId}
class="cursor-pointer rounded-full border-2 border-red-600 px-2 py-1 text-xs font-bold text-red-600 transition-all duration-200 hover:border-dashed disabled:opacity-50"
title="delete"
>
<Undo2 size={12} />
</button>
{#if undoingOrder === event.orderId || deletingBonus === event.bonusId}
...
{:else}
<Trash2 size={12} />
{/if}
</button>
{/if}
<div class="text-right">
<p class="font-bold {getTimelineColor(event.type, event.amount)}">
@ -999,3 +1026,41 @@
</div>
</div>
{/if}
<!-- Timeline Delete Confirmation Modal -->
{#if showDeleteConfirm}
<div
class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
onclick={(e) => e.target === e.currentTarget && (showDeleteConfirm = null)}
onkeydown={(e) => e.key === 'Escape' && (showDeleteConfirm = null)}
role="dialog"
tabindex="-1"
>
<div class="w-full max-w-md rounded-2xl border-4 border-black bg-white p-6">
<h2 class="mb-4 text-2xl font-bold">
delete {showDeleteConfirm.type === 'bonus' ? 'bonus' : 'order'}
</h2>
<p class="mb-6 text-gray-600">
{#if showDeleteConfirm.type === 'bonus'}
this will permanently delete this bonus entry from the database. the user's balance will be recalculated.
{:else}
this will permanently delete this order and all associated records (refinery upgrades, rolls, penalties). item stock will be restored.
{/if}
</p>
<div class="flex gap-3">
<button
onclick={() => (showDeleteConfirm = null)}
class="flex-1 cursor-pointer rounded-full border-4 border-black px-4 py-2 font-bold transition-all duration-200 hover:border-dashed"
>
cancel
</button>
<button
onclick={executeTimelineDelete}
class="flex-1 cursor-pointer rounded-full border-4 border-red-600 bg-red-600 px-4 py-2 font-bold text-white transition-all duration-200 hover:border-dashed disabled:cursor-not-allowed disabled:opacity-50"
>
delete
</button>
</div>
</div>
</div>
{/if}