feat: MakerWorld Debug-Screenshot + bessere Fehlerdiagnose bei fehlgeschlagenem Abruf
This commit is contained in:
@@ -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' });
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user