diff --git a/pages/admin/card-import.js b/pages/admin/card-import.js index 843dfe1..5fe5f10 100644 --- a/pages/admin/card-import.js +++ b/pages/admin/card-import.js @@ -10,6 +10,41 @@ const CardImport = () => { const [setCode, setSetCode] = useState(''); const [isImporting, setIsImporting] = useState(false); const [importResult, setImportResult] = useState(null); + const [isSyncing, setIsSyncing] = useState(false); + const [syncResult, setSyncResult] = useState(null); + + const handleCatalogSync = async () => { + setIsSyncing(true); + setSyncResult(null); + + try { + const response = await fetch('/api/admin/sync-catalog', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${localStorage.getItem('auth_token')}`, + }, + }); + + const result = await response.json(); + + if (response.ok) { + setSyncResult({ success: true, ...result }); + } else { + setSyncResult({ + success: false, + message: result.error || 'Catalog sync failed', + }); + } + } catch (error) { + setSyncResult({ + success: false, + message: `Network error: ${error.message}`, + }); + } finally { + setIsSyncing(false); + } + }; const handleImport = async () => { if (!setCode.trim()) { @@ -130,6 +165,85 @@ const CardImport = () => {

+ {/* Catalog sync */} +
+
+
+

+ Catalog sync +

+

+ Import up to three newest missing MTG and Pokémon sets — the same job the weekly Vercel cron runs. + Use this to backfill recent releases without curl. +

+
+ +
+ + {syncResult && ( +
+

+ {syncResult.success ? 'Catalog sync complete' : 'Catalog sync failed'} +

+ {syncResult.success ? ( +
+

+ Imported {syncResult.imported} cards ({syncResult.skipped} skipped). +

+ {(syncResult.pendingMtgSets != null || syncResult.pendingPokemonSets != null) && ( +

+ Still pending: {syncResult.pendingMtgSets} MTG sets,{' '} + {syncResult.pendingPokemonSets} Pokémon sets. +

+ )} + {syncResult.lorcana &&

{syncResult.lorcana}

} + {Array.isArray(syncResult.setsProcessed) && syncResult.setsProcessed.length > 0 && ( +
    + {syncResult.setsProcessed.map((set) => ( +
  • + {set.name} ({set.game}/{set.setCode}) — {set.imported} imported +
  • + ))} +
+ )} + {Array.isArray(syncResult.errors) && syncResult.errors.length > 0 && ( +
    + {syncResult.errors.map((entry) => ( +
  • + {entry.game}/{entry.setCode}: {entry.message} +
  • + ))} +
+ )} +
+ ) : ( +

{syncResult.message}

+ )} +
+ )} +
+
{/* Import Form */}
diff --git a/pages/api/admin/sync-catalog.js b/pages/api/admin/sync-catalog.js new file mode 100644 index 0000000..e246178 --- /dev/null +++ b/pages/api/admin/sync-catalog.js @@ -0,0 +1,31 @@ +import { getUserFromRequest } from '../../../lib/permission-middleware'; +import { checkImportRateLimit } from '../../../lib/rate-limit.js'; +import { runCatalogSync } from '../../../lib/card-import/sync-catalog.js'; + +export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + const user = await getUserFromRequest(req); + if (!user) { + return res.status(401).json({ error: 'Authentication required' }); + } + if (user.role !== 'admin') { + return res.status(403).json({ error: 'Admin access required' }); + } + + const { allowed, reset } = await checkImportRateLimit(req, user.userId); + if (!allowed) { + res.setHeader('Retry-After', Math.ceil((reset - Date.now()) / 1000)); + return res.status(429).json({ error: 'Too many attempts. Try again later.' }); + } + + try { + const summary = await runCatalogSync(); + return res.status(200).json({ success: true, ...summary }); + } catch (error) { + console.error('[POST /api/admin/sync-catalog]', error); + return res.status(500).json({ error: 'Catalog sync failed', details: error.message }); + } +} diff --git a/vercel.json b/vercel.json index 398cd19..258f564 100644 --- a/vercel.json +++ b/vercel.json @@ -9,6 +9,9 @@ "functions": { "pages/api/cron/sync-catalog.js": { "maxDuration": 300 + }, + "pages/api/admin/sync-catalog.js": { + "maxDuration": 300 } } }