feat: MakerWorld Debug-Screenshot + bessere Fehlerdiagnose bei fehlgeschlagenem Abruf

This commit is contained in:
2026-07-11 02:28:48 +02:00
parent 59cced35be
commit ae13d1b239
2 changed files with 62 additions and 8 deletions

View File

@@ -130,7 +130,16 @@ async function getNextDataFromPage(page) {
} }
async function isCloudflareChallenge(page) { async function isCloudflareChallenge(page) {
const title = await page.title().catch(() => ''); 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) { function isLoginUrl(url) {
return /bambulab\.com\/[^/]*\/?sign-in|makerworld\.com\/[a-z]{2}\/sign-in/i.test(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"]']); const pwField = await findFirstSelector(page, ['input[type="password"]']);
if (!emailField || !pwField) { 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 }); await page.type(emailField, creds.username, { delay: 25 });
@@ -192,6 +202,7 @@ async function attemptLogin(userId, browser, page) {
throw err; 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'); 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 browser;
let page;
try { try {
browser = await launchBrowser(userId); browser = await launchBrowser(userId);
const page = await browser.newPage(); page = await browser.newPage();
await page.setUserAgent(USER_AGENT); await page.setUserAgent(USER_AGENT);
await page.setViewport({ width: 1366, height: 900 }); await page.setViewport({ width: 1366, height: 900 });
await page.goto(DATA_URL, { waitUntil: 'networkidle2', timeout: 45000 }); 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)); await new Promise(r => setTimeout(r, 4000));
} }
@@ -223,7 +237,10 @@ async function fetchStatsViaBrowserInner(userId, forceRefresh) {
} }
if (!parsed) { 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); const data = mapStatisticalData(parsed);
@@ -325,15 +342,26 @@ router.get('/login-status', authenticate, (req, res) => {
const userId = req.user.id; const userId = req.user.id;
const cache = getCache(userId); const cache = getCache(userId);
const creds = getCredentials(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({ res.json({
configured: !!creds, configured: !!creds,
username: creds?.username || null, username: creds?.username || null,
lastFetchedAt: cache.fetchedAt || null, lastFetchedAt: cache.fetchedAt || null,
lastError: cache.error || null, lastError: cache.error || null,
needsCode: pendingLoginByUser.has(userId), 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) => { router.post('/credentials', authenticate, async (req, res) => {
const { username, password } = req.body; const { username, password } = req.body;
if (!username || !password) return res.status(400).json({ error: 'Benutzername und Passwort erforderlich' }); if (!username || !password) return res.status(400).json({ error: 'Benutzername und Passwort erforderlich' });

View File

@@ -214,6 +214,9 @@ function MakerworldTab({ toast }) {
const [savingCookie, setSavingCookie] = useState(false); const [savingCookie, setSavingCookie] = useState(false);
const [showCookieFallback, setShowCookieFallback] = useState(false); const [showCookieFallback, setShowCookieFallback] = useState(false);
const [debugImgUrl, setDebugImgUrl] = useState(null);
const [loadingScreenshot, setLoadingScreenshot] = useState(false);
const load = () => { const load = () => {
setLoading(true); setLoading(true);
api('/tools/makerworld/stats').then(setState).catch(()=>{}).finally(()=>setLoading(false)); api('/tools/makerworld/stats').then(setState).catch(()=>{}).finally(()=>setLoading(false));
@@ -268,6 +271,20 @@ function MakerworldTab({ toast }) {
} catch(e) { toast?.(e.message,'error'); } } 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 () => { const submitCode = async () => {
if (!codeInput.trim()) return; if (!codeInput.trim()) return;
setSubmittingCode(true); setSubmittingCode(true);
@@ -393,11 +410,20 @@ function MakerworldTab({ toast }) {
</div> </div>
)} )}
{state?.error && d && ( {state?.error && (
<div style={{...S.card,borderColor:'rgba(248,113,113,0.3)',background:'rgba(248,113,113,0.06)'}}> <div style={{...S.card,borderColor:'rgba(248,113,113,0.3)',background:'rgba(248,113,113,0.06)'}}>
<div style={{color:'#f87171',fontFamily:'monospace',fontSize:11}}> <div style={{color:'#f87171',fontFamily:'monospace',fontSize:11,marginBottom:8}}>
Letzter Abruf fehlgeschlagen ({state.error}) zeige zuletzt bekannten Stand. {state.error}{d ? ' — zeige zuletzt bekannten Stand.' : ''}
</div> </div>
<button onClick={viewDebugScreenshot} disabled={loadingScreenshot}
style={{...S.btn('#888888'),opacity:loadingScreenshot?0.5:1}}>
{loadingScreenshot?'…':'🖼 Debug-Screenshot ansehen'}
</button>
{debugImgUrl && (
<div style={{marginTop:10}}>
<img src={debugImgUrl} alt="Debug-Screenshot" style={{width:'100%',borderRadius:8,border:'1px solid rgba(255,255,255,0.1)'}}/>
</div>
)}
</div> </div>
)} )}