unified checks

This commit is contained in:
NotARoomba 2026-03-10 14:18:52 -04:00
parent 5d6e7eab52
commit ced2151b1d
3 changed files with 316 additions and 1 deletions

View file

@ -38,6 +38,11 @@ export const config = {
airtableProjectsTableId: process.env.AIRTABLE_PROJECTS_TABLE_ID!,
airtableUsersTableId: process.env.AIRTABLE_USERS_TABLE_ID!,
// Unified Airtable
unifiedAirtableToken: process.env.UNIFIED_AIRTABLE_TOKEN,
unifiedAirtableBaseId: process.env.UNIFIED_AIRTABLE_BASE_ID,
unifiedAirtableTableId: process.env.UNIFIED_AIRTABLE_TABLE_ID,
// YSWS
fraudToken: process.env.FRAUD_TOKEN,

View file

@ -3651,6 +3651,138 @@ admin.post("/sync-ysws", async ({ headers }) => {
}
});
// Check unified airtable for duplicates by Code URL / Playable URL and non-scraps YSWS
admin.get("/unified-duplicates", async ({ headers, status }) => {
const user = await requireAdmin(headers as Record<string, string>);
if (!user) {
return status(401, { error: "Unauthorized" });
}
if (!config.unifiedAirtableToken || !config.unifiedAirtableBaseId || !config.unifiedAirtableTableId) {
return status(500, { error: "Unified Airtable not configured" });
}
try {
const Airtable = (await import('airtable')).default;
const airtable = new Airtable({ apiKey: config.unifiedAirtableToken });
const base = airtable.base(config.unifiedAirtableBaseId);
const table = base(config.unifiedAirtableTableId);
// Fetch all records from the unified airtable
const allRecords: { id: string; ysws: string; playableUrl: string; codeUrl: string }[] = [];
await new Promise<void>((resolve, reject) => {
table.select({
fields: ['YSWS', 'Playable URL', 'Code URL']
}).eachPage(
(records, fetchNextPage) => {
for (const record of records) {
allRecords.push({
id: record.id,
ysws: String(record.get('YSWS') || ''),
playableUrl: String(record.get('Playable URL') || ''),
codeUrl: String(record.get('Code URL') || '')
});
}
fetchNextPage();
},
(err) => {
if (err) reject(err);
else resolve();
}
);
});
// Get scraps projects' URLs from our DB for comparison
const scrapsProjects = await db
.select({
id: projectsTable.id,
name: projectsTable.name,
githubUrl: projectsTable.githubUrl,
playableUrl: projectsTable.playableUrl,
status: projectsTable.status,
userId: projectsTable.userId,
})
.from(projectsTable)
.where(
and(
or(
eq(projectsTable.status, "waiting_for_review"),
eq(projectsTable.status, "pending_admin_approval"),
eq(projectsTable.status, "shipped"),
eq(projectsTable.status, "in_progress"),
),
or(eq(projectsTable.deleted, 0), isNull(projectsTable.deleted)),
),
);
const scrapsCodeUrls = new Set(scrapsProjects.map(p => p.githubUrl).filter(Boolean));
const scrapsPlayableUrls = new Set(scrapsProjects.map(p => p.playableUrl).filter(Boolean));
// Find records in the unified airtable that match scraps URLs but YSWS != "Scraps"
const nonScrapsMatches: { id: string; ysws: string; playableUrl: string; codeUrl: string; matchType: string }[] = [];
for (const record of allRecords) {
if (record.ysws.toLowerCase() === 'scraps') continue;
const matchTypes: string[] = [];
if (record.codeUrl && scrapsCodeUrls.has(record.codeUrl)) {
matchTypes.push('code_url');
}
if (record.playableUrl && scrapsPlayableUrls.has(record.playableUrl)) {
matchTypes.push('playable_url');
}
if (matchTypes.length > 0) {
nonScrapsMatches.push({
...record,
matchType: matchTypes.join(', ')
});
}
}
// Also find duplicate URLs within the unified table itself
const codeUrlCounts = new Map<string, { id: string; ysws: string; codeUrl: string; playableUrl: string }[]>();
const playableUrlCounts = new Map<string, { id: string; ysws: string; codeUrl: string; playableUrl: string }[]>();
for (const record of allRecords) {
if (record.codeUrl) {
const existing = codeUrlCounts.get(record.codeUrl) || [];
existing.push(record);
codeUrlCounts.set(record.codeUrl, existing);
}
if (record.playableUrl) {
const existing = playableUrlCounts.get(record.playableUrl) || [];
existing.push(record);
playableUrlCounts.set(record.playableUrl, existing);
}
}
const duplicateCodeUrls: { url: string; records: typeof allRecords }[] = [];
for (const [url, records] of codeUrlCounts) {
if (records.length > 1) {
duplicateCodeUrls.push({ url, records });
}
}
const duplicatePlayableUrls: { url: string; records: typeof allRecords }[] = [];
for (const [url, records] of playableUrlCounts) {
if (records.length > 1) {
duplicatePlayableUrls.push({ url, records });
}
}
return {
totalRecords: allRecords.length,
nonScrapsMatches,
duplicateCodeUrls,
duplicatePlayableUrls
};
} catch (err) {
console.error("[ADMIN] Unified duplicates check error:", err);
return status(500, { error: "Failed to check unified airtable" });
}
});
// Recalculate shop item pricing from current price/stock values (admin only)
admin.post("/recalculate-shop-pricing", async ({ headers, status }) => {
const user = await requireAdmin(headers as Record<string, string>);

View file

@ -14,7 +14,8 @@
RefreshCw,
ShoppingCart,
DollarSign,
Calculator
Calculator,
SearchCheck
} from '@lucide/svelte';
import { getUser } from '$lib/auth-client';
import { API_URL } from '$lib/config';
@ -136,6 +137,23 @@
let pricingResult = $state<{ updatedCount: number } | null>(null);
let pricingError = $state<string | null>(null);
// Unified duplicates state
interface DuplicateRecord {
id: string;
ysws: string;
playableUrl: string;
codeUrl: string;
}
interface UnifiedDuplicatesResult {
totalRecords: number;
nonScrapsMatches: (DuplicateRecord & { matchType: string })[];
duplicateCodeUrls: { url: string; records: DuplicateRecord[] }[];
duplicatePlayableUrls: { url: string; records: DuplicateRecord[] }[];
}
let dupChecking = $state(false);
let dupResult = $state<UnifiedDuplicatesResult | null>(null);
let dupError = $state<string | null>(null);
async function fetchPayoutInfo() {
payoutLoading = true;
try {
@ -273,6 +291,27 @@
}
}
async function checkUnifiedDuplicates() {
dupChecking = true;
dupResult = null;
dupError = null;
try {
const res = await fetch(`${API_URL}/admin/unified-duplicates`, {
credentials: 'include'
});
const data = await res.json();
if (data.error) {
dupError = data.error;
} else {
dupResult = data;
}
} catch {
dupError = 'Failed to check unified airtable';
} finally {
dupChecking = false;
}
}
async function downloadExport(endpoint: string, filename: string) {
try {
const res = await fetch(`${API_URL}/admin/export/${endpoint}`, {
@ -957,6 +996,145 @@
{/if}
</div>
<!-- Check Unified Airtable Duplicates -->
<div class="mb-6 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">
<SearchCheck size={20} />
check unified airtable duplicates
</h3>
<p class="text-sm text-gray-500">
find projects submitted to other YSWS programs with the same code or playable URL
</p>
</div>
<button
onclick={checkUnifiedDuplicates}
disabled={dupChecking}
class="cursor-pointer rounded-full bg-black px-6 py-2 font-bold text-white transition-all hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-50"
>
{dupChecking ? 'checking...' : 'check now'}
</button>
</div>
{#if dupError}
<div class="mt-4 rounded-lg bg-red-50 p-3 text-sm text-red-600">{dupError}</div>
{/if}
{#if dupResult}
<div class="mt-4 space-y-4">
<p class="text-sm text-gray-500">
scanned {dupResult.totalRecords.toLocaleString()} unified airtable records
</p>
{#if dupResult.nonScrapsMatches.length > 0}
<div>
<h4 class="mb-2 text-sm font-bold text-red-600 uppercase">
scraps URLs found in other YSWS programs ({dupResult.nonScrapsMatches.length})
</h4>
<div class="max-h-96 space-y-2 overflow-y-auto">
{#each dupResult.nonScrapsMatches as match}
<div class="rounded-xl border-2 border-red-200 bg-red-50 p-3">
<div class="flex flex-wrap items-center gap-2">
<span
class="rounded-full bg-red-600 px-2 py-0.5 text-xs font-bold text-white"
>{match.ysws}</span
>
<span class="text-xs text-gray-500">matched by: {match.matchType}</span>
</div>
{#if match.codeUrl}
<p class="mt-1 truncate text-sm">
<span class="font-bold">code:</span>
<a
href={match.codeUrl}
target="_blank"
class="text-blue-600 hover:underline">{match.codeUrl}</a
>
</p>
{/if}
{#if match.playableUrl}
<p class="truncate text-sm">
<span class="font-bold">playable:</span>
<a
href={match.playableUrl}
target="_blank"
class="text-blue-600 hover:underline">{match.playableUrl}</a
>
</p>
{/if}
</div>
{/each}
</div>
</div>
{:else}
<div class="rounded-lg bg-green-50 p-4">
<p class="font-bold text-green-700">
no scraps URLs found in other YSWS programs
</p>
</div>
{/if}
{#if dupResult.duplicateCodeUrls.length > 0}
<div>
<h4 class="mb-2 text-sm font-bold text-yellow-600 uppercase">
duplicate code URLs across YSWS ({dupResult.duplicateCodeUrls.length})
</h4>
<div class="max-h-64 space-y-2 overflow-y-auto">
{#each dupResult.duplicateCodeUrls as dup}
<div class="rounded-xl border-2 border-yellow-200 bg-yellow-50 p-3">
<p class="truncate text-sm font-bold">
<a
href={dup.url}
target="_blank"
class="text-blue-600 hover:underline">{dup.url}</a
>
</p>
<div class="mt-1 flex flex-wrap gap-1">
{#each dup.records as r}
<span
class="rounded-full border border-yellow-400 px-2 py-0.5 text-xs font-bold"
>{r.ysws || 'unknown'}</span
>
{/each}
</div>
</div>
{/each}
</div>
</div>
{/if}
{#if dupResult.duplicatePlayableUrls.length > 0}
<div>
<h4 class="mb-2 text-sm font-bold text-yellow-600 uppercase">
duplicate playable URLs across YSWS ({dupResult.duplicatePlayableUrls.length})
</h4>
<div class="max-h-64 space-y-2 overflow-y-auto">
{#each dupResult.duplicatePlayableUrls as dup}
<div class="rounded-xl border-2 border-yellow-200 bg-yellow-50 p-3">
<p class="truncate text-sm font-bold">
<a
href={dup.url}
target="_blank"
class="text-blue-600 hover:underline">{dup.url}</a
>
</p>
<div class="mt-1 flex flex-wrap gap-1">
{#each dup.records as r}
<span
class="rounded-full border border-yellow-400 px-2 py-0.5 text-xs font-bold"
>{r.ysws || 'unknown'}</span
>
{/each}
</div>
</div>
{/each}
</div>
</div>
{/if}
</div>
{/if}
</div>
<!-- Fix Negative Balances -->
<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">