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) {
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' });

View File

@@ -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 }) {
</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={{color:'#f87171',fontFamily:'monospace',fontSize:11}}>
Letzter Abruf fehlgeschlagen ({state.error}) zeige zuletzt bekannten Stand.
<div style={{color:'#f87171',fontFamily:'monospace',fontSize:11,marginBottom:8}}>
{state.error}{d ? ' — zeige zuletzt bekannten Stand.' : ''}
</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>
)}