Merge remote changes and update project files
This commit is contained in:
@@ -18,9 +18,13 @@ import { OnboardingGate } from "./components/onboarding/OnboardingGate";
|
||||
import { SidebarHeader } from "./components/sidebar/SidebarHeader";
|
||||
import { useHashRoute } from "./lib/routing";
|
||||
import { account, databases, databaseId, usersCollectionId } from "./lib/appwrite";
|
||||
import { fetchManagedAccounts } from "./services/accountsService";
|
||||
import { useScan } from "./context/ScanContext";
|
||||
import ScanningLoader from "./components/ui/ScanningLoader";
|
||||
|
||||
export default function App() {
|
||||
const { route, navigate } = useHashRoute();
|
||||
const { scanning, scanProgress } = useScan();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [status, setStatus] = useState({ loading: true, authed: false, error: "" });
|
||||
@@ -32,8 +36,15 @@ export default function App() {
|
||||
const [checkingUserDoc, setCheckingUserDoc] = useState(false);
|
||||
const [onboardingLoading, setOnboardingLoading] = useState(false);
|
||||
const [onboardingError, setOnboardingError] = useState("");
|
||||
const [userExtensionLoad, setUserExtensionLoad] = useState(null); // null = nicht geprüft, true/false = geprüft
|
||||
const [hasAccounts, setHasAccounts] = useState(false); // true wenn User Accounts hat
|
||||
|
||||
const showGate = !hasUserDoc && !checkingUserDoc && authUser !== null;
|
||||
// Gate soll angezeigt werden, wenn:
|
||||
// 1. User-Dokument nicht existiert ODER
|
||||
// 2. User-Dokument existiert, aber user_extension_load = false UND keine Accounts vorhanden
|
||||
// Gate wird versteckt, wenn:
|
||||
// - User-Dokument existiert UND (user_extension_load = true ODER Accounts vorhanden)
|
||||
const showGate = (!hasUserDoc || (hasUserDoc && userExtensionLoad === false && !hasAccounts)) && !checkingUserDoc && authUser !== null;
|
||||
|
||||
async function checkUserDocument(userId) {
|
||||
if (!databases || !databaseId || !usersCollectionId) {
|
||||
@@ -66,13 +77,53 @@ export default function App() {
|
||||
|
||||
// Prüfe, ob User-Dokument existiert
|
||||
const userDocExists = await checkUserDocument(user.$id);
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:68',message:'refreshAuth: userDocExists check',data:{userId:user.$id,userDocExists},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H2'})}).catch(()=>{});
|
||||
// #endregion
|
||||
setHasUserDoc(userDocExists);
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:71',message:'refreshAuth: hasUserDoc set',data:{hasUserDoc:userDocExists,showGateWillBe:!userDocExists && !checkingUserDoc && user !== null},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H2'})}).catch(()=>{});
|
||||
// #endregion
|
||||
|
||||
// Prüfe user_extension_load und Accounts, wenn User-Dokument existiert
|
||||
if (userDocExists && databases && databaseId && usersCollectionId) {
|
||||
try {
|
||||
const userDoc = await databases.getDocument(databaseId, usersCollectionId, user.$id);
|
||||
const extensionLoad = userDoc?.user_extension_load === true;
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:78',message:'refreshAuth: userDoc retrieved for extension check',data:{userExtensionLoad:userDoc?.user_extension_load,userExtensionLoadType:typeof userDoc?.user_extension_load,userExtensionLoadStrictFalse:userDoc?.user_extension_load === false,extensionLoad},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H2'})}).catch(()=>{});
|
||||
// #endregion
|
||||
setUserExtensionLoad(extensionLoad);
|
||||
|
||||
// Prüfe auch Accounts
|
||||
try {
|
||||
const existingAccounts = await fetchManagedAccounts(user.$id);
|
||||
const hasAccountsValue = Array.isArray(existingAccounts) && existingAccounts.length > 0;
|
||||
setHasAccounts(hasAccountsValue);
|
||||
} catch (accountsErr) {
|
||||
setHasAccounts(false);
|
||||
}
|
||||
} catch (docErr) {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:82',message:'refreshAuth: error getting userDoc for extension check',data:{error:docErr.message},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H2'})}).catch(()=>{});
|
||||
// #endregion
|
||||
// Bei Fehler: Setze auf null (nicht geprüft)
|
||||
setUserExtensionLoad(null);
|
||||
setHasAccounts(false);
|
||||
}
|
||||
} else {
|
||||
// Kein User-Dokument → user_extension_load ist nicht relevant
|
||||
setUserExtensionLoad(null);
|
||||
setHasAccounts(false);
|
||||
}
|
||||
|
||||
await handoffJwtToExtension();
|
||||
} catch (e) {
|
||||
setStatus({ loading: false, authed: false, error: "" });
|
||||
setAuthUser(null);
|
||||
setHasUserDoc(false);
|
||||
setUserExtensionLoad(null);
|
||||
setHasAccounts(false);
|
||||
} finally {
|
||||
setCheckingUserDoc(false);
|
||||
}
|
||||
@@ -94,18 +145,50 @@ export default function App() {
|
||||
const userDocExists = await checkUserDocument(user.$id);
|
||||
setHasUserDoc(userDocExists);
|
||||
|
||||
// Prüfe user_extension_load und Accounts, wenn User-Dokument existiert
|
||||
if (userDocExists && databases && databaseId && usersCollectionId) {
|
||||
try {
|
||||
const userDoc = await databases.getDocument(databaseId, usersCollectionId, user.$id);
|
||||
const extensionLoad = userDoc?.user_extension_load === true;
|
||||
setUserExtensionLoad(extensionLoad);
|
||||
|
||||
// Prüfe auch Accounts
|
||||
try {
|
||||
const existingAccounts = await fetchManagedAccounts(user.$id);
|
||||
const hasAccountsValue = Array.isArray(existingAccounts) && existingAccounts.length > 0;
|
||||
setHasAccounts(hasAccountsValue);
|
||||
} catch (accountsErr) {
|
||||
setHasAccounts(false);
|
||||
}
|
||||
} catch (docErr) {
|
||||
setUserExtensionLoad(null);
|
||||
setHasAccounts(false);
|
||||
}
|
||||
} else {
|
||||
setUserExtensionLoad(null);
|
||||
setHasAccounts(false);
|
||||
}
|
||||
|
||||
await handoffJwtToExtension();
|
||||
} catch (e) {
|
||||
setStatus({ loading: false, authed: false, error: "Login fehlgeschlagen" });
|
||||
setAuthUser(null);
|
||||
setHasUserDoc(false);
|
||||
setUserExtensionLoad(null);
|
||||
setHasAccounts(false);
|
||||
} finally {
|
||||
setCheckingUserDoc(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleOnboardingStart() {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:162',message:'handleOnboardingStart: entry',data:{hasAuthUser:!!authUser,hasDatabases:!!databases,hasDatabaseId:!!databaseId,hasUsersCollectionId:!!usersCollectionId},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
if (!authUser || !databases || !databaseId || !usersCollectionId) {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:165',message:'handleOnboardingStart: configuration incomplete',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
setOnboardingError("Fehler: Konfiguration unvollständig");
|
||||
return;
|
||||
}
|
||||
@@ -114,38 +197,147 @@ export default function App() {
|
||||
setOnboardingError("");
|
||||
|
||||
try {
|
||||
await databases.createDocument(
|
||||
databaseId,
|
||||
usersCollectionId,
|
||||
authUser.$id, // Document-ID = Auth-User-ID
|
||||
{
|
||||
user_name: authUser.name || "User"
|
||||
}
|
||||
);
|
||||
|
||||
// Erfolg: User-Dokument erstellt
|
||||
setHasUserDoc(true);
|
||||
setOnboardingError("");
|
||||
} catch (e) {
|
||||
// 409 Conflict bedeutet, dass das Dokument bereits existiert
|
||||
// Das ist ok, da wir idempotent sein wollen
|
||||
if (e.code === 409 || e.type === 'document_already_exists') {
|
||||
setHasUserDoc(true);
|
||||
setOnboardingError("");
|
||||
} else if (e.code === 401 || e.type === 'general_unauthorized_scope') {
|
||||
// 401 Unauthorized: Permissions nicht richtig gesetzt
|
||||
setOnboardingError(
|
||||
"Berechtigung verweigert. Bitte prüfe in Appwrite, ob die users Collection die richtigen Permissions hat. " +
|
||||
"Siehe setup/USERS_COLLECTION_SETUP.md für Details."
|
||||
);
|
||||
} else {
|
||||
// Andere Fehler anzeigen
|
||||
setOnboardingError(e.message || "Fehler beim Erstellen des Profils. Bitte versuche es erneut.");
|
||||
// Prüfe zuerst, ob User-Dokument bereits existiert
|
||||
let userDocExists = false;
|
||||
try {
|
||||
await databases.getDocument(databaseId, usersCollectionId, authUser.$id);
|
||||
userDocExists = true;
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:178',message:'handleOnboardingStart: userDoc exists',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
} catch (e) {
|
||||
// Dokument existiert nicht - wird erstellt
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:182',message:'handleOnboardingStart: userDoc does not exist, will create',data:{error:e.message},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
}
|
||||
|
||||
if (!userDocExists) {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:187',message:'handleOnboardingStart: creating userDoc',data:{userId:authUser.$id},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
await databases.createDocument(
|
||||
databaseId,
|
||||
usersCollectionId,
|
||||
authUser.$id, // Document-ID = Auth-User-ID
|
||||
{
|
||||
user_name: authUser.name || "User",
|
||||
user_extension_load: false
|
||||
}
|
||||
);
|
||||
// Erfolg: User-Dokument erstellt
|
||||
setHasUserDoc(true);
|
||||
// user_extension_load ist false beim Erstellen (siehe payload)
|
||||
setUserExtensionLoad(false);
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:202',message:'handleOnboardingStart: userDoc created, setting hasUserDoc=true',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
|
||||
// Prüfe, ob User bereits Accounts hat (nach dem Erstellen des User-Dokuments)
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:207',message:'handleOnboardingStart: checking for accounts after userDoc creation',data:{userId:authUser.$id},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
try {
|
||||
const existingAccounts = await fetchManagedAccounts(authUser.$id);
|
||||
const hasAccountsValue = Array.isArray(existingAccounts) && existingAccounts.length > 0;
|
||||
setHasAccounts(hasAccountsValue);
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:212',message:'handleOnboardingStart: accounts check result after creation',data:{hasAccounts:hasAccountsValue,accountsCount:existingAccounts?.length || 0},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
} catch (accountsErr) {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:216',message:'handleOnboardingStart: error checking accounts after creation',data:{error:accountsErr.message},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
setHasAccounts(false);
|
||||
}
|
||||
} else {
|
||||
// Dokument existiert bereits - prüfe user_extension_load
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:206',message:'handleOnboardingStart: userDoc exists, checking extension load',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
const userDoc = await databases.getDocument(databaseId, usersCollectionId, authUser.$id);
|
||||
const extensionLoad = userDoc?.user_extension_load === true;
|
||||
setHasUserDoc(true);
|
||||
setUserExtensionLoad(extensionLoad);
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:212',message:'handleOnboardingStart: userDoc retrieved, setting hasUserDoc=true',data:{extensionLoad},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
}
|
||||
|
||||
// Prüfe, ob User Accounts hat (nach dem Erstellen/Prüfen des User-Dokuments)
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:217',message:'handleOnboardingStart: checking for accounts',data:{userId:authUser.$id},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
try {
|
||||
const existingAccounts = await fetchManagedAccounts(authUser.$id);
|
||||
const hasAccountsValue = Array.isArray(existingAccounts) && existingAccounts.length > 0;
|
||||
setHasAccounts(hasAccountsValue);
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:222',message:'handleOnboardingStart: accounts check result',data:{hasAccounts:hasAccountsValue,accountsCount:existingAccounts?.length || 0},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
} catch (accountsErr) {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:226',message:'handleOnboardingStart: error checking accounts',data:{error:accountsErr.message},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
setHasAccounts(false);
|
||||
}
|
||||
|
||||
setOnboardingError("");
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:232',message:'handleOnboardingStart: success, showGate will be',data:{hasUserDoc:true,userExtensionLoad,hasAccounts,showGateWillBe:(hasUserDoc && userExtensionLoad === false && !hasAccounts)},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
} catch (e) {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:220',message:'handleOnboardingStart: error caught',data:{errorCode:e.code,errorType:e.type,errorMessage:e.message},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
// 409 Conflict bedeutet, dass das Dokument bereits existiert
|
||||
// Das ist ok, da wir idempotent sein wollen
|
||||
if (e.code === 409 || e.type === 'document_already_exists') {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:225',message:'handleOnboardingStart: document already exists (409)',data:{},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
setHasUserDoc(true);
|
||||
// Prüfe user_extension_load für existierendes Dokument
|
||||
if (authUser && databases && databaseId && usersCollectionId) {
|
||||
try {
|
||||
const userDoc = await databases.getDocument(databaseId, usersCollectionId, authUser.$id);
|
||||
const extensionLoad = userDoc?.user_extension_load === true;
|
||||
setUserExtensionLoad(extensionLoad);
|
||||
|
||||
// Prüfe auch Accounts
|
||||
try {
|
||||
const existingAccounts = await fetchManagedAccounts(authUser.$id);
|
||||
const hasAccountsValue = Array.isArray(existingAccounts) && existingAccounts.length > 0;
|
||||
setHasAccounts(hasAccountsValue);
|
||||
} catch (accountsErr) {
|
||||
setHasAccounts(false);
|
||||
}
|
||||
} catch (docErr) {
|
||||
setUserExtensionLoad(null);
|
||||
setHasAccounts(false);
|
||||
}
|
||||
} else {
|
||||
setUserExtensionLoad(null);
|
||||
setHasAccounts(false);
|
||||
}
|
||||
setOnboardingError("");
|
||||
} else if (e.code === 401 || e.type === 'general_unauthorized_scope') {
|
||||
// 401 Unauthorized: Permissions nicht richtig gesetzt
|
||||
setOnboardingError(
|
||||
"Berechtigung verweigert. Bitte prüfe in Appwrite, ob die users Collection die richtigen Permissions hat. " +
|
||||
"Siehe setup/USERS_COLLECTION_SETUP.md für Details."
|
||||
);
|
||||
} else {
|
||||
// Andere Fehler anzeigen
|
||||
setOnboardingError(e.message || "Fehler beim Erstellen des Profils. Bitte versuche es erneut.");
|
||||
}
|
||||
} finally {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'App.jsx:245',message:'handleOnboardingStart: finally, setting loading=false',data:{hasUserDoc,userExtensionLoad,showGateWillBe:(!hasUserDoc || (hasUserDoc && userExtensionLoad === false)) && !checkingUserDoc && authUser !== null},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
setOnboardingLoading(false);
|
||||
}
|
||||
} finally {
|
||||
setOnboardingLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
setStatus((s) => ({ ...s, loading: true, error: "" }));
|
||||
@@ -153,9 +345,11 @@ export default function App() {
|
||||
await account.deleteSession("current");
|
||||
} catch {}
|
||||
setStatus({ loading: false, authed: false, error: "" });
|
||||
setAuthUser(null);
|
||||
setHasUserDoc(false);
|
||||
setOnboardingError("");
|
||||
setAuthUser(null);
|
||||
setHasUserDoc(false);
|
||||
setUserExtensionLoad(null);
|
||||
setHasAccounts(false);
|
||||
setOnboardingError("");
|
||||
|
||||
// Extension informieren: Token weg
|
||||
sendToExtension({ type: "AUTH_CLEARED" });
|
||||
@@ -202,6 +396,7 @@ export default function App() {
|
||||
icon: (
|
||||
<IconShoppingBag className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />
|
||||
),
|
||||
disabled: scanning,
|
||||
onClick: (e) => {
|
||||
e.preventDefault();
|
||||
navigate("/accounts");
|
||||
@@ -288,6 +483,7 @@ export default function App() {
|
||||
onStart={handleOnboardingStart}
|
||||
loading={onboardingLoading}
|
||||
error={onboardingError}
|
||||
initialPhase={userExtensionLoad === false ? "extension" : "welcome"}
|
||||
/>
|
||||
)}
|
||||
<div style={{ display: showGate ? "none" : "block" }}>
|
||||
@@ -297,26 +493,31 @@ export default function App() {
|
||||
"flex w-full flex-1 flex-col overflow-hidden rounded-md border border-neutral-200 bg-gray-100 md:flex-row dark:border-neutral-700 dark:bg-neutral-800",
|
||||
"h-screen relative z-10"
|
||||
)}>
|
||||
<Sidebar open={sidebarOpen} setOpen={setSidebarOpen} animate={true}>
|
||||
<SidebarBody className="justify-between gap-10">
|
||||
<div className="flex flex-1 flex-col overflow-x-hidden overflow-y-auto">
|
||||
<SidebarHeader />
|
||||
<div className="mt-8 flex flex-col gap-2">
|
||||
{links.map((link, idx) => (
|
||||
<SidebarLink key={idx} link={link} />
|
||||
))}
|
||||
{!scanning && (
|
||||
<Sidebar open={sidebarOpen} setOpen={setSidebarOpen} animate={true}>
|
||||
<SidebarBody className="justify-between gap-10">
|
||||
<div className="flex flex-1 flex-col overflow-x-hidden overflow-y-auto">
|
||||
<SidebarHeader />
|
||||
<div className="mt-8 flex flex-col gap-2">
|
||||
{links.map((link, idx) => (
|
||||
<SidebarLink key={idx} link={link} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<LogoutButton onClick={(e) => {
|
||||
e.preventDefault();
|
||||
logout();
|
||||
}} />
|
||||
</div>
|
||||
</SidebarBody>
|
||||
</Sidebar>
|
||||
<div>
|
||||
<LogoutButton onClick={(e) => {
|
||||
e.preventDefault();
|
||||
logout();
|
||||
}} />
|
||||
</div>
|
||||
</SidebarBody>
|
||||
</Sidebar>
|
||||
)}
|
||||
{renderContent()}
|
||||
</div>
|
||||
{scanning && (
|
||||
<ScanningLoader percent={scanProgress?.percent ?? 0} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { DataTable } from "../ui/DataTable";
|
||||
import { Pagination } from "../ui/Pagination";
|
||||
import { Filters } from "../ui/Filters";
|
||||
import { cn } from "../../../lib/utils";
|
||||
import { getProductsPage, getProductPreview } from "../../../services/dashboardService";
|
||||
import { scanProductsForAccount } from "../../../services/productsService";
|
||||
import { getScanProgress } from "../../../services/ebayParserService";
|
||||
import { useScan } from "../../../context/ScanContext";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
|
||||
const SCAN_POLL_MS = 300;
|
||||
|
||||
export const ProductsSection = ({ onJumpToSection, activeAccountId }) => {
|
||||
const [statusFilter, setStatusFilter] = useState("all");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
@@ -18,8 +22,9 @@ export const ProductsSection = ({ onJumpToSection, activeAccountId }) => {
|
||||
const [total, setTotal] = useState(0);
|
||||
const [selectedProduct, setSelectedProduct] = useState(null);
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [scanToast, setScanToast] = useState({ show: false, message: "", type: "success" });
|
||||
const pollRef = useRef(null);
|
||||
const { scanning, startScan, endScan, updateProgress } = useScan();
|
||||
|
||||
// Lade Products wenn activeAccountId oder Filter sich ändern
|
||||
useEffect(() => {
|
||||
@@ -121,17 +126,39 @@ export const ProductsSection = ({ onJumpToSection, activeAccountId }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
setScanning(true);
|
||||
setError(null);
|
||||
startScan();
|
||||
|
||||
const stopPolling = () => {
|
||||
if (pollRef.current) {
|
||||
clearInterval(pollRef.current);
|
||||
pollRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
pollRef.current = setInterval(async () => {
|
||||
try {
|
||||
const p = await getScanProgress();
|
||||
if (p && (p.percent != null || p.phase || p.complete)) {
|
||||
updateProgress({
|
||||
percent: p.percent ?? 0,
|
||||
phase: p.phase ?? "idle",
|
||||
total: p.total ?? 0,
|
||||
current: p.current ?? 0,
|
||||
complete: !!p.complete,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, SCAN_POLL_MS);
|
||||
|
||||
try {
|
||||
// Führe Scan aus
|
||||
const result = await scanProductsForAccount(activeAccountId);
|
||||
stopPolling();
|
||||
|
||||
// Refresh Products-Liste
|
||||
await loadProducts();
|
||||
|
||||
// Zeige Erfolgs-Toast
|
||||
const updated = result.updated ?? 0;
|
||||
setScanToast({
|
||||
show: true,
|
||||
@@ -142,23 +169,45 @@ export const ProductsSection = ({ onJumpToSection, activeAccountId }) => {
|
||||
setScanToast({ show: false, message: "", type: "success" });
|
||||
}, 3000);
|
||||
} catch (e) {
|
||||
stopPolling();
|
||||
console.error("Fehler beim Scannen der Produkte:", e);
|
||||
|
||||
// Logge meta für Dev-Debugging
|
||||
|
||||
const meta = e.meta || {};
|
||||
if (Object.keys(meta).length > 0) {
|
||||
console.log("[scan meta]", meta);
|
||||
}
|
||||
|
||||
// Prüfe auf no_items_found oder empty_items
|
||||
|
||||
const errorMsg = e.message || "Fehler beim Scannen der Produkte";
|
||||
const isNoItems = errorMsg.includes("no_items_found") || errorMsg.includes("empty_items");
|
||||
|
||||
// Zeige Toast mit spezifischer Meldung für 0 Items
|
||||
const toastMessage = isNoItems
|
||||
? "0 Produkte gefunden. Bitte pruefe, ob die URL auf den Shop/Artikel-Bereich des Sellers zeigt."
|
||||
: errorMsg;
|
||||
|
||||
const isParsingFailed = errorMsg.includes("Parsing failed");
|
||||
|
||||
let toastMessage = errorMsg;
|
||||
if (isNoItems) {
|
||||
toastMessage = "0 Produkte gefunden. Bitte prüfe, ob die URL auf den Shop/Artikel-Bereich des Sellers zeigt.";
|
||||
} else if (isParsingFailed) {
|
||||
toastMessage = "Extension konnte keine Produkte finden. ";
|
||||
if (meta.pageType && meta.pageType !== "unknown") {
|
||||
toastMessage += `Seitentyp: ${meta.pageType}. `;
|
||||
}
|
||||
if (meta.reason) {
|
||||
toastMessage += `Grund: ${meta.reason}. `;
|
||||
}
|
||||
if (meta.finalUrl) {
|
||||
toastMessage += `URL: ${meta.finalUrl}`;
|
||||
}
|
||||
if (!meta.pageType && !meta.reason && !meta.finalUrl) {
|
||||
toastMessage += "Bitte stelle sicher, dass die Account-URL auf eine Seite mit Produkt-Listings zeigt (z.B. Storefront oder Seller-Listings).";
|
||||
}
|
||||
} else if (e.meta) {
|
||||
const m = e.meta;
|
||||
if (m.pageType && m.pageType !== "unknown") {
|
||||
toastMessage += ` (Seitentyp: ${m.pageType})`;
|
||||
}
|
||||
if (m.reason) {
|
||||
toastMessage += ` (${m.reason})`;
|
||||
}
|
||||
}
|
||||
|
||||
setScanToast({
|
||||
show: true,
|
||||
message: toastMessage,
|
||||
@@ -166,9 +215,13 @@ export const ProductsSection = ({ onJumpToSection, activeAccountId }) => {
|
||||
});
|
||||
setTimeout(() => {
|
||||
setScanToast({ show: false, message: "", type: "success" });
|
||||
}, 3000);
|
||||
}, 5000);
|
||||
} finally {
|
||||
setScanning(false);
|
||||
if (pollRef.current) {
|
||||
clearInterval(pollRef.current);
|
||||
pollRef.current = null;
|
||||
}
|
||||
endScan();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -131,11 +131,24 @@ export const SidebarLink = ({
|
||||
...props
|
||||
}) => {
|
||||
const { open, animate } = useSidebar();
|
||||
const disabled = !!link.disabled;
|
||||
const handleClick = (e) => {
|
||||
if (disabled) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
link.onClick?.(e);
|
||||
};
|
||||
return (
|
||||
<a
|
||||
href={link.href}
|
||||
onClick={link.onClick}
|
||||
className={cn("flex items-center justify-start gap-2 group/sidebar py-2", className)}
|
||||
onClick={handleClick}
|
||||
className={cn(
|
||||
"flex items-center justify-start gap-2 group/sidebar py-2",
|
||||
disabled && "pointer-events-none opacity-50 cursor-not-allowed",
|
||||
className
|
||||
)}
|
||||
aria-disabled={disabled}
|
||||
{...props}>
|
||||
{link.icon}
|
||||
<motion.span
|
||||
|
||||
113
Server/src/components/ui/ScanningLoader.jsx
Normal file
113
Server/src/components/ui/ScanningLoader.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
|
||||
.loader-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
font-family: "Inter", sans-serif;
|
||||
font-size: 1.2em;
|
||||
font-weight: 300;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.loader {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
animation: loader-rotate 2s linear infinite;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@keyframes loader-rotate {
|
||||
0% {
|
||||
transform: rotate(90deg);
|
||||
box-shadow:
|
||||
0 10px 20px 0 #fff inset,
|
||||
0 20px 30px 0 #ad5fff inset,
|
||||
0 60px 60px 0 #471eec inset;
|
||||
}
|
||||
50% {
|
||||
transform: rotate(270deg);
|
||||
box-shadow:
|
||||
0 10px 20px 0 #fff inset,
|
||||
0 20px 10px 0 #d60a47 inset,
|
||||
0 40px 60px 0 #311e80 inset;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(450deg);
|
||||
box-shadow:
|
||||
0 10px 20px 0 #fff inset,
|
||||
0 20px 30px 0 #ad5fff inset,
|
||||
0 60px 60px 0 #471eec inset;
|
||||
}
|
||||
}
|
||||
|
||||
.loader-letter {
|
||||
display: inline-block;
|
||||
opacity: 0.4;
|
||||
transform: translateY(0);
|
||||
animation: loader-letter-anim 2s infinite;
|
||||
z-index: 1;
|
||||
border-radius: 50ch;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.loader-letter:nth-child(1) { animation-delay: 0s; }
|
||||
.loader-letter:nth-child(2) { animation-delay: 0.1s; }
|
||||
.loader-letter:nth-child(3) { animation-delay: 0.2s; }
|
||||
.loader-letter:nth-child(4) { animation-delay: 0.3s; }
|
||||
.loader-letter:nth-child(5) { animation-delay: 0.4s; }
|
||||
.loader-letter:nth-child(6) { animation-delay: 0.5s; }
|
||||
.loader-letter:nth-child(7) { animation-delay: 0.6s; }
|
||||
.loader-letter:nth-child(8) { animation-delay: 0.7s; }
|
||||
.loader-letter:nth-child(9) { animation-delay: 0.8s; }
|
||||
.loader-letter:nth-child(10) { animation-delay: 0.9s; }
|
||||
|
||||
@keyframes loader-letter-anim {
|
||||
0%, 100% { opacity: 0.4; transform: translateY(0); }
|
||||
20% { opacity: 1; transform: scale(1.15); }
|
||||
40% { opacity: 0.7; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.loader-percent {
|
||||
margin-left: 2px;
|
||||
opacity: 0.9;
|
||||
z-index: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
const LETTERS = "Scanning".split("");
|
||||
|
||||
export default function ScanningLoader({ percent = 0 }) {
|
||||
return (
|
||||
<StyledWrapper>
|
||||
<div className="loader-wrapper">
|
||||
{LETTERS.map((char, i) => (
|
||||
<span key={i} className="loader-letter">
|
||||
{char}
|
||||
</span>
|
||||
))}
|
||||
<span className="loader-percent">{Math.round(percent)}%</span>
|
||||
<div className="loader" />
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
import React from "react";
|
||||
import { motion } from "motion/react";
|
||||
|
||||
export default function ColourfulText({ text }) {
|
||||
const colors = [
|
||||
export default function ColourfulText({ text, usePurpleTheme = false }) {
|
||||
const defaultColors = [
|
||||
"rgb(131, 179, 32)",
|
||||
"rgb(47, 195, 106)",
|
||||
"rgb(42, 169, 210)",
|
||||
@@ -15,6 +15,21 @@ export default function ColourfulText({ text }) {
|
||||
"rgb(232, 98, 63)",
|
||||
"rgb(249, 129, 47)",
|
||||
];
|
||||
|
||||
const purpleColors = [
|
||||
"rgb(174, 182, 192)",
|
||||
"rgb(217, 141, 233)",
|
||||
"rgb(212, 170, 235)",
|
||||
"rgb(230, 170, 239)",
|
||||
"rgb(231, 170, 240)",
|
||||
"rgb(231, 170, 240)",
|
||||
"rgb(231, 170, 240)",
|
||||
"rgb(231, 170, 240)",
|
||||
"rgb(231, 170, 240)",
|
||||
"rgb(231, 170, 240)",
|
||||
];
|
||||
|
||||
const colors = usePurpleTheme ? purpleColors : defaultColors;
|
||||
|
||||
const [currentColors, setCurrentColors] = React.useState(colors);
|
||||
const [count, setCount] = React.useState(0);
|
||||
|
||||
260
Server/src/components/ui/dotted-glow-background.jsx
Normal file
260
Server/src/components/ui/dotted-glow-background.jsx
Normal file
@@ -0,0 +1,260 @@
|
||||
"use client";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
|
||||
/**
|
||||
* Canvas-based dotted background that randomly glows and dims.
|
||||
* - Uses a stable grid of dots.
|
||||
* - Each dot gets its own phase + speed producing organic shimmering.
|
||||
* - Handles high-DPI and resizes via ResizeObserver.
|
||||
*/
|
||||
export const DottedGlowBackground = ({
|
||||
className,
|
||||
gap = 12,
|
||||
radius = 2,
|
||||
color = "rgba(0,0,0,0.7)",
|
||||
darkColor,
|
||||
glowColor = "rgba(0, 170, 255, 0.85)",
|
||||
darkGlowColor,
|
||||
colorLightVar,
|
||||
colorDarkVar,
|
||||
glowColorLightVar,
|
||||
glowColorDarkVar,
|
||||
opacity = 0.6,
|
||||
backgroundOpacity = 0,
|
||||
speedMin = 0.4,
|
||||
speedMax = 1.3,
|
||||
speedScale = 1
|
||||
}) => {
|
||||
const canvasRef = useRef(null);
|
||||
const containerRef = useRef(null);
|
||||
const [resolvedColor, setResolvedColor] = useState(color);
|
||||
const [resolvedGlowColor, setResolvedGlowColor] = useState(glowColor);
|
||||
|
||||
// Resolve CSS variable value from the container or root
|
||||
const resolveCssVariable = (el, variableName) => {
|
||||
if (!variableName) return null;
|
||||
const normalized = variableName.startsWith("--")
|
||||
? variableName
|
||||
: `--${variableName}`;
|
||||
const fromEl = getComputedStyle(el)
|
||||
.getPropertyValue(normalized)
|
||||
.trim();
|
||||
if (fromEl) return fromEl;
|
||||
const root = document.documentElement;
|
||||
const fromRoot = getComputedStyle(root).getPropertyValue(normalized).trim();
|
||||
return fromRoot || null;
|
||||
};
|
||||
|
||||
const detectDarkMode = () => {
|
||||
const root = document.documentElement;
|
||||
if (root.classList.contains("dark")) return true;
|
||||
if (root.classList.contains("light")) return false;
|
||||
return (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
};
|
||||
|
||||
// Keep resolved colors in sync with theme changes and prop updates
|
||||
useEffect(() => {
|
||||
const container = containerRef.current ?? document.documentElement;
|
||||
|
||||
const compute = () => {
|
||||
const isDark = detectDarkMode();
|
||||
|
||||
let nextColor = color;
|
||||
let nextGlow = glowColor;
|
||||
|
||||
if (isDark) {
|
||||
const varDot = resolveCssVariable(container, colorDarkVar);
|
||||
const varGlow = resolveCssVariable(container, glowColorDarkVar);
|
||||
nextColor = varDot || darkColor || nextColor;
|
||||
nextGlow = varGlow || darkGlowColor || nextGlow;
|
||||
} else {
|
||||
const varDot = resolveCssVariable(container, colorLightVar);
|
||||
const varGlow = resolveCssVariable(container, glowColorLightVar);
|
||||
nextColor = varDot || nextColor;
|
||||
nextGlow = varGlow || nextGlow;
|
||||
}
|
||||
|
||||
setResolvedColor(nextColor);
|
||||
setResolvedGlowColor(nextGlow);
|
||||
};
|
||||
|
||||
compute();
|
||||
|
||||
const mql = window.matchMedia
|
||||
? window.matchMedia("(prefers-color-scheme: dark)")
|
||||
: null;
|
||||
const handleMql = () => compute();
|
||||
mql?.addEventListener?.("change", handleMql);
|
||||
|
||||
const mo = new MutationObserver(() => compute());
|
||||
mo.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class", "style"],
|
||||
});
|
||||
|
||||
return () => {
|
||||
mql?.removeEventListener?.("change", handleMql);
|
||||
mo.disconnect();
|
||||
};
|
||||
}, [
|
||||
color,
|
||||
darkColor,
|
||||
glowColor,
|
||||
darkGlowColor,
|
||||
colorLightVar,
|
||||
colorDarkVar,
|
||||
glowColorLightVar,
|
||||
glowColorDarkVar,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = canvasRef.current;
|
||||
const container = containerRef.current;
|
||||
if (!el || !container) return;
|
||||
|
||||
const ctx = el.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
let raf = 0;
|
||||
let stopped = false;
|
||||
|
||||
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
||||
|
||||
const resize = () => {
|
||||
const { width, height } = container.getBoundingClientRect();
|
||||
el.width = Math.max(1, Math.floor(width * dpr));
|
||||
el.height = Math.max(1, Math.floor(height * dpr));
|
||||
el.style.width = `${Math.floor(width)}px`;
|
||||
el.style.height = `${Math.floor(height)}px`;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
};
|
||||
|
||||
const ro = new ResizeObserver(resize);
|
||||
ro.observe(container);
|
||||
resize();
|
||||
|
||||
// Precompute dot metadata for a medium-sized grid and regenerate on resize
|
||||
let dots = [];
|
||||
|
||||
const regenDots = () => {
|
||||
dots = [];
|
||||
const { width, height } = container.getBoundingClientRect();
|
||||
const cols = Math.ceil(width / gap) + 2;
|
||||
const rows = Math.ceil(height / gap) + 2;
|
||||
const min = Math.min(speedMin, speedMax);
|
||||
const max = Math.max(speedMin, speedMax);
|
||||
for (let i = -1; i < cols; i++) {
|
||||
for (let j = -1; j < rows; j++) {
|
||||
const x = i * gap + (j % 2 === 0 ? 0 : gap * 0.5); // offset every other row
|
||||
const y = j * gap;
|
||||
// Randomize phase and speed slightly per dot
|
||||
const phase = Math.random() * Math.PI * 2;
|
||||
const span = Math.max(max - min, 0);
|
||||
const speed = min + Math.random() * span; // configurable rad/s
|
||||
dots.push({ x, y, phase, speed });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const regenThrottled = () => {
|
||||
regenDots();
|
||||
};
|
||||
|
||||
regenDots();
|
||||
|
||||
let last = performance.now();
|
||||
|
||||
const draw = (now) => {
|
||||
if (stopped) return;
|
||||
const dt = (now - last) / 1000; // seconds
|
||||
last = now;
|
||||
const { width, height } = container.getBoundingClientRect();
|
||||
|
||||
ctx.clearRect(0, 0, el.width, el.height);
|
||||
ctx.globalAlpha = opacity;
|
||||
|
||||
// optional subtle background fade for depth (defaults to 0 = transparent)
|
||||
if (backgroundOpacity > 0) {
|
||||
const grad = ctx.createRadialGradient(
|
||||
width * 0.5,
|
||||
height * 0.4,
|
||||
Math.min(width, height) * 0.1,
|
||||
width * 0.5,
|
||||
height * 0.5,
|
||||
Math.max(width, height) * 0.7
|
||||
);
|
||||
grad.addColorStop(0, "rgba(0,0,0,0)");
|
||||
grad.addColorStop(1, `rgba(0,0,0,${Math.min(Math.max(backgroundOpacity, 0), 1)})`);
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
// animate dots
|
||||
ctx.save();
|
||||
ctx.fillStyle = resolvedColor;
|
||||
|
||||
const time = (now / 1000) * Math.max(speedScale, 0);
|
||||
for (let i = 0; i < dots.length; i++) {
|
||||
const d = dots[i];
|
||||
// Linear triangle wave 0..1..0 for linear glow/dim
|
||||
const mod = (time * d.speed + d.phase) % 2;
|
||||
const lin = mod < 1 ? mod : 2 - mod; // 0..1..0
|
||||
const a = 0.25 + 0.55 * lin; // 0.25..0.8 linearly
|
||||
|
||||
// draw glow when bright
|
||||
if (a > 0.6) {
|
||||
const glow = (a - 0.6) / 0.4; // 0..1
|
||||
ctx.shadowColor = resolvedGlowColor;
|
||||
ctx.shadowBlur = 6 * glow;
|
||||
} else {
|
||||
ctx.shadowColor = "transparent";
|
||||
ctx.shadowBlur = 0;
|
||||
}
|
||||
|
||||
ctx.globalAlpha = a * opacity;
|
||||
ctx.beginPath();
|
||||
ctx.arc(d.x, d.y, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
raf = requestAnimationFrame(draw);
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
resize();
|
||||
regenThrottled();
|
||||
};
|
||||
|
||||
window.addEventListener("resize", handleResize);
|
||||
raf = requestAnimationFrame(draw);
|
||||
|
||||
return () => {
|
||||
stopped = true;
|
||||
cancelAnimationFrame(raf);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
ro.disconnect();
|
||||
};
|
||||
}, [
|
||||
gap,
|
||||
radius,
|
||||
resolvedColor,
|
||||
resolvedGlowColor,
|
||||
opacity,
|
||||
backgroundOpacity,
|
||||
speedMin,
|
||||
speedMax,
|
||||
speedScale,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={className}
|
||||
style={{ position: "absolute", inset: 0 }}>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{ display: "block", width: "100%", height: "100%" }} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
51
Server/src/context/ScanContext.jsx
Normal file
51
Server/src/context/ScanContext.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { createContext, useContext, useState, useCallback } from "react";
|
||||
|
||||
const ScanContext = createContext(null);
|
||||
|
||||
export function useScan() {
|
||||
const ctx = useContext(ScanContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useScan must be used within ScanProvider");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* ScanProvider – hält scanning + scanProgress für Scan-UI (Loader, Dashboard ausblenden, etc.)
|
||||
*/
|
||||
export function ScanProvider({ children }) {
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [scanProgress, setScanProgress] = useState({
|
||||
percent: 0,
|
||||
phase: "idle",
|
||||
total: 0,
|
||||
current: 0,
|
||||
complete: false,
|
||||
});
|
||||
|
||||
const startScan = useCallback(() => {
|
||||
setScanning(true);
|
||||
setScanProgress({ percent: 0, phase: "listing", total: 0, current: 0, complete: false });
|
||||
}, []);
|
||||
|
||||
const updateProgress = useCallback((data) => {
|
||||
setScanProgress((prev) => ({ ...prev, ...data }));
|
||||
}, []);
|
||||
|
||||
const endScan = useCallback(() => {
|
||||
setScanning(false);
|
||||
setScanProgress({ percent: 0, phase: "idle", total: 0, current: 0, complete: false });
|
||||
}, []);
|
||||
|
||||
const value = {
|
||||
scanning,
|
||||
setScanning,
|
||||
scanProgress,
|
||||
setScanProgress,
|
||||
startScan,
|
||||
updateProgress,
|
||||
endScan,
|
||||
};
|
||||
|
||||
return <ScanContext.Provider value={value}>{children}</ScanContext.Provider>;
|
||||
}
|
||||
@@ -41,3 +41,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import { ScanProvider } from './context/ScanContext'
|
||||
import './index.css'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<ScanProvider>
|
||||
<App />
|
||||
</ScanProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
|
||||
@@ -99,7 +99,7 @@ export const AccountsPage = () => {
|
||||
updatePayload.account_platform_account_id = parsedData.sellerId;
|
||||
}
|
||||
|
||||
// Shop-Name und account_sells können auch leer sein (optional)
|
||||
// Shop-Name kann auch leer sein (optional)
|
||||
updatePayload.account_shop_name = parsedData.shopName || null;
|
||||
updatePayload.account_sells = parsedData.stats?.itemsSold ?? null;
|
||||
|
||||
@@ -584,21 +584,6 @@ export const AccountsPage = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-medium text-[var(--muted)]">
|
||||
Artikel verkauft (Auto)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={parsedData.stats?.itemsSold ?? "-"}
|
||||
readOnly
|
||||
className="w-full rounded-lg border border-neutral-200 bg-white px-3 py-2 text-sm text-[var(--text)] opacity-75 dark:bg-neutral-900 dark:border-neutral-700"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-[var(--muted)]">
|
||||
Automatisch aus dem eBay-Profil gelesen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="mb-1.5 block text-xs font-medium text-[var(--muted)]">
|
||||
Sales (Auto)
|
||||
|
||||
@@ -200,12 +200,8 @@ export async function createManagedAccount(authUserId, accountData) {
|
||||
account_platform_account_id,
|
||||
account_shop_name: accountData.account_shop_name || null,
|
||||
account_url: accountData.account_url || null,
|
||||
account_sells: accountData.account_sells ?? null,
|
||||
account_managed: true, // Immer true für über die UI erstellte Accounts
|
||||
account_sells: accountData.account_sells ?? null, // Setze account_sells wenn verfügbar
|
||||
};
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'accountsService.js:72',message:'createManagedAccount: payload before Appwrite',data:{account_sells:payload.account_sells,accountData_account_sells:accountData.account_sells},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'E'})}).catch(()=>{});
|
||||
// #endregion
|
||||
|
||||
// account_status ist optional - aufgrund Schema-Konflikt vorerst weglassen
|
||||
// TODO: Schema in Appwrite prüfen und korrigieren (Enum-Feld sollte String akzeptieren, nicht Array)
|
||||
@@ -261,10 +257,6 @@ export async function updateManagedAccount(accountId, accountData) {
|
||||
}
|
||||
});
|
||||
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'accountsService.js:133',message:'updateManagedAccount: before updateDocument',data:{accountId,payload,payloadKeys:Object.keys(payload)},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'A'})}).catch(()=>{});
|
||||
// #endregion
|
||||
|
||||
const document = await databases.updateDocument(
|
||||
databaseId,
|
||||
accountsCollectionId,
|
||||
@@ -272,15 +264,9 @@ export async function updateManagedAccount(accountId, accountData) {
|
||||
payload
|
||||
);
|
||||
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'accountsService.js:147',message:'updateManagedAccount: success',data:{accountId},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'A'})}).catch(()=>{});
|
||||
// #endregion
|
||||
return document;
|
||||
} catch (e) {
|
||||
console.error("Fehler beim Aktualisieren des Accounts:", e);
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'accountsService.js:149',message:'updateManagedAccount: error',data:{accountId,errorCode:e.code,errorMessage:e.message,errorType:e.type},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'C'})}).catch(()=>{});
|
||||
// #endregion
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,9 +129,6 @@ async function getExtensionId() {
|
||||
try {
|
||||
// Methode 1: Verwende gecachte Extension-ID (via postMessage vom Content Script empfangen)
|
||||
if (cachedExtensionId) {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'ebayParserService.js:135',message:'getExtensionId: found via cache',data:{cachedExtensionId},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'D'})}).catch(()=>{});
|
||||
// #endregion
|
||||
return cachedExtensionId;
|
||||
}
|
||||
|
||||
@@ -147,9 +144,6 @@ async function getExtensionId() {
|
||||
}
|
||||
}
|
||||
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'ebayParserService.js:150',message:'getExtensionId: not found after retries',data:{hasWindow:typeof window!=='undefined'},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'D'})}).catch(()=>{});
|
||||
// #endregion
|
||||
return null; // Nicht verfügbar
|
||||
} catch (e) {
|
||||
return null;
|
||||
@@ -163,10 +157,6 @@ async function getExtensionId() {
|
||||
* @throws {Error} - "Extension not available" oder andere Fehler
|
||||
*/
|
||||
async function parseViaExtension(url) {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'ebayParserService.js:165',message:'parseViaExtension: entry',data:{url,hasChrome:typeof chrome!=='undefined',hasRuntime:typeof chrome!=='undefined'&&!!chrome.runtime},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'D'})}).catch(()=>{});
|
||||
// #endregion
|
||||
|
||||
// Validierung
|
||||
if (!url || typeof url !== 'string') {
|
||||
throw new Error("Invalid URL");
|
||||
@@ -178,10 +168,6 @@ async function parseViaExtension(url) {
|
||||
try {
|
||||
const extensionId = await getExtensionId();
|
||||
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'ebayParserService.js:175',message:'parseViaExtension: got extension ID',data:{extensionId,url},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'D'})}).catch(()=>{});
|
||||
// #endregion
|
||||
|
||||
// Versuche chrome.runtime.sendMessage (mit oder ohne Extension-ID)
|
||||
return new Promise((resolve, reject) => {
|
||||
const message = {
|
||||
@@ -189,48 +175,43 @@ async function parseViaExtension(url) {
|
||||
url: url
|
||||
};
|
||||
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'ebayParserService.js:178',message:'parseViaExtension: sending message',data:{extensionId,url},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'D'})}).catch(()=>{});
|
||||
// #endregion
|
||||
|
||||
// SendMessage-Callback
|
||||
const sendMessageCallback = (response) => {
|
||||
// Check for Chrome runtime errors
|
||||
if (chrome.runtime.lastError) {
|
||||
const errorMsg = chrome.runtime.lastError.message || "Extension communication error";
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'ebayParserService.js:158',message:'parseViaExtension: chrome.runtime.sendMessage error',data:{error:errorMsg,extensionId},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'D'})}).catch(()=>{});
|
||||
// #endregion
|
||||
reject(new Error(errorMsg));
|
||||
return;
|
||||
}
|
||||
|
||||
if (response && response.ok && response.data) {
|
||||
// Ensure stats object is included (even if empty)
|
||||
let sellerId = response.data.sellerId || "";
|
||||
|
||||
// Fallback: Wenn Extension keinen sellerId liefert, versuche aus URL zu extrahieren
|
||||
if (!sellerId || sellerId.trim() === "") {
|
||||
// Versuche _ssn Parameter aus Suchergebnis-URLs zu extrahieren
|
||||
const ssnMatch = url.match(/[?&]_ssn=([^&]+)/i);
|
||||
if (ssnMatch && ssnMatch[1]) {
|
||||
sellerId = decodeURIComponent(ssnMatch[1]).trim();
|
||||
}
|
||||
}
|
||||
|
||||
const data = {
|
||||
sellerId: response.data.sellerId || "",
|
||||
sellerId: sellerId,
|
||||
shopName: response.data.shopName || "",
|
||||
market: response.data.market || "US",
|
||||
status: response.data.status || "unknown",
|
||||
stats: response.data.stats || {}
|
||||
};
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'ebayParserService.js:196',message:'parseViaExtension: response success',data:{hasStats:!!response.data.stats,itemsSold:response.data.stats?.itemsSold,stats:response.data.stats},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'D'})}).catch(()=>{});
|
||||
// #endregion
|
||||
resolve(data);
|
||||
} else {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'ebayParserService.js:210',message:'parseViaExtension: response failed',data:{error:response?.error,hasResponse:!!response,ok:response?.ok},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'D'})}).catch(()=>{});
|
||||
// #endregion
|
||||
reject(new Error(response?.error || "Extension parsing failed"));
|
||||
}
|
||||
};
|
||||
|
||||
// Benötigt Extension-ID (sendMessage von Webseiten aus erfordert immer Extension-ID)
|
||||
if (!extensionId) {
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'ebayParserService.js:175',message:'parseViaExtension: no extension ID',data:{hasWindowExtensionId:!!(typeof window!=='undefined'&&window.__EBAY_EXTENSION_ID__)},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'D'})}).catch(()=>{});
|
||||
// #endregion
|
||||
reject(new Error("Extension ID not available"));
|
||||
return;
|
||||
}
|
||||
@@ -265,8 +246,19 @@ async function parseViaExtension(url) {
|
||||
window.removeEventListener('message', responseHandler);
|
||||
|
||||
if (event.data?.ok && event.data?.data) {
|
||||
let sellerId = event.data.data.sellerId || "";
|
||||
|
||||
// Fallback: Wenn Extension keinen sellerId liefert, versuche aus URL zu extrahieren
|
||||
if (!sellerId || sellerId.trim() === "") {
|
||||
// Versuche _ssn Parameter aus Suchergebnis-URLs zu extrahieren
|
||||
const ssnMatch = url.match(/[?&]_ssn=([^&]+)/i);
|
||||
if (ssnMatch && ssnMatch[1]) {
|
||||
sellerId = decodeURIComponent(ssnMatch[1]).trim();
|
||||
}
|
||||
}
|
||||
|
||||
const data = {
|
||||
sellerId: event.data.data.sellerId || "",
|
||||
sellerId: sellerId,
|
||||
shopName: event.data.data.shopName || "",
|
||||
market: event.data.data.market || "US",
|
||||
status: event.data.data.status || "unknown",
|
||||
@@ -330,8 +322,11 @@ export async function parseViaExtensionScanProducts(url, accountId) {
|
||||
accountId: accountId
|
||||
};
|
||||
|
||||
const SCAN_TIMEOUT_MS = 300000;
|
||||
|
||||
// SendMessage-Callback
|
||||
const sendMessageCallback = (response) => {
|
||||
clearTimeout(timeoutId);
|
||||
// Check for Chrome runtime errors
|
||||
if (chrome.runtime.lastError) {
|
||||
const errorMsg = chrome.runtime.lastError.message || "Extension communication error";
|
||||
@@ -339,11 +334,12 @@ export async function parseViaExtensionScanProducts(url, accountId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response && response.ok && response.data) {
|
||||
const items = response.data.items || [];
|
||||
const meta = response.data.meta || response.meta || {};
|
||||
console.log("[ebayParserService] SCAN_PRODUCTS response:", response);
|
||||
|
||||
if (response && response.ok) {
|
||||
const items = response.data?.items || response.items || [];
|
||||
const meta = response.data?.meta || response.meta || {};
|
||||
|
||||
// Wenn items leer oder ok:false, werfe Fehler mit meta
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
const errorMsg = response?.error || "no_items_found";
|
||||
const error = new Error(`SCAN_PRODUCTS failed: ${errorMsg} (${meta.pageType || "unknown"})`);
|
||||
@@ -352,30 +348,27 @@ export async function parseViaExtensionScanProducts(url, accountId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Erfolg: gib items zurück
|
||||
resolve(items);
|
||||
} else {
|
||||
// Fehler: sende error + meta
|
||||
const meta = response?.meta || {};
|
||||
const errorMsg = response?.error || "Extension scanning failed";
|
||||
console.log("[ebayParserService] SCAN_PRODUCTS error:", errorMsg, "meta:", meta);
|
||||
const error = new Error(`SCAN_PRODUCTS failed: ${errorMsg} (${meta.pageType || "unknown"})`);
|
||||
error.meta = meta;
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
// Benötigt Extension-ID (sendMessage von Webseiten aus erfordert immer Extension-ID)
|
||||
if (!extensionId) {
|
||||
reject(new Error("Extension ID not available"));
|
||||
return;
|
||||
}
|
||||
|
||||
chrome.runtime.sendMessage(extensionId, message, sendMessageCallback);
|
||||
|
||||
// Timeout nach 20s (Extension hat intern 20s)
|
||||
setTimeout(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
reject(new Error("Extension timeout"));
|
||||
}, 20000);
|
||||
}, SCAN_TIMEOUT_MS);
|
||||
|
||||
chrome.runtime.sendMessage(extensionId, message, sendMessageCallback);
|
||||
});
|
||||
} catch (error) {
|
||||
// Chrome API Fehler: weiter zu Methode 2
|
||||
@@ -388,45 +381,51 @@ export async function parseViaExtensionScanProducts(url, accountId) {
|
||||
|
||||
// Methode 2: Window flag + postMessage (falls Content Script Relay vorhanden)
|
||||
if (typeof window !== 'undefined' && window.__EBAY_EXTENSION__ === true) {
|
||||
const SCAN_TIMEOUT_MS = 300000;
|
||||
return new Promise((resolve, reject) => {
|
||||
const messageId = `scan_${Date.now()}_${Math.random()}`;
|
||||
|
||||
// Listener für Antwort
|
||||
let settled = false;
|
||||
|
||||
const finish = (fn) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timeoutId);
|
||||
window.removeEventListener('message', responseHandler);
|
||||
fn();
|
||||
};
|
||||
|
||||
const responseHandler = (event) => {
|
||||
if (event.data?.source !== 'eship-extension' || event.data?.messageId !== messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.removeEventListener('message', responseHandler);
|
||||
|
||||
if (event.data?.ok && event.data?.data) {
|
||||
const items = event.data.data.items || event.data.items || [];
|
||||
const meta = event.data.data.meta || event.data.meta || {};
|
||||
|
||||
// Wenn items leer oder ok:false, werfe Fehler mit meta
|
||||
if (!Array.isArray(items) || items.length === 0) {
|
||||
const errorMsg = event.data?.error || "no_items_found";
|
||||
const error = new Error(`SCAN_PRODUCTS failed: ${errorMsg} (${meta.pageType || "unknown"})`);
|
||||
error.meta = meta;
|
||||
reject(error);
|
||||
finish(() => reject(error));
|
||||
return;
|
||||
}
|
||||
|
||||
// Erfolg: gib items zurück
|
||||
resolve(items);
|
||||
finish(() => resolve(items));
|
||||
} else {
|
||||
// Fehler: sende error + meta
|
||||
const meta = event.data?.meta || event.data?.data?.meta || {};
|
||||
const errorMsg = event.data?.error || "Extension scanning failed";
|
||||
const error = new Error(`SCAN_PRODUCTS failed: ${errorMsg} (${meta.pageType || "unknown"})`);
|
||||
error.meta = meta;
|
||||
reject(error);
|
||||
finish(() => reject(error));
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', responseHandler);
|
||||
|
||||
// Sende Request via postMessage
|
||||
const timeoutId = setTimeout(() => {
|
||||
finish(() => reject(new Error("Extension timeout")));
|
||||
}, SCAN_TIMEOUT_MS);
|
||||
|
||||
window.postMessage({
|
||||
source: 'eship-webapp',
|
||||
action: 'SCAN_PRODUCTS',
|
||||
@@ -434,12 +433,6 @@ export async function parseViaExtensionScanProducts(url, accountId) {
|
||||
accountId: accountId,
|
||||
messageId: messageId
|
||||
}, '*');
|
||||
|
||||
// Timeout
|
||||
setTimeout(() => {
|
||||
window.removeEventListener('message', responseHandler);
|
||||
reject(new Error("Extension timeout"));
|
||||
}, 20000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -447,6 +440,31 @@ export async function parseViaExtensionScanProducts(url, accountId) {
|
||||
throw new Error("Extension not available");
|
||||
}
|
||||
|
||||
/**
|
||||
* Holt den aktuellen Scan-Fortschritt von der Extension (für Polling während SCAN_PRODUCTS).
|
||||
* @returns {Promise<{ percent: number, phase: string, total: number, current: number, complete: boolean }|null>}
|
||||
*/
|
||||
export async function getScanProgress() {
|
||||
if (typeof chrome === "undefined" || !chrome?.runtime?.sendMessage) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const extensionId = await getExtensionId();
|
||||
if (!extensionId) return null;
|
||||
return new Promise((resolve) => {
|
||||
chrome.runtime.sendMessage(extensionId, { action: "GET_SCAN_PROGRESS" }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
resolve(response ?? null);
|
||||
});
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parst eine eBay-URL mit Stub-Logik (deterministisch, keine Network-Calls)
|
||||
* @param {string} url - eBay-Verkäuferprofil oder Shop-URL
|
||||
@@ -472,9 +490,18 @@ async function parseViaStub(url) {
|
||||
const hash = stableHash(url);
|
||||
const market = extractMarketFromUrl(url);
|
||||
|
||||
// Seller ID: Deterministic aus URL-Hash
|
||||
// Format: "ebay_" + hash (first 10 chars)
|
||||
const sellerId = `ebay_${hash.slice(0, 10)}`;
|
||||
// Seller ID: Versuche zuerst aus URL zu extrahieren (_ssn Parameter)
|
||||
let sellerId = "";
|
||||
const ssnMatch = url.match(/[?&]_ssn=([^&]+)/i);
|
||||
if (ssnMatch && ssnMatch[1]) {
|
||||
sellerId = decodeURIComponent(ssnMatch[1]).trim();
|
||||
}
|
||||
|
||||
// Fallback: Deterministic aus URL-Hash wenn kein _ssn Parameter gefunden
|
||||
if (!sellerId || sellerId.trim() === "") {
|
||||
// Format: "ebay_" + hash (first 10 chars)
|
||||
sellerId = `ebay_${hash.slice(0, 10)}`;
|
||||
}
|
||||
|
||||
// Shop Name: Generiert aus Hash (last 4 chars als Suffix)
|
||||
const shopNameSuffix = hash.slice(-4);
|
||||
@@ -504,27 +531,14 @@ async function parseViaStub(url) {
|
||||
export async function parseEbayAccount(url) {
|
||||
// Versuche IMMER Extension-Pfad zuerst (auch wenn Flag nicht gesetzt)
|
||||
// parseViaExtension prüft selbst, ob Extension verfügbar ist
|
||||
// #region agent log
|
||||
const extAvailable = isExtensionAvailable();
|
||||
const hasChromeRuntime = typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.sendMessage;
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'ebayParserService.js:292',message:'parseEbayAccount: route decision',data:{extAvailable,hasChromeRuntime,url},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'D'})}).catch(()=>{});
|
||||
// #endregion
|
||||
|
||||
try {
|
||||
// Versuche Extension zu nutzen (auch wenn Flag nicht gesetzt - parseViaExtension prüft selbst)
|
||||
return await parseViaExtension(url);
|
||||
} catch (e) {
|
||||
// Extension-Fehler: Fallback zu Stub
|
||||
console.warn("Extension parsing failed, falling back to stub:", e.message);
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'ebayParserService.js:299',message:'parseEbayAccount: extension error, using stub',data:{error:e.message},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'D'})}).catch(()=>{});
|
||||
// #endregion
|
||||
}
|
||||
|
||||
// Fallback: Stub-Implementierung
|
||||
const stubResult = await parseViaStub(url);
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'ebayParserService.js:304',message:'parseEbayAccount: stub result',data:{itemsSold:stubResult.stats?.itemsSold},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'D'})}).catch(()=>{});
|
||||
// #endregion
|
||||
return stubResult;
|
||||
return await parseViaStub(url);
|
||||
}
|
||||
@@ -11,6 +11,46 @@ import { parseViaExtensionScanProducts } from "./ebayParserService";
|
||||
|
||||
const productsCollectionId = import.meta.env.VITE_APPWRITE_PRODUCTS_COLLECTION_ID || "products";
|
||||
|
||||
/** Deutsche eBay-Zustandsbezeichnungen → product_condition Enum (längere zuerst) */
|
||||
const CONDITION_DE_TO_ENUM = [
|
||||
["neu mit etikett", "new_with_tags"],
|
||||
["neu ohne etikett", "new_without_tags"],
|
||||
["neu mit mängeln", "new_with_defects"],
|
||||
["vom hersteller generalüberholt", "manufacturer_refurbished"],
|
||||
["vom hersteller generalueberholt", "manufacturer_refurbished"],
|
||||
["vom verkäufer generalüberholt", "seller_refurbished"],
|
||||
["vom verkäufer generalueberholt", "seller_refurbished"],
|
||||
["vom verkaeufer generalüberholt", "seller_refurbished"],
|
||||
["vom verkaeufer generalueberholt", "seller_refurbished"],
|
||||
["defekt oder unvollständig", "for_parts_or_not_working"],
|
||||
["defekt oder unvollstaendig", "for_parts_or_not_working"],
|
||||
["sehr gut", "very_good"],
|
||||
["wie neu", "like_new"],
|
||||
["ausstellerstück", "display_item"],
|
||||
["ausstellerstueck", "display_item"],
|
||||
["unbenutzt", "unused"],
|
||||
["antik", "antique"],
|
||||
["neu", "new"],
|
||||
["gebraucht", "used"],
|
||||
["gut", "good"],
|
||||
["akzeptabel", "acceptable"],
|
||||
];
|
||||
|
||||
/**
|
||||
* Mappt deutsche eBay-Zustandsangabe auf product_condition Enum.
|
||||
* @param {string|null|undefined} de - z.B. "Neu", "Gebraucht", "Neu mit Etikett"
|
||||
* @returns {string|null} Enum-Wert oder null wenn nicht zuordenbar
|
||||
*/
|
||||
function mapConditionDeToEnum(de) {
|
||||
if (de == null || typeof de !== "string") return null;
|
||||
const n = de.trim().toLowerCase().replace(/\s+/g, " ");
|
||||
if (!n) return null;
|
||||
for (const [phrase, enumVal] of CONDITION_DE_TO_ENUM) {
|
||||
if (n.includes(phrase)) return enumVal;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt alle Products für einen bestimmten Account
|
||||
* @param {string} accountId - ID des Accounts (product_account_id)
|
||||
@@ -68,6 +108,31 @@ export async function listProductsByAccount(accountId, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
const EXISTING_PRODUCTS_PAGE_SIZE = 100;
|
||||
|
||||
/**
|
||||
* Lädt alle Products eines Accounts für Duplikat-Prüfung (paginiert, Appwrite-Limit 100/Request).
|
||||
* @param {string} accountId
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async function fetchAllProductsByAccount(accountId) {
|
||||
const all = [];
|
||||
let offset = 0;
|
||||
let hasMore = true;
|
||||
while (hasMore) {
|
||||
const page = await listProductsByAccount(accountId, {
|
||||
limit: EXISTING_PRODUCTS_PAGE_SIZE,
|
||||
offset,
|
||||
orderBy: "$createdAt",
|
||||
orderType: "asc",
|
||||
});
|
||||
all.push(...page);
|
||||
hasMore = page.length === EXISTING_PRODUCTS_PAGE_SIZE;
|
||||
offset += page.length;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt ein einzelnes Product nach ID
|
||||
* @param {string} productId - ID des Products
|
||||
@@ -118,33 +183,97 @@ export async function scanProductsForAccount(accountId) {
|
||||
}
|
||||
|
||||
// 2. Extension aufrufen: scanne Produkte
|
||||
const items = await parseViaExtensionScanProducts(account.account_url, accountId);
|
||||
let items;
|
||||
try {
|
||||
items = await parseViaExtensionScanProducts(account.account_url, accountId);
|
||||
} catch (extensionError) {
|
||||
// Extension-Fehler: Parsing failed
|
||||
console.error("Extension-Fehler beim Scannen:", extensionError);
|
||||
|
||||
// Erstelle detaillierte Fehlermeldung mit meta-Informationen
|
||||
const meta = extensionError.meta || {};
|
||||
let errorMessage = extensionError.message || "Parsing failed";
|
||||
|
||||
// Füge meta-Details hinzu, falls vorhanden
|
||||
if (meta.pageType) {
|
||||
errorMessage += ` (Seitentyp: ${meta.pageType})`;
|
||||
}
|
||||
if (meta.reason) {
|
||||
errorMessage += ` (Grund: ${meta.reason})`;
|
||||
}
|
||||
if (meta.finalUrl) {
|
||||
errorMessage += ` (URL: ${meta.finalUrl})`;
|
||||
}
|
||||
|
||||
const detailedError = new Error(`Extension-Fehler: ${errorMessage}`);
|
||||
detailedError.meta = meta;
|
||||
throw detailedError;
|
||||
}
|
||||
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'productsService.js:scanProductsForAccount',message:'items from extension',data:{itemsCount:items?.length},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H4'})}).catch(()=>{});
|
||||
// #endregion
|
||||
// 3. Response-Items zu Product-Schema mappen
|
||||
const mappedProducts = items.map(item => {
|
||||
// Validiere, dass platformProductId vorhanden ist
|
||||
if (!item.platformProductId) {
|
||||
console.warn("Item ohne platformProductId übersprungen:", item);
|
||||
return null;
|
||||
}
|
||||
const id = String(item.platformProductId).trim();
|
||||
if (id === "123456" || id.length < 10) {
|
||||
console.warn("Item mit Platzhalter-/ungültiger ID übersprungen (eBay-IDs 10–12 Ziffern):", id);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
product_platform_product_id: item.platformProductId,
|
||||
product_title: item.title || "",
|
||||
product_price: item.price ?? undefined, // undefined statt null für Appwrite
|
||||
product_currency: item.currency ?? undefined, // auto-fill from market if undefined
|
||||
product_price: item.price ?? undefined,
|
||||
product_currency: item.currency ?? undefined,
|
||||
product_url: item.url || "",
|
||||
product_status: item.status ?? "unknown",
|
||||
product_category: item.category ?? "unknown",
|
||||
product_condition: item.condition ?? "unknown"
|
||||
product_category: String(item.category ?? "unknown").slice(0, 255),
|
||||
product_condition: mapConditionDeToEnum(item.condition) ?? "used",
|
||||
product_quantity_sold: item.quantitySold ?? undefined,
|
||||
product_quantity_available: item.quantityAvailable ?? undefined,
|
||||
product_watch_count: item.watchCount ?? undefined,
|
||||
product_in_carts_count: item.inCartsCount ?? undefined
|
||||
};
|
||||
}).filter(Boolean); // Entferne null-Einträge
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'productsService.js:scanProductsForAccount',message:'mapped products',data:{mappedCount:mappedProducts.length},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H2'})}).catch(()=>{});
|
||||
// #endregion
|
||||
|
||||
// 4. upsertProductsForAccount aufrufen
|
||||
const result = await upsertProductsForAccount(accountId, mappedProducts);
|
||||
let result;
|
||||
try {
|
||||
result = await upsertProductsForAccount(accountId, mappedProducts);
|
||||
} catch (dbError) {
|
||||
// Datenbank-Fehler: Collection oder Attribute fehlen
|
||||
console.error("Datenbank-Fehler beim Speichern:", dbError);
|
||||
|
||||
// Prüfe auf spezifische Datenbank-Fehler
|
||||
if (dbError.code === 404 || dbError.type === 'collection_not_found') {
|
||||
throw new Error("Datenbank-Fehler: Products-Collection existiert nicht. Bitte Datenbank-Schema einrichten.");
|
||||
}
|
||||
if (dbError.code === 400 || dbError.message?.includes('attribute')) {
|
||||
throw new Error(`Datenbank-Fehler: Ein Attribut fehlt oder ist ungültig. Details: ${dbError.message}`);
|
||||
}
|
||||
if (dbError.code === 401 || dbError.type === 'permission_denied') {
|
||||
throw new Error("Datenbank-Fehler: Keine Berechtigung zum Speichern von Produkten. Bitte Berechtigungen prüfen.");
|
||||
}
|
||||
|
||||
// Generischer Datenbank-Fehler
|
||||
throw new Error(`Datenbank-Fehler beim Speichern: ${dbError.message || "Unbekannter Fehler"}`);
|
||||
}
|
||||
|
||||
// 5. account_last_scan_at aktualisieren
|
||||
await updateAccountLastScanAt(accountId, new Date().toISOString());
|
||||
try {
|
||||
await updateAccountLastScanAt(accountId, new Date().toISOString());
|
||||
} catch (updateError) {
|
||||
// Nicht kritisch, nur loggen
|
||||
console.warn("Fehler beim Aktualisieren von account_last_scan_at:", updateError);
|
||||
}
|
||||
|
||||
// 6. Return { created, updated }
|
||||
return result;
|
||||
@@ -186,11 +315,11 @@ export async function upsertProductsForAccount(accountId, products) {
|
||||
const currency = deriveCurrencyFromMarket(market);
|
||||
const platform = "ebay"; // Enum-Werte sind lowercase gemäß Fehlermeldung: [amazon], [ebay]
|
||||
|
||||
// Lade bestehende Produkte für Duplikat-Prüfung
|
||||
const existingProducts = await listProductsByAccount(accountId, {
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
});
|
||||
// Lade alle bestehenden Produkte (paginiert, Appwrite max 100/Request)
|
||||
const existingProducts = await fetchAllProductsByAccount(accountId);
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'productsService.js:upsertProductsForAccount',message:'existing products loaded',data:{productsToUpsert:products.length,existingCount:existingProducts.length},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
|
||||
// Erstelle Map von platform_product_id -> bestehendes Produkt
|
||||
const existingProductsMap = new Map();
|
||||
@@ -234,22 +363,43 @@ export async function upsertProductsForAccount(accountId, products) {
|
||||
);
|
||||
updated++;
|
||||
} else {
|
||||
// Erstelle neues Produkt
|
||||
// Erstelle neues Produkt (product_first_fullscan_at = heutiges Datum)
|
||||
await databases.createDocument(
|
||||
databaseId,
|
||||
productsCollectionId,
|
||||
ID.unique(),
|
||||
fullProductData
|
||||
{
|
||||
...fullProductData,
|
||||
product_first_fullscan_at: new Date().toISOString()
|
||||
}
|
||||
);
|
||||
created++;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Fehler beim Upsert des Produkts "${platformProductId}":`, e);
|
||||
// Weiter mit nächstem Produkt
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'productsService.js:upsertProductsForAccount',message:'upsert error',data:{platformProductId,code:e?.code,type:e?.type,message:String(e?.message||'')},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H3'})}).catch(()=>{});
|
||||
// #endregion
|
||||
// Prüfe auf spezifische Datenbank-Fehler
|
||||
if (e.code === 404 || e.type === 'collection_not_found' || e.type === 'document_not_found') {
|
||||
throw new Error("Datenbank-Fehler: Products-Collection oder Dokument existiert nicht. Bitte Datenbank-Schema einrichten.");
|
||||
}
|
||||
if (e.code === 400 || e.message?.includes('attribute') || e.message?.includes('required')) {
|
||||
throw new Error(`Datenbank-Fehler: Ein Attribut fehlt oder ist ungültig. Produkt: ${platformProductId}, Details: ${e.message}`);
|
||||
}
|
||||
if (e.code === 401 || e.type === 'permission_denied') {
|
||||
throw new Error("Datenbank-Fehler: Keine Berechtigung zum Speichern von Produkten. Bitte Berechtigungen prüfen.");
|
||||
}
|
||||
|
||||
// Bei anderen Fehlern: Weiter mit nächstem Produkt (nicht kritisch)
|
||||
console.warn(`Überspringe Produkt "${platformProductId}" aufgrund von Fehler:`, e.message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7242/ingest/246fe132-ecc5-435f-bd9c-fe5e8e26089d',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'productsService.js:upsertProductsForAccount',message:'upsert result',data:{created,updated},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'H1'})}).catch(()=>{});
|
||||
// #endregion
|
||||
return { created, updated };
|
||||
} catch (e) {
|
||||
console.error("Fehler beim Upsert der Produkte:", e);
|
||||
|
||||
Reference in New Issue
Block a user