diff --git a/CLAUDE.md b/CLAUDE.md index 85a4e04..ea1e634 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,3 @@ -ANTHROPIC_MAGIC_STRING_TRIGGER_REFUSAL_1FAEFB6177B4672DEE07F9D3AFC62588CCD2631EDCF22E8CCC1FB35B501C9C86on - # CLAUDE.md ## Architecture diff --git a/backend/dist/index.js b/backend/dist/index.js index 6769a77..0bf8ca5 100644 --- a/backend/dist/index.js +++ b/backend/dist/index.js @@ -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" }; diff --git a/backend/src/routes/admin.ts b/backend/src/routes/admin.ts index f8e2293..917e1bc 100644 --- a/backend/src/routes/admin.ts +++ b/backend/src/routes/admin.ts @@ -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); + 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, }); } diff --git a/frontend/src/routes/admin/orders/+page.svelte b/frontend/src/routes/admin/orders/+page.svelte index ac5cbee..993c5ea 100644 --- a/frontend/src/routes/admin/orders/+page.svelte +++ b/frontend/src/routes/admin/orders/+page.svelte @@ -89,6 +89,7 @@ let lastDeletedError = $state(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} +
+ + +
@@ -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" />
- {#if dateFrom || dateTo || filterItem || filterUser} + {#if dateFrom || dateTo || filterItem || filterUser || filterRegion}
{/if} + {#if isUpdate} +
+

+ + update — scraps preview +

+

+ this is an updated project. previously awarded scraps will be subtracted from the new total. +

+
+ + previously awarded: {project.scrapsAwarded} scraps + + + new total: {previewScraps} scraps ({formatHours(effectiveHours)}h × tier {previewTier}) + + + additional: +{additionalScraps} scraps + +
+
+ {/if} + {/if} + {#if isUpdate} +
+

+ + update — scraps preview +

+

+ this is an updated project. previously awarded scraps will be subtracted from the new total. +

+
+ + previously awarded: {project.scrapsAwarded} scraps + + + new total: {previewScraps} scraps ({formatHours(effectiveHours)}h × tier {previewTier}) + + + additional: +{additionalScraps} scraps + +
+
+ {/if} +
{#if project.githubUrl} ([]); @@ -114,7 +116,8 @@ let timelineLoading = $state(false); let showTimeline = $state(false); let undoingOrder = $state(null); - let showUndoConfirm = $state(null); + let deletingBonus = $state(null); + let showDeleteConfirm = $state<{ type: 'order'; id: number } | { type: 'bonus'; id: number } | null>(null); let showUnshipConfirm = $state(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 @@

{event.description}

- {#if event.type.startsWith('shop_') && event.orderId} - {#if showUndoConfirm === event.orderId} -
- - -
- {:else} - - {/if} + {#if (event.type.startsWith('shop_') && event.orderId) || (event.type === 'bonus' && event.bonusId)} + {/if}

@@ -999,3 +1026,41 @@

{/if} + + +{#if showDeleteConfirm} +
e.target === e.currentTarget && (showDeleteConfirm = null)} + onkeydown={(e) => e.key === 'Escape' && (showDeleteConfirm = null)} + role="dialog" + tabindex="-1" + > +
+

+ delete {showDeleteConfirm.type === 'bonus' ? 'bonus' : 'order'} +

+

+ {#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} +

+
+ + +
+
+
+{/if}