diff --git a/backend/src/tools/makerworld/routes.js b/backend/src/tools/makerworld/routes.js index 84995dd..4e44fba 100644 --- a/backend/src/tools/makerworld/routes.js +++ b/backend/src/tools/makerworld/routes.js @@ -130,7 +130,16 @@ async function getNextDataFromPage(page) { } async function isCloudflareChallenge(page) { const title = await page.title().catch(() => ''); - return /just a moment|checking your browser|attention required/i.test(title); + return /just a moment|checking your browser|attention required|einen moment|überprüfung|einen augenblick|verifizierung ihres browsers/i.test(title); +} +// Speichert einen Screenshot + die aktuelle URL/Titel zur Fehlerdiagnose +async function saveDebugScreenshot(userId, page) { + const dir = `${PROFILE_BASE_DIR}/${userId}`; + fs.mkdirSync(dir, { recursive: true }); + await page.screenshot({ path: `${dir}/last-failure.png`, fullPage: false }); + fs.writeFileSync(`${dir}/last-failure.json`, JSON.stringify({ + url: page.url(), title: await page.title().catch(() => '?'), at: new Date().toISOString(), + })); } function isLoginUrl(url) { return /bambulab\.com\/[^/]*\/?sign-in|makerworld\.com\/[a-z]{2}\/sign-in/i.test(url || ''); @@ -155,7 +164,8 @@ async function attemptLogin(userId, browser, page) { ]); const pwField = await findFirstSelector(page, ['input[type="password"]']); if (!emailField || !pwField) { - throw new Error('Login-Formular nicht gefunden (Selektoren evtl. veraltet)'); + await saveDebugScreenshot(userId, page).catch(() => {}); + throw new Error(`Login-Formular nicht gefunden (Selektoren evtl. veraltet) — Seite: "${await page.title().catch(()=>'?')}" (${page.url()})`); } await page.type(emailField, creds.username, { delay: 25 }); @@ -192,6 +202,7 @@ async function attemptLogin(userId, browser, page) { throw err; } + await saveDebugScreenshot(userId, page).catch(() => {}); throw new Error('Login fehlgeschlagen (weder Daten noch Code-Feld erkannt) — evtl. falsche Zugangsdaten oder geändertes Seitenlayout'); } @@ -205,14 +216,17 @@ async function fetchStatsViaBrowserInner(userId, forceRefresh) { } let browser; + let page; try { browser = await launchBrowser(userId); - const page = await browser.newPage(); + page = await browser.newPage(); await page.setUserAgent(USER_AGENT); await page.setViewport({ width: 1366, height: 900 }); await page.goto(DATA_URL, { waitUntil: 'networkidle2', timeout: 45000 }); - for (let i = 0; i < 3 && await isCloudflareChallenge(page); i++) { + // Bis zu 20 Sekunden Zeit geben, falls Cloudflare eine automatische + // Managed-Challenge zeigt (löst sich bei echtem Chromium meist selbst) + for (let i = 0; i < 5 && await isCloudflareChallenge(page); i++) { await new Promise(r => setTimeout(r, 4000)); } @@ -223,7 +237,10 @@ async function fetchStatsViaBrowserInner(userId, forceRefresh) { } if (!parsed) { - throw new Error('__NEXT_DATA__ nicht gefunden (weder Daten noch erkennbare Login-/Challenge-Seite) — Seitenlayout evtl. geändert'); + const debugTitle = await page.title().catch(() => '?'); + const debugUrl = page.url(); + await saveDebugScreenshot(userId, page).catch(() => {}); + throw new Error(`__NEXT_DATA__ nicht gefunden — Seite: "${debugTitle}" (${debugUrl}). Debug-Screenshot verfügbar über GET /debug-screenshot`); } const data = mapStatisticalData(parsed); @@ -325,15 +342,26 @@ router.get('/login-status', authenticate, (req, res) => { const userId = req.user.id; const cache = getCache(userId); const creds = getCredentials(userId); + const debugPath = `${PROFILE_BASE_DIR}/${userId}/last-failure.json`; + let debugInfo = null; + try { debugInfo = JSON.parse(fs.readFileSync(debugPath, 'utf8')); } catch {} res.json({ configured: !!creds, username: creds?.username || null, lastFetchedAt: cache.fetchedAt || null, lastError: cache.error || null, needsCode: pendingLoginByUser.has(userId), + debugScreenshot: debugInfo, }); }); +// Letzter Fehl-Screenshot (Diagnose) — nur der eigene +router.get('/debug-screenshot', authenticate, (req, res) => { + const path = `${PROFILE_BASE_DIR}/${req.user.id}/last-failure.png`; + if (!fs.existsSync(path)) return res.status(404).json({ error: 'Kein Debug-Screenshot vorhanden' }); + res.sendFile(path); +}); + router.post('/credentials', authenticate, async (req, res) => { const { username, password } = req.body; if (!username || !password) return res.status(400).json({ error: 'Benutzername und Passwort erforderlich' }); diff --git a/frontend/src/tools/statistik.jsx b/frontend/src/tools/statistik.jsx index 63f32e3..833546a 100644 --- a/frontend/src/tools/statistik.jsx +++ b/frontend/src/tools/statistik.jsx @@ -214,6 +214,9 @@ function MakerworldTab({ toast }) { const [savingCookie, setSavingCookie] = useState(false); const [showCookieFallback, setShowCookieFallback] = useState(false); + const [debugImgUrl, setDebugImgUrl] = useState(null); + const [loadingScreenshot, setLoadingScreenshot] = useState(false); + const load = () => { setLoading(true); api('/tools/makerworld/stats').then(setState).catch(()=>{}).finally(()=>setLoading(false)); @@ -268,6 +271,20 @@ function MakerworldTab({ toast }) { } catch(e) { toast?.(e.message,'error'); } }; + const viewDebugScreenshot = async () => { + setLoadingScreenshot(true); + try { + const token = localStorage.getItem('sk_token'); + const res = await fetch('/api/tools/makerworld/debug-screenshot', { + headers: token ? { 'Authorization': 'Bearer '+token } : {}, + }); + if (!res.ok) { toast?.('Kein Debug-Screenshot vorhanden','error'); return; } + const blob = await res.blob(); + setDebugImgUrl(URL.createObjectURL(blob)); + } catch(e) { toast?.(e.message,'error'); } + finally { setLoadingScreenshot(false); } + }; + const submitCode = async () => { if (!codeInput.trim()) return; setSubmittingCode(true); @@ -393,11 +410,20 @@ function MakerworldTab({ toast }) { )} - {state?.error && d && ( + {state?.error && (
-
- ⚠ Letzter Abruf fehlgeschlagen ({state.error}) — zeige zuletzt bekannten Stand. +
+ ⚠ {state.error}{d ? ' — zeige zuletzt bekannten Stand.' : ''}
+ + {debugImgUrl && ( +
+ Debug-Screenshot +
+ )}
)}