feat: MakerWorld komplett auf Bookmarklet-Ansatz umgestellt (kein Puppeteer/Login mehr)

This commit is contained in:
2026-07-11 09:28:15 +02:00
parent 9f1dd4e3b8
commit 142eda070c
4 changed files with 154 additions and 674 deletions

View File

@@ -199,114 +199,38 @@ function OrdersInPeriod({ fromDate, toDate }) {
// MakerWorld Creator-Center Statistik
function MakerworldTab({ toast }) {
const [state, setState] = useState(null); // { configured, data, error, fetchedAt, needsCode }
const [state, setState] = useState(null); // { configured, data, fetchedAt }
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [tokenInfo, setTokenInfo] = useState(null); // { token, origin }
const [regenerating, setRegenerating] = useState(false);
const [showModels, setShowModels] = useState(false);
const [loginStatus, setLoginStatus] = useState(null);
const [credForm, setCredForm] = useState({ username:'', password:'' });
const [savingCreds, setSavingCreds] = useState(false);
const [codeInput, setCodeInput] = useState('');
const [submittingCode, setSubmittingCode] = useState(false);
const [cookieInput, setCookieInput] = useState('');
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));
};
const loadLoginStatus = () => {
api('/tools/makerworld/login-status').then(setLoginStatus).catch(()=>{});
const loadToken = () => {
api('/tools/makerworld/ingest-token').then(setTokenInfo).catch(()=>{});
};
useEffect(() => { load(); loadLoginStatus(); }, []);
useEffect(() => { load(); loadToken(); }, []);
// Solange auf einen Code gewartet wird, alle paar Sekunden pollen
useEffect(() => {
if (!state?.needsCode) return;
const iv = setInterval(() => { load(); loadLoginStatus(); }, 5000);
return () => clearInterval(iv);
}, [state?.needsCode]);
const refresh = async () => {
setRefreshing(true);
const regenerateToken = async () => {
if (!window.confirm('Neues Bookmarklet erzeugen? Das alte funktioniert danach nicht mehr — du musst es neu ins Lesezeichen legen.')) return;
setRegenerating(true);
try {
const r = await api('/tools/makerworld/refresh', { method:'POST' });
setState(p => ({ ...p, ...r }));
loadLoginStatus();
if (r.needsCode) toast?.('MakerWorld verlangt einen E-Mail-Verifizierungscode');
else if (r.error) toast?.(r.error,'error');
else toast?.('Aktualisiert');
const r = await api('/tools/makerworld/ingest-token/regenerate', { method:'POST' });
setTokenInfo(r);
toast?.('Neues Bookmarklet erzeugt — bitte neu einrichten');
} catch(e) { toast?.(e.message,'error'); }
finally { setRefreshing(false); }
finally { setRegenerating(false); }
};
const saveCredentials = async () => {
if (!credForm.username.trim() || !credForm.password) return;
setSavingCreds(true);
try {
const r = await api('/tools/makerworld/credentials', { body:credForm });
setCredForm({ username:'', password:'' });
setState(p => ({ ...p, ...r, configured:true }));
loadLoginStatus();
if (r.needsCode) toast?.('Zugangsdaten gespeichert — E-Mail-Code erforderlich');
else if (r.error) toast?.('Gespeichert, aber Testabruf fehlgeschlagen: '+r.error,'error');
else toast?.('Zugangsdaten gespeichert, Abruf erfolgreich');
} catch(e) { toast?.(e.message,'error'); }
finally { setSavingCreds(false); }
};
const bookmarkletCode = tokenInfo ? `javascript:(function(){try{var el=document.getElementById('__NEXT_DATA__');if(!el){alert('MakerWorld: __NEXT_DATA__ nicht gefunden. Bist du auf der Statistikseite (/my/data-overview/model)?');return;}var nd=JSON.parse(el.textContent);var sd=nd.props&&nd.props.pageProps&&nd.props.pageProps.statisticalData;if(!sd||!sd.summary){alert('MakerWorld: Keine Statistikdaten auf dieser Seite gefunden.');return;}fetch('${tokenInfo.origin}/api/tools/makerworld/ingest',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:'${tokenInfo.token}',statisticalData:sd})}).then(function(r){return r.json();}).then(function(j){alert(j.ok?'DickenDock aktualisiert ✓':('Fehler: '+(j.error||'unbekannt')));}).catch(function(e){alert('Fehler: '+e.message);});}catch(e){alert('Fehler: '+e.message);}})();` : '';
const removeCredentials = async () => {
try {
await api('/tools/makerworld/credentials', { method:'DELETE' });
toast?.('Zugangsdaten entfernt');
setState({ configured:false, data:null, error:null, fetchedAt:null, needsCode:false });
loadLoginStatus();
} catch(e) { toast?.(e.message,'error'); }
};
const viewDebugScreenshot = async () => {
setLoadingScreenshot(true);
try {
const r = await api('/tools/makerworld/debug-screenshot');
if (!r.image) { toast?.('Kein Debug-Screenshot vorhanden','error'); return; }
setDebugImgUrl(r.image); // bereits eine data:-URI, CSP-konform (kein blob:)
} catch(e) { toast?.(e.message,'error'); }
finally { setLoadingScreenshot(false); }
};
const submitCode = async () => {
if (!codeInput.trim()) return;
setSubmittingCode(true);
try {
const r = await api('/tools/makerworld/verify-code', { body:{ code:codeInput.trim() } });
setState(p => ({ ...p, ...r }));
loadLoginStatus();
if (r.needsCode) toast?.(r.error || 'Code nicht akzeptiert, nochmal versuchen','error');
else if (r.error) toast?.(r.error,'error');
else { toast?.('Login erfolgreich'); setCodeInput(''); }
} catch(e) { toast?.(e.message,'error'); }
finally { setSubmittingCode(false); }
};
const saveCookie = async () => {
if (!cookieInput.trim()) return;
setSavingCookie(true);
try {
const r = await api('/tools/makerworld/cookie', { body:{ cookie: cookieInput } });
setCookieInput('');
setState(p => ({ ...p, ...r, configured:true }));
if (r.error) toast?.('Gespeichert, aber Testabruf fehlgeschlagen: '+r.error,'error');
else toast?.('Cookie übernommen, Abruf erfolgreich');
loadLoginStatus();
} catch(e) { toast?.(e.message,'error'); }
finally { setSavingCookie(false); }
const copyBookmarklet = async () => {
try { await navigator.clipboard.writeText(bookmarkletCode); toast?.('Bookmarklet-Code kopiert'); }
catch { toast?.('Kopieren nicht möglich — bitte manuell markieren','error'); }
};
if (loading) return <div style={{color:'rgba(255,255,255,0.3)',fontFamily:'monospace',fontSize:12}}>Lädt</div>;
@@ -318,108 +242,54 @@ function MakerworldTab({ toast }) {
const min = Math.round((Date.now()-ts)/60000);
if (min < 1) return 'gerade eben';
if (min < 60) return `vor ${min} Min.`;
return `vor ${Math.round(min/60)} Std.`;
if (min < 1440) return `vor ${Math.round(min/60)} Std.`;
return `vor ${Math.round(min/1440)} Tag(en)`;
};
return (
<div>
{/* Admin: Code-Eingabe hat höchste Priorität, wenn gerade angefordert */}
{state?.needsCode && (
<div style={{...S.card,borderColor:'rgba(255,230,109,0.4)',background:'rgba(255,230,109,0.08)'}}>
<div style={{...S.head,color:'#ffe66d'}}>📧 VERIFIZIERUNGSCODE ERFORDERLICH</div>
<div style={{...S.sub,marginBottom:8}}>
MakerWorld hat einen Code an deine E-Mail-Adresse geschickt. Bitte hier eintragen
(5 Minuten Zeit, danach muss der Login-Versuch neu gestartet werden).
</div>
<div style={{display:'flex',gap:8}}>
<input value={codeInput} onChange={e=>setCodeInput(e.target.value)}
onKeyDown={e=>e.key==='Enter'&&submitCode()}
placeholder="Code aus E-Mail" style={{...S.inp,flex:1}}/>
<button onClick={submitCode} disabled={submittingCode||!codeInput.trim()}
style={{...S.btn('#ffe66d'),opacity:(submittingCode||!codeInput.trim())?0.5:1,whiteSpace:'nowrap'}}>
{submittingCode?'…':'✓ Bestätigen'}
</button>
</div>
<div style={S.card}>
<div style={S.head}>EINRICHTUNG (EINMALIG)</div>
<div style={{...S.sub,marginBottom:10}}>
Kein Login, kein Passwort nötig nur ein Lesezeichen. Öffne die Seite{' '}
<code style={{color:'#4ecdc4'}}>makerworld.com/de/my/data-overview/model</code>{' '}
in deinem eigenen Browser (wo du eh eingeloggt bist), klicke dann auf das Lesezeichen
die aktuell auf der Seite geladenen Zahlen werden direkt an DickenDock geschickt.
</div>
)}
{/* Admin: Zugangsdaten */}
{!state?.needsCode && (
<div style={S.card}>
<div style={S.head}>MEINE MAKERWORLD-ZUGANGSDATEN</div>
{loginStatus && (
<div style={{...S.sub,marginBottom:10}}>
{loginStatus.configured
? `Eingerichtet als "${loginStatus.username}" · Letzter Abruf: ${fmtAgo(loginStatus.lastFetchedAt)}`
: 'Noch keine Zugangsdaten hinterlegt.'}
{loginStatus.lastError && <div style={{color:'#f87171',marginTop:4}}> {loginStatus.lastError}</div>}
</div>
)}
<div style={{...S.sub,marginBottom:8}}>
Wird verschlüsselt gespeichert und nur für deinen eigenen Abruf genutzt andere Nutzer sehen
weder deine Zugangsdaten noch deine Zahlen. Ein echter Browser mit dauerhaftem Profil hält die
Session am Laufen bei aktiver Nutzung wird ein erneuter Login kaum je nötig. Falls doch,
erscheint hier automatisch die Code-Eingabe.
</div>
<div style={{display:'flex',gap:8,flexWrap:'wrap',marginBottom:8}}>
<input value={credForm.username} onChange={e=>setCredForm(p=>({...p,username:e.target.value}))}
placeholder="E-Mail / Benutzername" style={{...S.inp,flex:1,minWidth:160}}/>
<input type="password" value={credForm.password} onChange={e=>setCredForm(p=>({...p,password:e.target.value}))}
onKeyDown={e=>e.key==='Enter'&&saveCredentials()}
placeholder="Passwort" style={{...S.inp,flex:1,minWidth:160}}/>
</div>
<div style={{display:'flex',gap:8,flexWrap:'wrap'}}>
<button onClick={saveCredentials} disabled={savingCreds||!credForm.username.trim()||!credForm.password}
style={{...S.btn('#4ade80'),opacity:(savingCreds||!credForm.username.trim()||!credForm.password)?0.5:1}}>
{savingCreds?'…':'💾 Speichern & testen'}
</button>
<button onClick={refresh} disabled={refreshing}
style={{...S.btn('#4ecdc4'),opacity:refreshing?0.5:1}}>
{refreshing?'…':'🔄 Neu abrufen'}
</button>
<button onClick={()=>setShowCookieFallback(p=>!p)} style={{...S.btn('#888888')}}>
{showCookieFallback ? '▲ Cookie-Fallback ausblenden' : '⚙ Cookie-Fallback (manuell)'}
</button>
{loginStatus?.configured && (
<button onClick={removeCredentials} style={{...S.btn('#f87171')}}>
🗑 Zugangsdaten entfernen
</button>
)}
</div>
{showCookieFallback && (
<div style={{marginTop:14,paddingTop:14,borderTop:'1px solid rgba(255,255,255,0.07)'}}>
<div style={{...S.sub,marginBottom:8}}>
Nur falls der automatische Login nicht funktioniert: kompletten Cookie-Header aus einem
eingeloggten Browser-Tab kopieren (Entwicklertools Netzwerk Request an
/de/my/data-overview/model "Cookie"-Header) und hier einfügen.
</div>
<textarea value={cookieInput} onChange={e=>setCookieInput(e.target.value)}
placeholder="lang=de; cf_clearance=...; token=...; ..."
rows={3} style={{...S.inp,resize:'vertical',fontSize:10,marginBottom:8}}/>
<button onClick={saveCookie} disabled={savingCookie||!cookieInput.trim()}
style={{...S.btn('#4ecdc4'),opacity:(savingCookie||!cookieInput.trim())?0.5:1}}>
{savingCookie?'…':'💾 Cookie übernehmen & testen'}
</button>
</div>
)}
<div style={{...S.sub,marginBottom:6,fontWeight:700,color:'rgba(255,255,255,0.6)'}}>Am PC:</div>
<div style={{...S.sub,marginBottom:10}}>
Lesezeichenleiste anzeigen, dann diesen Link dorthin ziehen:
</div>
<div style={{marginBottom:14}}>
<a href={bookmarkletCode} onClick={e=>e.preventDefault()} draggable="true"
style={{...S.btn('#4ecdc4'),display:'inline-block',cursor:'grab',textDecoration:'none'}}>
📌 DickenDock Sync
</a>
</div>
)}
{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,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'}
<div style={{...S.sub,marginBottom:6,fontWeight:700,color:'rgba(255,255,255,0.6)'}}>Am Handy:</div>
<div style={{...S.sub,marginBottom:10}}>
Erst irgendeine Seite als Lesezeichen speichern, dann dieses Lesezeichen bearbeiten und
als Namen z.B. "DickenDock Sync" eintragen als URL den kopierten Code einfügen (ersetzt
die bisherige URL komplett):
</div>
<button onClick={copyBookmarklet} style={{...S.btn('#4ecdc4')}}>
📋 Bookmarklet-Code kopieren
</button>
<div style={{display:'flex',gap:8,marginTop:14,paddingTop:14,borderTop:'1px solid rgba(255,255,255,0.07)'}}>
<button onClick={regenerateToken} disabled={regenerating} style={{...S.btn('#f87171'),opacity:regenerating?0.5:1}}>
{regenerating?'…':'🔄 Bookmarklet zurücksetzen'}
</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>
{!state?.configured && (
<div style={S.card}>
<div style={{color:'rgba(255,255,255,0.4)',fontFamily:'monospace',fontSize:12}}>
Noch keine Daten empfangen Lesezeichen einrichten und einmal anklicken.
</div>
</div>
)}