Compare commits
2 Commits
dbeaa5006f
...
d0066d3974
| Author | SHA1 | Date | |
|---|---|---|---|
| d0066d3974 | |||
| 636ca1341c |
@@ -1,60 +0,0 @@
|
|||||||
{"location":"productsService.js:116","message":"scanProductsForAccount: market derived","data":{"accountId":"696ccb2400395714987c","market":"DE","account_platform_market":"DE","account_url":"https://www.ebay.de/sch/i.html?item=397047173300&rt=nc&_trksid=p4429486.m3561.l161211&_ssn=miceusi"},"timestamp":1768741151411,"sessionId":"debug-session","runId":"run1","hypothesisId":"C"}
|
|
||||||
{"location":"productsService.js:127","message":"scanProductsForAccount: currency derived","data":{"market":"DE","currency":"EUR","productsCollectionId":"products","databaseId":"eship-db","platformWillBeSetTo":"eBay"},"timestamp":1768741151411,"sessionId":"debug-session","runId":"post-fix","hypothesisId":"E"}
|
|
||||||
{"location":"productsService.js:190","message":"scanProductsForAccount: payload before createDocument","data":{"platformProductId":"stub_1243_1","product_platform":"ebay","product_platform_type":"string","product_platform_length":4,"product_platform_JSON":"\"ebay\"","fullPayload":"{\"product_account_id\":\"696ccb2400395714987c\",\"product_platform\":\"ebay\",\"product_platform_market\":\"DE\",\"product_currency\":\"EUR\",\"product_platform_product_id\":\"stub_1243_1\",\"product_title\":\"Scanned Item 1\",\"product_price\":58.5,\"product_url\":\"https://www.ebay.de/itm/fake-696ccb24-1\"}","payloadKeys":["product_account_id","product_platform","product_platform_market","product_currency","product_platform_product_id","product_title","product_price","product_url"]},"timestamp":1768741151507,"sessionId":"debug-session","runId":"run2","hypothesisId":"D"}
|
|
||||||
{"location":"productsService.js:185","message":"scanProductsForAccount: createDocument success","data":{"platformProductId":"stub_1243_1","created":0},"timestamp":1768741151644,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"productsService.js:190","message":"scanProductsForAccount: payload before createDocument","data":{"platformProductId":"stub_1243_2","product_platform":"ebay","product_platform_type":"string","product_platform_length":4,"product_platform_JSON":"\"ebay\"","fullPayload":"{\"product_account_id\":\"696ccb2400395714987c\",\"product_platform\":\"ebay\",\"product_platform_market\":\"DE\",\"product_currency\":\"EUR\",\"product_platform_product_id\":\"stub_1243_2\",\"product_title\":\"Scanned Item 2\",\"product_price\":64,\"product_url\":\"https://www.ebay.de/itm/fake-696ccb24-2\"}","payloadKeys":["product_account_id","product_platform","product_platform_market","product_currency","product_platform_product_id","product_title","product_price","product_url"]},"timestamp":1768741151644,"sessionId":"debug-session","runId":"run2","hypothesisId":"D"}
|
|
||||||
{"location":"productsService.js:185","message":"scanProductsForAccount: createDocument success","data":{"platformProductId":"stub_1243_2","created":1},"timestamp":1768741151700,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"productsService.js:190","message":"scanProductsForAccount: payload before createDocument","data":{"platformProductId":"stub_1243_3","product_platform":"ebay","product_platform_type":"string","product_platform_length":4,"product_platform_JSON":"\"ebay\"","fullPayload":"{\"product_account_id\":\"696ccb2400395714987c\",\"product_platform\":\"ebay\",\"product_platform_market\":\"DE\",\"product_currency\":\"EUR\",\"product_platform_product_id\":\"stub_1243_3\",\"product_title\":\"Scanned Item 3\",\"product_price\":69.5,\"product_url\":\"https://www.ebay.de/itm/fake-696ccb24-3\"}","payloadKeys":["product_account_id","product_platform","product_platform_market","product_currency","product_platform_product_id","product_title","product_price","product_url"]},"timestamp":1768741151701,"sessionId":"debug-session","runId":"run2","hypothesisId":"D"}
|
|
||||||
{"location":"productsService.js:185","message":"scanProductsForAccount: createDocument success","data":{"platformProductId":"stub_1243_3","created":2},"timestamp":1768741151751,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"productsService.js:190","message":"scanProductsForAccount: payload before createDocument","data":{"platformProductId":"stub_1243_4","product_platform":"ebay","product_platform_type":"string","product_platform_length":4,"product_platform_JSON":"\"ebay\"","fullPayload":"{\"product_account_id\":\"696ccb2400395714987c\",\"product_platform\":\"ebay\",\"product_platform_market\":\"DE\",\"product_currency\":\"EUR\",\"product_platform_product_id\":\"stub_1243_4\",\"product_title\":\"Scanned Item 4\",\"product_price\":75,\"product_url\":\"https://www.ebay.de/itm/fake-696ccb24-4\"}","payloadKeys":["product_account_id","product_platform","product_platform_market","product_currency","product_platform_product_id","product_title","product_price","product_url"]},"timestamp":1768741151752,"sessionId":"debug-session","runId":"run2","hypothesisId":"D"}
|
|
||||||
{"location":"productsService.js:185","message":"scanProductsForAccount: createDocument success","data":{"platformProductId":"stub_1243_4","created":3},"timestamp":1768741151800,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"productsService.js:190","message":"scanProductsForAccount: payload before createDocument","data":{"platformProductId":"stub_1243_5","product_platform":"ebay","product_platform_type":"string","product_platform_length":4,"product_platform_JSON":"\"ebay\"","fullPayload":"{\"product_account_id\":\"696ccb2400395714987c\",\"product_platform\":\"ebay\",\"product_platform_market\":\"DE\",\"product_currency\":\"EUR\",\"product_platform_product_id\":\"stub_1243_5\",\"product_title\":\"Scanned Item 5\",\"product_price\":80.5,\"product_url\":\"https://www.ebay.de/itm/fake-696ccb24-5\"}","payloadKeys":["product_account_id","product_platform","product_platform_market","product_currency","product_platform_product_id","product_title","product_price","product_url"]},"timestamp":1768741151800,"sessionId":"debug-session","runId":"run2","hypothesisId":"D"}
|
|
||||||
{"location":"productsService.js:185","message":"scanProductsForAccount: createDocument success","data":{"platformProductId":"stub_1243_5","created":4},"timestamp":1768741151844,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"ebayParserService.js:292","message":"parseEbayAccount: route decision","data":{"extAvailable":true,"url":"https://www.ebay.de/str/goldbloom25?_trksid=p4429486.m3561.l161211"},"timestamp":1768741538650,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:135","message":"getExtensionId: found via cache","data":{"cachedExtensionId":"ikldokdleojiinjklkhkkhfhpfafeaoc"},"timestamp":1768741538651,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:158","message":"parseViaExtension: chrome.runtime.sendMessage error","data":{"error":"Could not establish connection. Receiving end does not exist.","extensionId":"ikldokdleojiinjklkhkkhfhpfafeaoc"},"timestamp":1768741538659,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:299","message":"parseEbayAccount: extension error, using stub","data":{"error":"Could not establish connection. Receiving end does not exist."},"timestamp":1768741538659,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:304","message":"parseEbayAccount: stub result","data":{"itemsSold":null},"timestamp":1768741538660,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"AccountsPage.jsx:193","message":"handleFormSubmit: parsedData before save","data":{"hasStats":true,"itemsSold":null,"accountSellsValue":null},"timestamp":1768741540654,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"accountsService.js:72","message":"createManagedAccount: payload before Appwrite","data":{"account_sells":null,"accountData_account_sells":null},"timestamp":1768741540655,"sessionId":"debug-session","runId":"run1","hypothesisId":"E"}
|
|
||||||
{"location":"ebayParserService.js:292","message":"parseEbayAccount: route decision","data":{"extAvailable":true,"url":"https://www.ebay.de/str/goldbloom25?_trksid=p4429486.m3561.l161211"},"timestamp":1768741552261,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:135","message":"getExtensionId: found via cache","data":{"cachedExtensionId":"ikldokdleojiinjklkhkkhfhpfafeaoc"},"timestamp":1768741552262,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:158","message":"parseViaExtension: chrome.runtime.sendMessage error","data":{"error":"Could not establish connection. Receiving end does not exist.","extensionId":"ikldokdleojiinjklkhkkhfhpfafeaoc"},"timestamp":1768741552269,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:299","message":"parseEbayAccount: extension error, using stub","data":{"error":"Could not establish connection. Receiving end does not exist."},"timestamp":1768741552269,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:304","message":"parseEbayAccount: stub result","data":{"itemsSold":null},"timestamp":1768741552270,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"AccountsPage.jsx:93","message":"handleRefreshAccount: update payload","data":{"payload":{"account_platform_market":"DE","account_platform_account_id":"ebay_0000uuvjdi","account_shop_name":"eBay Seller vjdi","account_sells":null}},"timestamp":1768741552270,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"accountsService.js:133","message":"updateManagedAccount: before updateDocument","data":{"accountId":"696cdaa40028f011f5d0","payload":{"account_platform_market":"DE","account_platform_account_id":"ebay_0000uuvjdi","account_shop_name":"eBay Seller vjdi"},"payloadKeys":["account_platform_market","account_platform_account_id","account_shop_name"]},"timestamp":1768741552270,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"accountsService.js:147","message":"updateManagedAccount: success","data":{"accountId":"696cdaa40028f011f5d0"},"timestamp":1768741552346,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"ebayParserService.js:292","message":"parseEbayAccount: route decision","data":{"extAvailable":true,"url":"https://www.ebay.de/str/ihaveitmusic?_trksid=p4429486.m3561.l161211"},"timestamp":1768741553567,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:135","message":"getExtensionId: found via cache","data":{"cachedExtensionId":"ikldokdleojiinjklkhkkhfhpfafeaoc"},"timestamp":1768741553568,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:158","message":"parseViaExtension: chrome.runtime.sendMessage error","data":{"error":"Could not establish connection. Receiving end does not exist.","extensionId":"ikldokdleojiinjklkhkkhfhpfafeaoc"},"timestamp":1768741553574,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:299","message":"parseEbayAccount: extension error, using stub","data":{"error":"Could not establish connection. Receiving end does not exist."},"timestamp":1768741553574,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:304","message":"parseEbayAccount: stub result","data":{"itemsSold":null},"timestamp":1768741553574,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"AccountsPage.jsx:93","message":"handleRefreshAccount: update payload","data":{"payload":{"account_platform_market":"DE","account_platform_account_id":"ebay_0000h1nxit","account_shop_name":"eBay Seller nxit","account_sells":null}},"timestamp":1768741553575,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"accountsService.js:133","message":"updateManagedAccount: before updateDocument","data":{"accountId":"696cbd07000703cf5437","payload":{"account_platform_market":"DE","account_platform_account_id":"ebay_0000h1nxit","account_shop_name":"eBay Seller nxit"},"payloadKeys":["account_platform_market","account_platform_account_id","account_shop_name"]},"timestamp":1768741553575,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"accountsService.js:147","message":"updateManagedAccount: success","data":{"accountId":"696cbd07000703cf5437"},"timestamp":1768741553651,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"ebayParserService.js:292","message":"parseEbayAccount: route decision","data":{"extAvailable":true,"url":"https://www.ebay.de/str/goldbloom25?_trksid=p4429486.m3561.l161211"},"timestamp":1768741571041,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:135","message":"getExtensionId: found via cache","data":{"cachedExtensionId":"ikldokdleojiinjklkhkkhfhpfafeaoc"},"timestamp":1768741571042,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:160","message":"parseViaExtension: response data from extension","data":{"hasStats":true,"itemsSold":1588,"stats":{"positiveRate":7,"feedbackCount":0,"itemsForSale":588,"itemsSold":1588}},"timestamp":1768741573909,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"AccountsPage.jsx:93","message":"handleRefreshAccount: update payload","data":{"payload":{"account_platform_market":"DE","account_platform_account_id":"goldbloom25","account_shop_name":"goldbloom25","account_sells":1588}},"timestamp":1768741573909,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"accountsService.js:133","message":"updateManagedAccount: before updateDocument","data":{"accountId":"696cdaa40028f011f5d0","payload":{"account_platform_market":"DE","account_platform_account_id":"goldbloom25","account_shop_name":"goldbloom25","account_sells":1588},"payloadKeys":["account_platform_market","account_platform_account_id","account_shop_name","account_sells"]},"timestamp":1768741573909,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"accountsService.js:147","message":"updateManagedAccount: success","data":{"accountId":"696cdaa40028f011f5d0"},"timestamp":1768741574001,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"ebayParserService.js:292","message":"parseEbayAccount: route decision","data":{"extAvailable":true,"url":"https://www.ebay.de/sch/i.html?item=397047173300&rt=nc&_trksid=p4429486.m3561.l161211&_ssn=miceusi"},"timestamp":1768741581711,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:135","message":"getExtensionId: found via cache","data":{"cachedExtensionId":"ikldokdleojiinjklkhkkhfhpfafeaoc"},"timestamp":1768741581712,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:299","message":"parseEbayAccount: extension error, using stub","data":{"error":"timeout"},"timestamp":1768741596751,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"AccountsPage.jsx:93","message":"handleRefreshAccount: update payload","data":{"payload":{"account_platform_market":"DE","account_platform_account_id":"ebay_00002jm9ly","account_shop_name":"eBay Seller m9ly","account_sells":null}},"timestamp":1768741596752,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"accountsService.js:133","message":"updateManagedAccount: before updateDocument","data":{"accountId":"696ccb2400395714987c","payload":{"account_platform_market":"DE","account_platform_account_id":"ebay_00002jm9ly","account_shop_name":"eBay Seller m9ly"},"payloadKeys":["account_platform_market","account_platform_account_id","account_shop_name"]},"timestamp":1768741596752,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"ebayParserService.js:304","message":"parseEbayAccount: stub result","data":{"itemsSold":null},"timestamp":1768741596752,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"accountsService.js:147","message":"updateManagedAccount: success","data":{"accountId":"696ccb2400395714987c"},"timestamp":1768741596841,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"ebayParserService.js:292","message":"parseEbayAccount: route decision","data":{"extAvailable":true,"url":"https://www.ebay.de/str/ihaveitmusic?_trksid=p4429486.m3561.l161211"},"timestamp":1768741610165,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:135","message":"getExtensionId: found via cache","data":{"cachedExtensionId":"ikldokdleojiinjklkhkkhfhpfafeaoc"},"timestamp":1768741610165,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:160","message":"parseViaExtension: response data from extension","data":{"hasStats":true,"itemsSold":280535,"stats":{"positiveRate":5,"feedbackCount":0,"itemsForSale":535,"itemsSold":280535}},"timestamp":1768741616427,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"accountsService.js:133","message":"updateManagedAccount: before updateDocument","data":{"accountId":"696cbd07000703cf5437","payload":{"account_platform_market":"DE","account_platform_account_id":"ihaveitmusic","account_shop_name":"iHaveit","account_sells":280535},"payloadKeys":["account_platform_market","account_platform_account_id","account_shop_name","account_sells"]},"timestamp":1768741616428,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"AccountsPage.jsx:93","message":"handleRefreshAccount: update payload","data":{"payload":{"account_platform_market":"DE","account_platform_account_id":"ihaveitmusic","account_shop_name":"iHaveit","account_sells":280535}},"timestamp":1768741616427,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"accountsService.js:147","message":"updateManagedAccount: success","data":{"accountId":"696cbd07000703cf5437"},"timestamp":1768741616513,"sessionId":"debug-session","runId":"run1","hypothesisId":"A"}
|
|
||||||
{"location":"ebayParserService.js:150","message":"getExtensionId: not found after retries","data":{"hasWindow":true},"timestamp":1768742766564,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:150","message":"getExtensionId: not found after retries","data":{"hasWindow":true},"timestamp":1768742773836,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:135","message":"getExtensionId: found via cache","data":{"cachedExtensionId":"ikldokdleojiinjklkhkkhfhpfafeaoc"},"timestamp":1768742796643,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:135","message":"getExtensionId: found via cache","data":{"cachedExtensionId":"ikldokdleojiinjklkhkkhfhpfafeaoc"},"timestamp":1768742817118,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:135","message":"getExtensionId: found via cache","data":{"cachedExtensionId":"ikldokdleojiinjklkhkkhfhpfafeaoc"},"timestamp":1768742900325,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
{"location":"ebayParserService.js:135","message":"getExtensionId: found via cache","data":{"cachedExtensionId":"ikldokdleojiinjklkhkkhfhpfafeaoc"},"timestamp":1768742905657,"sessionId":"debug-session","runId":"run1","hypothesisId":"D"}
|
|
||||||
@@ -2,9 +2,12 @@ const STORAGE_KEY = "auth_jwt";
|
|||||||
const BACKEND_URL = "http://localhost:5173"; // TODO: Backend URL konfigurieren
|
const BACKEND_URL = "http://localhost:5173"; // TODO: Backend URL konfigurieren
|
||||||
|
|
||||||
const PARSE_TIMEOUT_MS = 15000; // 15 seconds
|
const PARSE_TIMEOUT_MS = 15000; // 15 seconds
|
||||||
const SCAN_TIMEOUT_MS = 20000; // 20 seconds (seller listing pages can be slower)
|
const SCAN_TIMEOUT_MS = 45000; // 45 seconds (listing with _ipg=240 can be slow)
|
||||||
const activeParseRequests = new Map(); // Map<tabId, { timeout, originalSender, resolve }>
|
const activeParseRequests = new Map(); // Map<tabId, { timeout, originalSender, resolve }>
|
||||||
const activeScanRequests = new Map(); // Map<tabId, { timeout, sendResponse }>
|
const activeScanRequests = new Map(); // Map<tabId, { timeout, sendResponse }>;
|
||||||
|
|
||||||
|
/** Aktueller Scan-Fortschritt für GET_SCAN_PROGRESS (Polling durch Web-App) */
|
||||||
|
let currentScanProgress = null;
|
||||||
|
|
||||||
// Messages from content script (der von der Web-App kommt)
|
// Messages from content script (der von der Web-App kommt)
|
||||||
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||||
@@ -64,6 +67,11 @@ chrome.runtime.onMessageExternal.addListener((msg, sender, sendResponse) => {
|
|||||||
handleScanProductsRequest(msg.url, msg.accountId, sendResponse);
|
handleScanProductsRequest(msg.url, msg.accountId, sendResponse);
|
||||||
return true; // async
|
return true; // async
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (msg?.action === "GET_SCAN_PROGRESS") {
|
||||||
|
sendResponse(currentScanProgress ?? { percent: 0, phase: "idle", total: 0, current: 0, complete: false });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,6 +86,8 @@ async function handleParseRequest(url, sendResponse) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("[BACKGROUND] Creating tab for parse request:", url);
|
||||||
|
|
||||||
// Create hidden tab
|
// Create hidden tab
|
||||||
const tab = await chrome.tabs.create({
|
const tab = await chrome.tabs.create({
|
||||||
url: url,
|
url: url,
|
||||||
@@ -85,6 +95,7 @@ async function handleParseRequest(url, sendResponse) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const tabId = tab.id;
|
const tabId = tab.id;
|
||||||
|
console.log("[BACKGROUND] Tab created:", tabId);
|
||||||
|
|
||||||
// Set up timeout
|
// Set up timeout
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
@@ -104,23 +115,13 @@ async function handleParseRequest(url, sendResponse) {
|
|||||||
// Tab is fully loaded
|
// Tab is fully loaded
|
||||||
if (changeInfo.status === 'complete' && updatedTab.url) {
|
if (changeInfo.status === 'complete' && updatedTab.url) {
|
||||||
chrome.tabs.onUpdated.removeListener(checkTabLoaded);
|
chrome.tabs.onUpdated.removeListener(checkTabLoaded);
|
||||||
|
console.log("[BACKGROUND] Tab loaded, sending parse message:", updatedTab.url);
|
||||||
|
|
||||||
// Small delay to ensure DOM is ready
|
// Wait longer for content script to auto-load (if it does)
|
||||||
|
// Content scripts from manifest.json might need more time in hidden tabs
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Send parse message to content script
|
sendParseMessageWithRetry(tabId, 0);
|
||||||
chrome.tabs.sendMessage(tabId, { action: "PARSE_EBAY" })
|
}, 2000); // 2 seconds delay - give content script time to auto-load
|
||||||
.then(response => {
|
|
||||||
if (response && response.ok && response.data) {
|
|
||||||
handleParseComplete(tabId, response.data);
|
|
||||||
} else {
|
|
||||||
cleanupParseRequest(tabId, null, { ok: false, error: "Parsing failed" });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error("Error sending parse message:", err);
|
|
||||||
cleanupParseRequest(tabId, null, { ok: false, error: "Content script error" });
|
|
||||||
});
|
|
||||||
}, 1000); // 1 second delay for DOM ready
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -132,6 +133,185 @@ async function handleParseRequest(url, sendResponse) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a URL is an eBay URL
|
||||||
|
* @param {string} url - URL to check
|
||||||
|
* @returns {boolean} - true if URL is an eBay URL
|
||||||
|
*/
|
||||||
|
function isEbayUrl(url) {
|
||||||
|
if (!url) return false;
|
||||||
|
const urlLower = url.toLowerCase();
|
||||||
|
return urlLower.includes('ebay.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injects the eBay content script manually if it's not already loaded
|
||||||
|
* @param {number} tabId - Tab ID
|
||||||
|
* @returns {Promise<boolean>} - true if injection successful or already loaded
|
||||||
|
*/
|
||||||
|
async function ensureContentScriptInjected(tabId) {
|
||||||
|
try {
|
||||||
|
// First, check if tab URL is an eBay URL
|
||||||
|
const tab = await chrome.tabs.get(tabId);
|
||||||
|
|
||||||
|
if (!isEbayUrl(tab.url)) {
|
||||||
|
console.log("[BACKGROUND] Tab is not an eBay URL:", tab.url);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if content script is already loaded by sending a ping
|
||||||
|
try {
|
||||||
|
await chrome.tabs.sendMessage(tabId, { action: "PING" });
|
||||||
|
console.log("[BACKGROUND] Content script already loaded");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (pingError) {
|
||||||
|
// Content script not loaded, try to inject it manually
|
||||||
|
console.log("[BACKGROUND] Content script not found, injecting manually...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get tab info to check frames
|
||||||
|
const tabInfo = await chrome.tabs.get(tabId);
|
||||||
|
// Inject full ebay-content-script.js so PING, PARSE_EBAY and PARSE_PRODUCT_LIST are all handled.
|
||||||
|
// The previous inline injection only handled PING+PARSE_EBAY; SCAN sends PARSE_PRODUCT_LIST.
|
||||||
|
try {
|
||||||
|
await chrome.scripting.executeScript({
|
||||||
|
target: { tabId: tabId, frameIds: [0] },
|
||||||
|
files: ["ebay-content-script.js"]
|
||||||
|
});
|
||||||
|
console.log("[BACKGROUND] ebay-content-script.js injected successfully");
|
||||||
|
} catch (injectError) {
|
||||||
|
console.error("[BACKGROUND] Failed to inject ebay-content-script:", injectError);
|
||||||
|
throw injectError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also try injecting a simple test script to verify injection works
|
||||||
|
try {
|
||||||
|
await chrome.scripting.executeScript({
|
||||||
|
target: { tabId: tabId, frameIds: [0] },
|
||||||
|
func: () => {
|
||||||
|
console.log("[TEST-INJECTION] Test script executed in main frame");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (testError) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait longer for the script to fully initialize and register message listeners
|
||||||
|
// Retry PING to verify the script is ready
|
||||||
|
// Also try sending to frameId 0 explicitly
|
||||||
|
for (let pingAttempt = 0; pingAttempt < 10; pingAttempt++) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 300)); // 300ms between pings
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Try sending to main frame explicitly
|
||||||
|
try {
|
||||||
|
await chrome.tabs.sendMessage(tabId, { action: "PING" }, { frameId: 0 });
|
||||||
|
} catch (frameErr) {
|
||||||
|
// Fallback: try without frameId
|
||||||
|
await chrome.tabs.sendMessage(tabId, { action: "PING" });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[BACKGROUND] Content script ready after injection");
|
||||||
|
return true;
|
||||||
|
} catch (pingErr) {
|
||||||
|
// Continue to next attempt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Even if PING failed, return true - the script might still work
|
||||||
|
console.warn("[BACKGROUND] Content script injected but PING not responding");
|
||||||
|
return true;
|
||||||
|
} catch (injectError) {
|
||||||
|
console.error("[BACKGROUND] Failed to inject content script:", injectError);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[BACKGROUND] Error checking/injecting content script:", error);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends parse message to content script with retry mechanism
|
||||||
|
* @param {number} tabId - Tab ID
|
||||||
|
* @param {number} attempt - Current attempt number (0-based)
|
||||||
|
*/
|
||||||
|
async function sendParseMessageWithRetry(tabId, attempt) {
|
||||||
|
const maxAttempts = 3;
|
||||||
|
const retryDelay = 500; // 500ms between retries
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`[BACKGROUND] Sending parse message (attempt ${attempt + 1}/${maxAttempts}) to tab:`, tabId);
|
||||||
|
|
||||||
|
// On first attempt, ensure content script is injected
|
||||||
|
if (attempt === 0) {
|
||||||
|
const injected = await ensureContentScriptInjected(tabId);
|
||||||
|
|
||||||
|
if (!injected) {
|
||||||
|
throw new Error("Could not inject content script");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if content script is injected by trying to send a ping
|
||||||
|
// If this fails, the content script might not be loaded yet
|
||||||
|
// Try sending to main frame explicitly first
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await chrome.tabs.sendMessage(tabId, { action: "PARSE_EBAY" }, { frameId: 0 });
|
||||||
|
} catch (frameErr) {
|
||||||
|
// Fallback: try without frameId
|
||||||
|
response = await chrome.tabs.sendMessage(tabId, { action: "PARSE_EBAY" });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[BACKGROUND] Parse response received:", response);
|
||||||
|
|
||||||
|
if (response && response.ok && response.data) {
|
||||||
|
handleParseComplete(tabId, response.data);
|
||||||
|
} else {
|
||||||
|
cleanupParseRequest(tabId, null, {
|
||||||
|
ok: false,
|
||||||
|
error: response?.error || "Parsing failed"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Check if error is due to content script not being ready
|
||||||
|
const runtimeError = chrome.runtime.lastError?.message || "";
|
||||||
|
const isContentScriptError =
|
||||||
|
err.message?.includes("Could not establish connection") ||
|
||||||
|
err.message?.includes("Receiving end does not exist") ||
|
||||||
|
err.message?.includes("Could not inject content script") ||
|
||||||
|
runtimeError.includes("Receiving end does not exist") ||
|
||||||
|
runtimeError.includes("Could not establish connection");
|
||||||
|
|
||||||
|
console.log(`[BACKGROUND] Error sending parse message (attempt ${attempt + 1}):`, {
|
||||||
|
error: err.message,
|
||||||
|
runtimeError: runtimeError,
|
||||||
|
isContentScriptError: isContentScriptError
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isContentScriptError && attempt < maxAttempts) {
|
||||||
|
// Content script not ready yet, retry after delay
|
||||||
|
console.log(`[BACKGROUND] Content script not ready, retrying in ${retryDelay}ms...`);
|
||||||
|
setTimeout(() => {
|
||||||
|
sendParseMessageWithRetry(tabId, attempt + 1);
|
||||||
|
}, retryDelay);
|
||||||
|
} else {
|
||||||
|
// Max retries reached or different error
|
||||||
|
const errorMessage = isContentScriptError
|
||||||
|
? "Content script konnte nicht geladen werden. Bitte Extension neu laden."
|
||||||
|
: (err.message || runtimeError || "Content script error");
|
||||||
|
cleanupParseRequest(tabId, null, {
|
||||||
|
ok: false,
|
||||||
|
error: errorMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles parse complete response from content script
|
* Handles parse complete response from content script
|
||||||
*/
|
*/
|
||||||
@@ -174,9 +354,32 @@ async function cleanupParseRequest(tabId, data, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform account URL (e.g. /str/topmons) to listing URL with 240 items per page.
|
||||||
|
* Example: https://www.ebay.de/str/topmons?... → https://www.ebay.de/sch/i.html?_ssn=topmons&store_name=topmons&_ipg=240
|
||||||
|
*/
|
||||||
|
function transformAccountUrlTo240ItemsUrl(accountUrl) {
|
||||||
|
try {
|
||||||
|
const url = new URL(accountUrl);
|
||||||
|
const pathname = url.pathname;
|
||||||
|
|
||||||
|
const strMatch = pathname.match(/\/str\/([^\/\?]+)/);
|
||||||
|
if (!strMatch || !strMatch[1]) {
|
||||||
|
throw new Error("Could not extract account name from URL (expected /str/{name})");
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountName = strMatch[1];
|
||||||
|
const baseUrl = `${url.protocol}//${url.hostname}`;
|
||||||
|
const listingUrl = `${baseUrl}/sch/i.html?_ssn=${encodeURIComponent(accountName)}&store_name=${encodeURIComponent(accountName)}&_ipg=240`;
|
||||||
|
return listingUrl;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`Failed to transform account URL: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles eBay product scan request
|
* Handles eBay product scan request
|
||||||
* Creates a hidden tab, waits for load, sends parse message to content script
|
* Transforms account URL to listing URL with _ipg=240, creates hidden tab, parses, then loads each item in separate tab.
|
||||||
*/
|
*/
|
||||||
async function handleScanProductsRequest(url, accountId, sendResponse) {
|
async function handleScanProductsRequest(url, accountId, sendResponse) {
|
||||||
try {
|
try {
|
||||||
@@ -186,9 +389,18 @@ async function handleScanProductsRequest(url, accountId, sendResponse) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let targetUrl = url;
|
||||||
|
try {
|
||||||
|
targetUrl = transformAccountUrlTo240ItemsUrl(url);
|
||||||
|
console.log("[BACKGROUND] Scan using listing URL (240 items):", targetUrl);
|
||||||
|
} catch (transformErr) {
|
||||||
|
console.warn("[BACKGROUND] URL transform failed, using original URL:", transformErr.message);
|
||||||
|
// Fallback: use original URL (e.g. already /sch/ or non-/str/)
|
||||||
|
}
|
||||||
|
|
||||||
// Create hidden tab
|
// Create hidden tab
|
||||||
const tab = await chrome.tabs.create({
|
const tab = await chrome.tabs.create({
|
||||||
url: url,
|
url: targetUrl,
|
||||||
active: false
|
active: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -205,53 +417,17 @@ async function handleScanProductsRequest(url, accountId, sendResponse) {
|
|||||||
sendResponse: sendResponse
|
sendResponse: sendResponse
|
||||||
});
|
});
|
||||||
|
|
||||||
|
currentScanProgress = { percent: 0, phase: "listing", total: 0, current: 0, complete: false };
|
||||||
|
|
||||||
// Wait for tab to load, then send parse message
|
// Wait for tab to load, then send parse message
|
||||||
const checkTabLoaded = (updatedTabId, changeInfo, updatedTab) => {
|
const checkTabLoaded = (updatedTabId, changeInfo, updatedTab) => {
|
||||||
if (updatedTabId !== tabId) return;
|
if (updatedTabId !== tabId) return;
|
||||||
|
|
||||||
// Tab is fully loaded
|
// Tab is fully loaded
|
||||||
if (changeInfo.status === 'complete' && updatedTab.url) {
|
if (changeInfo.status === 'complete' && updatedTab.url) {
|
||||||
chrome.tabs.onUpdated.removeListener(checkTabLoaded);
|
chrome.tabs.onUpdated.removeListener(checkTabLoaded);
|
||||||
|
|
||||||
// Small delay to ensure DOM is ready
|
// Small delay to ensure DOM is ready
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Send parse message to content script
|
sendScanMessageWithRetry(tabId, 0);
|
||||||
chrome.tabs.sendMessage(tabId, { action: "PARSE_PRODUCT_LIST" })
|
|
||||||
.then(response => {
|
|
||||||
if (response && response.ok) {
|
|
||||||
// Prüfe ob items vorhanden und nicht leer
|
|
||||||
const items = response.items || response.data?.items || [];
|
|
||||||
const meta = response.meta || response.data?.meta || {};
|
|
||||||
|
|
||||||
// Log meta für Debugging
|
|
||||||
console.log("[SCAN meta]", meta);
|
|
||||||
|
|
||||||
if (items.length === 0) {
|
|
||||||
// Leere items: behandele als Fehler mit meta
|
|
||||||
cleanupScanRequest(tabId, null, {
|
|
||||||
ok: false,
|
|
||||||
error: "empty_items",
|
|
||||||
meta: meta
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Erfolg: sende items + meta
|
|
||||||
handleScanComplete(tabId, { items, meta });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fehler: sende error + meta
|
|
||||||
const meta = response?.meta || {};
|
|
||||||
console.log("[SCAN meta]", meta);
|
|
||||||
cleanupScanRequest(tabId, null, {
|
|
||||||
ok: false,
|
|
||||||
error: response?.error || "Parsing failed",
|
|
||||||
meta: meta
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error("Error sending parse message:", err);
|
|
||||||
cleanupScanRequest(tabId, null, { ok: false, error: "Content script error" });
|
|
||||||
});
|
|
||||||
}, 1000); // 1 second delay for DOM ready
|
}, 1000); // 1 second delay for DOM ready
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -264,11 +440,200 @@ async function handleScanProductsRequest(url, accountId, sendResponse) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends scan message to content script with retry mechanism
|
||||||
|
* @param {number} tabId - Tab ID
|
||||||
|
* @param {number} attempt - Current attempt number (0-based)
|
||||||
|
*/
|
||||||
|
async function sendScanMessageWithRetry(tabId, attempt) {
|
||||||
|
const maxAttempts = 3;
|
||||||
|
const retryDelay = 500; // 500ms between retries
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`[BACKGROUND] Sending scan message (attempt ${attempt + 1}/${maxAttempts}) to tab:`, tabId);
|
||||||
|
|
||||||
|
// On first attempt, ensure content script is injected
|
||||||
|
if (attempt === 0) {
|
||||||
|
const injected = await ensureContentScriptInjected(tabId);
|
||||||
|
if (!injected) {
|
||||||
|
throw new Error("Could not inject content script");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await chrome.tabs.sendMessage(tabId, { action: "PARSE_PRODUCT_LIST" });
|
||||||
|
|
||||||
|
if (response && response.ok) {
|
||||||
|
// Prüfe ob items vorhanden und nicht leer
|
||||||
|
const items = response.items || response.data?.items || [];
|
||||||
|
const meta = response.meta || response.data?.meta || {};
|
||||||
|
|
||||||
|
// Log meta für Debugging
|
||||||
|
console.log("[SCAN meta]", meta);
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
// Leere items: behandele als Fehler mit meta
|
||||||
|
cleanupScanRequest(tabId, null, {
|
||||||
|
ok: false,
|
||||||
|
error: "empty_items",
|
||||||
|
meta: meta
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (currentScanProgress) {
|
||||||
|
currentScanProgress.phase = "details";
|
||||||
|
currentScanProgress.total = items.length;
|
||||||
|
currentScanProgress.percent = 10;
|
||||||
|
}
|
||||||
|
await handleScanComplete(tabId, { items, meta });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fehler: sende error + meta
|
||||||
|
const meta = response?.meta || {};
|
||||||
|
console.log("[SCAN meta]", meta);
|
||||||
|
cleanupScanRequest(tabId, null, {
|
||||||
|
ok: false,
|
||||||
|
error: response?.error || "Parsing failed",
|
||||||
|
meta: meta
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Check if error is due to content script not being ready
|
||||||
|
const runtimeError = chrome.runtime.lastError?.message || "";
|
||||||
|
const isContentScriptError =
|
||||||
|
err.message?.includes("Could not establish connection") ||
|
||||||
|
err.message?.includes("Receiving end does not exist") ||
|
||||||
|
err.message?.includes("Could not inject content script") ||
|
||||||
|
runtimeError.includes("Receiving end does not exist") ||
|
||||||
|
runtimeError.includes("Could not establish connection");
|
||||||
|
|
||||||
|
console.log(`[BACKGROUND] Error sending scan message (attempt ${attempt + 1}):`, {
|
||||||
|
error: err.message,
|
||||||
|
runtimeError: runtimeError,
|
||||||
|
isContentScriptError: isContentScriptError
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isContentScriptError && attempt < maxAttempts) {
|
||||||
|
// Content script not ready yet, retry after delay
|
||||||
|
console.log(`[BACKGROUND] Content script not ready, retrying in ${retryDelay}ms...`);
|
||||||
|
setTimeout(() => {
|
||||||
|
sendScanMessageWithRetry(tabId, attempt + 1);
|
||||||
|
}, retryDelay);
|
||||||
|
} else {
|
||||||
|
// Max retries reached or different error
|
||||||
|
const errorMessage = isContentScriptError
|
||||||
|
? "Content script konnte nicht geladen werden. Bitte Extension neu laden."
|
||||||
|
: (err.message || runtimeError || "Content script error");
|
||||||
|
cleanupScanRequest(tabId, null, {
|
||||||
|
ok: false,
|
||||||
|
error: errorMessage,
|
||||||
|
meta: {
|
||||||
|
pageType: "unknown",
|
||||||
|
finalUrl: "",
|
||||||
|
attempts: attempt + 1,
|
||||||
|
reason: "content_script_error"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ITEM_TAB_LOAD_TIMEOUT_MS = 3000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens each item URL in a separate background tab, waits for load, parses detail page
|
||||||
|
* (title, price, currency, category, condition), merges into item, then closes tab.
|
||||||
|
*/
|
||||||
|
async function loadAndParseEachItemTab(items) {
|
||||||
|
if (!Array.isArray(items) || items.length === 0) return;
|
||||||
|
|
||||||
|
const total = items.length;
|
||||||
|
for (let i = 0; i < total; i++) {
|
||||||
|
const item = items[i];
|
||||||
|
if (!item || !item.url) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tab = await chrome.tabs.create({
|
||||||
|
url: item.url,
|
||||||
|
active: false
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const listener = (tabId, changeInfo) => {
|
||||||
|
if (tabId === tab.id && changeInfo.status === "complete") {
|
||||||
|
chrome.tabs.onUpdated.removeListener(listener);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
chrome.tabs.onUpdated.addListener(listener);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
chrome.tabs.onUpdated.removeListener(listener);
|
||||||
|
resolve();
|
||||||
|
}, ITEM_TAB_LOAD_TIMEOUT_MS);
|
||||||
|
});
|
||||||
|
|
||||||
|
const injected = await ensureContentScriptInjected(tab.id);
|
||||||
|
if (!injected) {
|
||||||
|
console.warn(`[BACKGROUND] Content script not ready in item tab ${i + 1}, skipping detail parse`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = injected
|
||||||
|
? await chrome.tabs.sendMessage(tab.id, { action: "PARSE_ITEM_DETAIL" })
|
||||||
|
: null;
|
||||||
|
if (response?.ok && response?.data) {
|
||||||
|
const d = response.data;
|
||||||
|
if (d.title != null && d.title !== "") item.title = d.title;
|
||||||
|
if (d.price != null) item.price = d.price;
|
||||||
|
if (d.currency != null && d.currency !== "") item.currency = d.currency;
|
||||||
|
if (d.category != null && d.category !== "") item.category = d.category;
|
||||||
|
if (d.condition != null && d.condition !== "") item.condition = d.condition;
|
||||||
|
if (d.quantityAvailable != null) item.quantityAvailable = d.quantityAvailable;
|
||||||
|
if (d.quantitySold != null) item.quantitySold = d.quantitySold;
|
||||||
|
if (d.watchCount != null) item.watchCount = d.watchCount;
|
||||||
|
if (d.inCartsCount != null) item.inCartsCount = d.inCartsCount;
|
||||||
|
}
|
||||||
|
} catch (parseErr) {
|
||||||
|
console.warn(`[BACKGROUND] PARSE_ITEM_DETAIL failed for ${item.url}:`, parseErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentScanProgress && total > 0) {
|
||||||
|
currentScanProgress.current = i + 1;
|
||||||
|
currentScanProgress.percent = Math.min(99, 10 + Math.round((90 * (i + 1)) / total));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await chrome.tabs.remove(tab.id);
|
||||||
|
} catch (removeErr) {
|
||||||
|
console.warn(`[BACKGROUND] Could not close item tab:`, removeErr);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[BACKGROUND] Failed to load/parse item tab ${i + 1}/${total}:`, item.url, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles scan complete response from content script
|
* Handles scan complete response from content script
|
||||||
*/
|
*/
|
||||||
function handleScanComplete(tabId, data) {
|
async function handleScanComplete(tabId, data) {
|
||||||
cleanupScanRequest(tabId, data, null);
|
const request = activeScanRequests.get(tabId);
|
||||||
|
if (request && request.timeout) {
|
||||||
|
clearTimeout(request.timeout);
|
||||||
|
request.timeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = data?.items || [];
|
||||||
|
if (items.length > 0) {
|
||||||
|
await loadAndParseEachItemTab(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentScanProgress) {
|
||||||
|
currentScanProgress.percent = 100;
|
||||||
|
currentScanProgress.complete = true;
|
||||||
|
currentScanProgress.current = items.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
await cleanupScanRequest(tabId, data, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -286,6 +651,8 @@ async function cleanupScanRequest(tabId, data, error) {
|
|||||||
// Remove from active requests
|
// Remove from active requests
|
||||||
activeScanRequests.delete(tabId);
|
activeScanRequests.delete(tabId);
|
||||||
|
|
||||||
|
currentScanProgress = null;
|
||||||
|
|
||||||
// Close tab (always, even on error)
|
// Close tab (always, even on error)
|
||||||
try {
|
try {
|
||||||
await chrome.tabs.remove(tabId);
|
await chrome.tabs.remove(tabId);
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ function setExtensionFlag() {
|
|||||||
const hasRuntime = hasChrome && chrome.runtime;
|
const hasRuntime = hasChrome && chrome.runtime;
|
||||||
const runtimeId = hasRuntime ? chrome.runtime.id : null;
|
const runtimeId = hasRuntime ? chrome.runtime.id : null;
|
||||||
if (typeof window !== 'undefined' && hasChrome && hasRuntime && runtimeId) {
|
if (typeof window !== 'undefined' && hasChrome && hasRuntime && runtimeId) {
|
||||||
|
// WICHTIG: Content Scripts haben einen isolierten Context
|
||||||
|
// window.__EBAY_EXTENSION__ wird im Content Script Context gesetzt,
|
||||||
|
// aber die Web-App läuft im Page Context - diese sind getrennt!
|
||||||
|
// Daher verlassen wir uns hauptsächlich auf postMessage
|
||||||
window.__EBAY_EXTENSION__ = true;
|
window.__EBAY_EXTENSION__ = true;
|
||||||
window.__EBAY_EXTENSION_ID__ = runtimeId; // Extension-ID für chrome.runtime.sendMessage
|
window.__EBAY_EXTENSION_ID__ = runtimeId; // Extension-ID für chrome.runtime.sendMessage
|
||||||
console.log('[ESHIP-CONTENT] window.__EBAY_EXTENSION__ set to true, ID:', runtimeId);
|
console.log('[ESHIP-CONTENT] window.__EBAY_EXTENSION__ set to true, ID:', runtimeId);
|
||||||
@@ -23,6 +27,8 @@ function setExtensionFlag() {
|
|||||||
|
|
||||||
// Versuche Flag sofort zu setzen
|
// Versuche Flag sofort zu setzen
|
||||||
console.log('[ESHIP-CONTENT] Content script loaded');
|
console.log('[ESHIP-CONTENT] Content script loaded');
|
||||||
|
console.log('[ESHIP-CONTENT] URL:', window.location.href);
|
||||||
|
console.log('[ESHIP-CONTENT] ReadyState:', document.readyState);
|
||||||
if (!setExtensionFlag()) {
|
if (!setExtensionFlag()) {
|
||||||
// Wenn window nicht verfügbar, warte auf DOMContentLoaded oder document.readyState
|
// Wenn window nicht verfügbar, warte auf DOMContentLoaded oder document.readyState
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
@@ -35,34 +41,113 @@ if (!setExtensionFlag()) {
|
|||||||
setExtensionFlag();
|
setExtensionFlag();
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
}
|
||||||
|
|
||||||
// Sende Extension-ID an Web-App via postMessage (da Content Script Isolation verhindert, dass window-Properties geteilt werden)
|
// Sende Extension-ID an Web-App via postMessage (da Content Script Isolation verhindert, dass window-Properties geteilt werden)
|
||||||
// Die Web-App kann dann die Extension-ID in ihrem eigenen Context speichern
|
// Die Web-App kann dann die Extension-ID in ihrem eigenen Context speichern
|
||||||
function sendExtensionIdToWebApp() {
|
function sendExtensionIdToWebApp() {
|
||||||
try {
|
try {
|
||||||
const runtimeId = chrome.runtime?.id;
|
const runtimeId = chrome.runtime?.id;
|
||||||
|
const hasRuntime = !!chrome.runtime;
|
||||||
|
const hasId = !!runtimeId;
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
|
||||||
if (runtimeId) {
|
if (runtimeId) {
|
||||||
// Sende Extension-ID an Web-App
|
const message = {
|
||||||
window.postMessage({
|
|
||||||
source: "eship-extension",
|
source: "eship-extension",
|
||||||
type: "EXTENSION_ID",
|
type: "EXTENSION_ID",
|
||||||
extensionId: runtimeId
|
extensionId: runtimeId
|
||||||
}, "*");
|
};
|
||||||
}
|
|
||||||
|
// Sende Extension-ID an Web-App
|
||||||
|
window.postMessage(message, "*");
|
||||||
|
|
||||||
|
} else {
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('[ESHIP-CONTENT] Error sending extension ID:', e);
|
console.error('[ESHIP-CONTENT] Error sending extension ID:', e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sende Extension-ID beim Laden
|
// Sende Extension-ID sofort beim Laden (wichtig für Seiten-Reload)
|
||||||
sendExtensionIdToWebApp();
|
sendExtensionIdToWebApp();
|
||||||
// Auch nach kurzer Verzögerung nochmal (falls Web-App noch nicht bereit ist)
|
|
||||||
|
// Sende auch nach kurzen Verzögerungen (falls Web-App noch nicht bereit ist)
|
||||||
|
setTimeout(sendExtensionIdToWebApp, 100);
|
||||||
setTimeout(sendExtensionIdToWebApp, 500);
|
setTimeout(sendExtensionIdToWebApp, 500);
|
||||||
|
setTimeout(sendExtensionIdToWebApp, 1000);
|
||||||
|
setTimeout(sendExtensionIdToWebApp, 2000);
|
||||||
|
|
||||||
|
// Kontinuierlich postMessage senden, damit die Web-App die Extension erkennen kann
|
||||||
|
// (auch wenn der User die Extension NACH dem Laden der Seite installiert)
|
||||||
|
let extensionAnnounceInterval = setInterval(() => {
|
||||||
|
sendExtensionIdToWebApp();
|
||||||
|
}, 2000); // Alle 2 Sekunden senden
|
||||||
|
|
||||||
|
// Stoppe das Intervall nach 5 Minuten (um Ressourcen zu sparen, aber lange genug für Onboarding)
|
||||||
|
setTimeout(() => {
|
||||||
|
if (extensionAnnounceInterval) {
|
||||||
|
clearInterval(extensionAnnounceInterval);
|
||||||
|
extensionAnnounceInterval = null;
|
||||||
|
}
|
||||||
|
}, 300000); // 5 Minuten
|
||||||
|
|
||||||
window.addEventListener("message", (event) => {
|
window.addEventListener("message", (event) => {
|
||||||
|
// PING_EXTENSION Handler: Web-App fragt aktiv nach Extension
|
||||||
|
// Akzeptiere sowohl "eship-webapp" als auch "eship-webapp-page-context"
|
||||||
|
if ((event.data?.source === MESSAGE_SOURCE || event.data?.source === "eship-webapp-page-context") && event.data?.type === "PING_EXTENSION") {
|
||||||
|
// Verwende console.log als Fallback, da fetch möglicherweise fehlschlägt
|
||||||
|
console.log('[ESHIP-CONTENT] Received PING_EXTENSION, responding...', event.data);
|
||||||
|
|
||||||
|
// Antworte sofort mit Extension-ID
|
||||||
|
const runtimeId = chrome.runtime?.id;
|
||||||
|
console.log('[ESHIP-CONTENT] Runtime ID:', runtimeId);
|
||||||
|
|
||||||
|
if (runtimeId) {
|
||||||
|
const response = {
|
||||||
|
source: "eship-extension",
|
||||||
|
type: "EXTENSION_ID",
|
||||||
|
extensionId: runtimeId,
|
||||||
|
requestId: event.data?.requestId
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('[ESHIP-CONTENT] Sending response:', response);
|
||||||
|
window.postMessage(response, "*");
|
||||||
|
|
||||||
|
// Setze user_extension_load auf true im Backend
|
||||||
|
// Hole JWT aus Storage und sende an Backend
|
||||||
|
chrome.storage.local.get("auth_jwt").then((data) => {
|
||||||
|
const jwt = data.auth_jwt;
|
||||||
|
if (jwt) {
|
||||||
|
// Backend-Endpoint aufrufen
|
||||||
|
fetch("http://localhost:3000/api/user/set-extension-loaded", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${jwt}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(result => {
|
||||||
|
console.log('[ESHIP-CONTENT] user_extension_load set to true:', result);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('[ESHIP-CONTENT] Failed to set user_extension_load:', err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.warn('[ESHIP-CONTENT] No JWT found, cannot set user_extension_load');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.error('[ESHIP-CONTENT] No runtime ID available!');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Sicherheitscheck: Nur Nachrichten von derselben Origin akzeptieren
|
// Sicherheitscheck: Nur Nachrichten von derselben Origin akzeptieren
|
||||||
if (event.data?.source !== MESSAGE_SOURCE) return;
|
if (event.data?.source !== MESSAGE_SOURCE && event.data?.source !== "eship-webapp-page-context") return;
|
||||||
|
|
||||||
// Auth Messages (JWT)
|
// Auth Messages (JWT)
|
||||||
if (event.data.type === "AUTH_JWT" || event.data.type === "AUTH_CLEARED") {
|
if (event.data.type === "AUTH_JWT" || event.data.type === "AUTH_CLEARED") {
|
||||||
|
|||||||
@@ -3,14 +3,50 @@
|
|||||||
* Wird auf eBay-Seiten ausgeführt und extrahiert Verkäufer-/Shop-Daten aus dem DOM
|
* Wird auf eBay-Seiten ausgeführt und extrahiert Verkäufer-/Shop-Daten aus dem DOM
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Log dass Content Script geladen wurde
|
||||||
|
console.log("[EBAY-CONTENT] Content script loaded on:", window.location.href);
|
||||||
|
|
||||||
// Message Listener für Parsing-Anfragen
|
// Message Listener für Parsing-Anfragen
|
||||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
|
// Ping-Handler für Content Script Verfügbarkeitsprüfung
|
||||||
|
if (message.action === "PING") {
|
||||||
|
sendResponse({ ok: true, ready: true });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.action === "PARSE_EBAY") {
|
if (message.action === "PARSE_EBAY") {
|
||||||
|
// Wrapper um sicherzustellen, dass immer eine Antwort gesendet wird
|
||||||
try {
|
try {
|
||||||
|
// Prüfe ob DOM bereit ist
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
// DOM noch nicht bereit, warte kurz
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
try {
|
||||||
|
const parsedData = parseEbayPage();
|
||||||
|
sendResponse({ ok: true, data: parsedData });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[EBAY-CONTENT] Error parsing:", error);
|
||||||
|
sendResponse({
|
||||||
|
ok: true,
|
||||||
|
data: {
|
||||||
|
sellerId: "",
|
||||||
|
shopName: "",
|
||||||
|
market: extractMarketFromHostname(),
|
||||||
|
status: "unknown",
|
||||||
|
stats: {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, { once: true });
|
||||||
|
return true; // async response
|
||||||
|
}
|
||||||
|
|
||||||
|
// DOM ist bereit, parse sofort
|
||||||
const parsedData = parseEbayPage();
|
const parsedData = parseEbayPage();
|
||||||
sendResponse({ ok: true, data: parsedData });
|
sendResponse({ ok: true, data: parsedData });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Niemals unhandled throws - immer graceful response
|
// Niemals unhandled throws - immer graceful response
|
||||||
|
console.error("[EBAY-CONTENT] Error in parse handler:", error);
|
||||||
sendResponse({
|
sendResponse({
|
||||||
ok: true,
|
ok: true,
|
||||||
data: {
|
data: {
|
||||||
@@ -25,6 +61,17 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|||||||
return true; // async response
|
return true; // async response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.action === "PARSE_ITEM_DETAIL") {
|
||||||
|
try {
|
||||||
|
const detail = parseItemDetailPage();
|
||||||
|
sendResponse({ ok: true, data: detail });
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[EBAY-CONTENT] PARSE_ITEM_DETAIL error:", e);
|
||||||
|
sendResponse({ ok: true, data: {} });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (message.action === "PARSE_PRODUCT_LIST") {
|
if (message.action === "PARSE_PRODUCT_LIST") {
|
||||||
// async function, need to return promise
|
// async function, need to return promise
|
||||||
parseProductList()
|
parseProductList()
|
||||||
@@ -133,6 +180,12 @@ function extractSellerId() {
|
|||||||
return strMatch[1].trim();
|
return strMatch[1].trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pattern: _ssn=username (Seller Name Parameter in Suchergebnis-URLs)
|
||||||
|
const ssnMatch = url.match(/[?&]_ssn=([^&]+)/i);
|
||||||
|
if (ssnMatch && ssnMatch[1]) {
|
||||||
|
return decodeURIComponent(ssnMatch[1]).trim();
|
||||||
|
}
|
||||||
|
|
||||||
// Methode 2: DOM-Elemente suchen
|
// Methode 2: DOM-Elemente suchen
|
||||||
// Suche nach verschiedenen Selektoren, die Seller-ID enthalten könnten
|
// Suche nach verschiedenen Selektoren, die Seller-ID enthalten könnten
|
||||||
const possibleSelectors = [
|
const possibleSelectors = [
|
||||||
@@ -633,7 +686,6 @@ async function parseProductList() {
|
|||||||
let pageType = detectPageType();
|
let pageType = detectPageType();
|
||||||
let finalUrl = window.location.href;
|
let finalUrl = window.location.href;
|
||||||
let reason = null;
|
let reason = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Schritt 1: Warte auf Item-Links (Polling)
|
// Schritt 1: Warte auf Item-Links (Polling)
|
||||||
let links = await waitForItemLinks(4000, 250, 5);
|
let links = await waitForItemLinks(4000, 250, 5);
|
||||||
@@ -718,17 +770,14 @@ async function parseProductList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return max 60 items (first page)
|
|
||||||
const result = parsedItems.slice(0, 60);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
items: result,
|
items: parsedItems,
|
||||||
meta: {
|
meta: {
|
||||||
pageType,
|
pageType,
|
||||||
finalUrl,
|
finalUrl,
|
||||||
attempts,
|
attempts,
|
||||||
reason: reason || (result.length > 0 ? "items_found" : "parsed_zero_items")
|
reason: reason || (parsedItems.length > 0 ? "items_found" : "parsed_zero_items")
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -746,66 +795,103 @@ async function parseProductList() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parst Produktdetail-Seite (/itm/...) für title, price, currency, category, condition,
|
||||||
|
* quantityAvailable, quantitySold, watchCount, inCartsCount.
|
||||||
|
*/
|
||||||
|
function parseItemDetailPage() {
|
||||||
|
const out = {
|
||||||
|
title: "",
|
||||||
|
price: null,
|
||||||
|
currency: null,
|
||||||
|
category: null,
|
||||||
|
condition: null,
|
||||||
|
quantityAvailable: null,
|
||||||
|
quantitySold: null,
|
||||||
|
watchCount: null,
|
||||||
|
inCartsCount: null
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
const url = window.location.href;
|
const titleEl = document.querySelector("[data-testid=\"x-item-title\"] h1 .ux-textspans") ||
|
||||||
const urlLower = url.toLowerCase();
|
document.querySelector("h1.x-item-title__mainTitle .ux-textspans");
|
||||||
|
if (titleEl) {
|
||||||
// Determine page type
|
out.title = (titleEl.textContent || "").trim().replace(/\s+/g, " ");
|
||||||
const isStorePage = urlLower.includes('/str/') || urlLower.includes('/store/');
|
}
|
||||||
const isSellerPage = urlLower.includes('/usr/');
|
|
||||||
|
const priceEl = document.querySelector("[data-testid=\"x-price-primary\"] .ux-textspans");
|
||||||
// Check if seller profile without items (try to find link to listings)
|
if (priceEl) {
|
||||||
if (isSellerPage && !isStorePage) {
|
const priceText = (priceEl.textContent || "").trim();
|
||||||
const itemsLink = document.querySelector('a[href*="/usr/"][href*="?items="]') ||
|
const parsed = parsePrice(priceText);
|
||||||
document.querySelector('a[href*="schid=mksr"]') ||
|
out.price = parsed.price;
|
||||||
Array.from(document.querySelectorAll('a')).find(a => {
|
out.currency = parsed.currency;
|
||||||
const text = (a.textContent || '').toLowerCase();
|
}
|
||||||
return text.includes('artikel') || text.includes('angebote') ||
|
|
||||||
text.includes('items for sale') || text.includes('see all items');
|
const breadcrumbs = document.querySelectorAll("a.seo-breadcrumb-text span");
|
||||||
});
|
if (breadcrumbs.length > 0) {
|
||||||
|
const parts = [];
|
||||||
if (!itemsLink) {
|
breadcrumbs.forEach((span) => {
|
||||||
// Try to find item cards directly
|
const t = (span.textContent || "").trim();
|
||||||
const hasItems = findItemLinks().length > 0;
|
if (t) parts.push(t);
|
||||||
if (!hasItems) {
|
});
|
||||||
throw new Error("no_items_page");
|
out.category = parts.join(" > ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const condEl = document.querySelector("dd.ux-labels-values__values .ux-textspans");
|
||||||
|
if (condEl) {
|
||||||
|
const raw = (condEl.textContent || "").trim();
|
||||||
|
const colonIdx = raw.indexOf(":");
|
||||||
|
out.condition = colonIdx >= 0 ? raw.slice(0, colonIdx).trim() : raw.slice(0, 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
// x-quantity__availability: "9 verfügbar", "8 verkauft"
|
||||||
|
const availEl = document.querySelector(".x-quantity__availability");
|
||||||
|
if (availEl) {
|
||||||
|
const text = (availEl.textContent || "").trim();
|
||||||
|
const verfMatch = text.match(/(\d+)\s*verfügbar/i);
|
||||||
|
if (verfMatch && verfMatch[1]) {
|
||||||
|
const n = parseInt(verfMatch[1], 10);
|
||||||
|
if (!isNaN(n) && n >= 0) out.quantityAvailable = n;
|
||||||
|
}
|
||||||
|
const verkMatch = text.match(/(\d+)\s*verkauft/i);
|
||||||
|
if (verkMatch && verkMatch[1]) {
|
||||||
|
const n = parseInt(verkMatch[1], 10);
|
||||||
|
if (!isNaN(n) && n >= 0) out.quantitySold = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// x-watch-heart-btn: .x-watch-heart-btn-text "14" oder aria-label "14 Beobachter"
|
||||||
|
const watchBtn = document.querySelector("button.x-watch-heart-btn");
|
||||||
|
if (watchBtn) {
|
||||||
|
const textEl = watchBtn.querySelector(".x-watch-heart-btn-text");
|
||||||
|
if (textEl) {
|
||||||
|
const n = parseInt((textEl.textContent || "").trim().replace(/\D/g, ""), 10);
|
||||||
|
if (!isNaN(n) && n >= 0) out.watchCount = n;
|
||||||
|
}
|
||||||
|
if (out.watchCount == null) {
|
||||||
|
const label = (watchBtn.getAttribute("aria-label") || "").trim();
|
||||||
|
const m = label.match(/(\d+)\s*Beobachter/i);
|
||||||
|
if (m && m[1]) {
|
||||||
|
const n = parseInt(m[1], 10);
|
||||||
|
if (!isNaN(n) && n >= 0) out.watchCount = n;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract items
|
// [data-testid="x-ebay-signal"]: "In 8 Warenkörben"
|
||||||
const items = findItemLinks();
|
const signalEl = document.querySelector("[data-testid=\"x-ebay-signal\"]");
|
||||||
|
if (signalEl) {
|
||||||
if (items.length === 0) {
|
const text = (signalEl.textContent || "").trim();
|
||||||
throw new Error("no_items_found");
|
const m = text.match(/in\s*(\d+)\s*warenkörben/i);
|
||||||
}
|
if (m && m[1]) {
|
||||||
|
const n = parseInt(m[1], 10);
|
||||||
// Parse each item
|
if (!isNaN(n) && n >= 0) out.inCartsCount = n;
|
||||||
const parsedItems = [];
|
|
||||||
const seenIds = new Set();
|
|
||||||
|
|
||||||
for (const itemLink of items) {
|
|
||||||
try {
|
|
||||||
const item = parseItemFromLink(itemLink);
|
|
||||||
|
|
||||||
// Deduplicate by platformProductId
|
|
||||||
if (item.platformProductId && !seenIds.has(item.platformProductId)) {
|
|
||||||
seenIds.add(item.platformProductId);
|
|
||||||
parsedItems.push(item);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Continue with next item if one fails
|
|
||||||
console.warn("Failed to parse item:", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
// Return max 60 items (first page)
|
console.warn("[EBAY-CONTENT] parseItemDetailPage error:", e);
|
||||||
return parsedItems.slice(0, 60);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// Re-throw to be caught by message handler
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -49,6 +49,41 @@ app.post("/api/action", async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Endpoint: Setze user_extension_load auf true
|
||||||
|
app.post("/api/user/set-extension-loaded", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const auth = req.headers.authorization || "";
|
||||||
|
const jwt = auth.startsWith("Bearer ") ? auth.slice(7) : "";
|
||||||
|
if (!jwt) return res.status(401).json({ ok: false, error: "missing token" });
|
||||||
|
|
||||||
|
// 1) User token validieren
|
||||||
|
const userClient = makeUserClient(jwt);
|
||||||
|
const account = new Account(userClient);
|
||||||
|
const user = await account.get();
|
||||||
|
|
||||||
|
// 2) User-Dokument mit Admin Key aktualisieren
|
||||||
|
const adminClient = makeAdminClient();
|
||||||
|
const db = new Databases(adminClient);
|
||||||
|
const databaseId = process.env.APPWRITE_DATABASE_ID || "eship-db";
|
||||||
|
const usersCollectionId = process.env.APPWRITE_USERS_COLLECTION_ID || "users";
|
||||||
|
|
||||||
|
// Update user document: setze user_extension_load auf true
|
||||||
|
await db.updateDocument(
|
||||||
|
databaseId,
|
||||||
|
usersCollectionId,
|
||||||
|
user.$id,
|
||||||
|
{
|
||||||
|
user_extension_load: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.json({ ok: true, userId: user.$id });
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error in /api/user/set-extension-loaded:", e);
|
||||||
|
return res.status(401).json({ ok: false, error: "unauthorized" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Backend server running on port ${PORT}`);
|
console.log(`Backend server running on port ${PORT}`);
|
||||||
});
|
});
|
||||||
|
|||||||
BIN
Server/public/extension.zip
Normal file
BIN
Server/public/extension.zip
Normal file
Binary file not shown.
@@ -18,9 +18,13 @@ import { OnboardingGate } from "./components/onboarding/OnboardingGate";
|
|||||||
import { SidebarHeader } from "./components/sidebar/SidebarHeader";
|
import { SidebarHeader } from "./components/sidebar/SidebarHeader";
|
||||||
import { useHashRoute } from "./lib/routing";
|
import { useHashRoute } from "./lib/routing";
|
||||||
import { account, databases, databaseId, usersCollectionId } from "./lib/appwrite";
|
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() {
|
export default function App() {
|
||||||
const { route, navigate } = useHashRoute();
|
const { route, navigate } = useHashRoute();
|
||||||
|
const { scanning, scanProgress } = useScan();
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [status, setStatus] = useState({ loading: true, authed: false, error: "" });
|
const [status, setStatus] = useState({ loading: true, authed: false, error: "" });
|
||||||
@@ -32,8 +36,15 @@ export default function App() {
|
|||||||
const [checkingUserDoc, setCheckingUserDoc] = useState(false);
|
const [checkingUserDoc, setCheckingUserDoc] = useState(false);
|
||||||
const [onboardingLoading, setOnboardingLoading] = useState(false);
|
const [onboardingLoading, setOnboardingLoading] = useState(false);
|
||||||
const [onboardingError, setOnboardingError] = useState("");
|
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) {
|
async function checkUserDocument(userId) {
|
||||||
if (!databases || !databaseId || !usersCollectionId) {
|
if (!databases || !databaseId || !usersCollectionId) {
|
||||||
@@ -66,13 +77,53 @@ export default function App() {
|
|||||||
|
|
||||||
// Prüfe, ob User-Dokument existiert
|
// Prüfe, ob User-Dokument existiert
|
||||||
const userDocExists = await checkUserDocument(user.$id);
|
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);
|
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();
|
await handoffJwtToExtension();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setStatus({ loading: false, authed: false, error: "" });
|
setStatus({ loading: false, authed: false, error: "" });
|
||||||
setAuthUser(null);
|
setAuthUser(null);
|
||||||
setHasUserDoc(false);
|
setHasUserDoc(false);
|
||||||
|
setUserExtensionLoad(null);
|
||||||
|
setHasAccounts(false);
|
||||||
} finally {
|
} finally {
|
||||||
setCheckingUserDoc(false);
|
setCheckingUserDoc(false);
|
||||||
}
|
}
|
||||||
@@ -94,18 +145,50 @@ export default function App() {
|
|||||||
const userDocExists = await checkUserDocument(user.$id);
|
const userDocExists = await checkUserDocument(user.$id);
|
||||||
setHasUserDoc(userDocExists);
|
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();
|
await handoffJwtToExtension();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setStatus({ loading: false, authed: false, error: "Login fehlgeschlagen" });
|
setStatus({ loading: false, authed: false, error: "Login fehlgeschlagen" });
|
||||||
setAuthUser(null);
|
setAuthUser(null);
|
||||||
setHasUserDoc(false);
|
setHasUserDoc(false);
|
||||||
|
setUserExtensionLoad(null);
|
||||||
|
setHasAccounts(false);
|
||||||
} finally {
|
} finally {
|
||||||
setCheckingUserDoc(false);
|
setCheckingUserDoc(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleOnboardingStart() {
|
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) {
|
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");
|
setOnboardingError("Fehler: Konfiguration unvollständig");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -114,38 +197,147 @@ export default function App() {
|
|||||||
setOnboardingError("");
|
setOnboardingError("");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await databases.createDocument(
|
// Prüfe zuerst, ob User-Dokument bereits existiert
|
||||||
databaseId,
|
let userDocExists = false;
|
||||||
usersCollectionId,
|
try {
|
||||||
authUser.$id, // Document-ID = Auth-User-ID
|
await databases.getDocument(databaseId, usersCollectionId, authUser.$id);
|
||||||
{
|
userDocExists = true;
|
||||||
user_name: authUser.name || "User"
|
// #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) {
|
||||||
// Erfolg: User-Dokument erstellt
|
// Dokument existiert nicht - wird erstellt
|
||||||
setHasUserDoc(true);
|
// #region agent log
|
||||||
setOnboardingError("");
|
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(()=>{});
|
||||||
} catch (e) {
|
// #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') {
|
if (!userDocExists) {
|
||||||
setHasUserDoc(true);
|
// #region agent log
|
||||||
setOnboardingError("");
|
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(()=>{});
|
||||||
} else if (e.code === 401 || e.type === 'general_unauthorized_scope') {
|
// #endregion
|
||||||
// 401 Unauthorized: Permissions nicht richtig gesetzt
|
await databases.createDocument(
|
||||||
setOnboardingError(
|
databaseId,
|
||||||
"Berechtigung verweigert. Bitte prüfe in Appwrite, ob die users Collection die richtigen Permissions hat. " +
|
usersCollectionId,
|
||||||
"Siehe setup/USERS_COLLECTION_SETUP.md für Details."
|
authUser.$id, // Document-ID = Auth-User-ID
|
||||||
);
|
{
|
||||||
} else {
|
user_name: authUser.name || "User",
|
||||||
// Andere Fehler anzeigen
|
user_extension_load: false
|
||||||
setOnboardingError(e.message || "Fehler beim Erstellen des Profils. Bitte versuche es erneut.");
|
}
|
||||||
|
);
|
||||||
|
// 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() {
|
async function logout() {
|
||||||
setStatus((s) => ({ ...s, loading: true, error: "" }));
|
setStatus((s) => ({ ...s, loading: true, error: "" }));
|
||||||
@@ -153,9 +345,11 @@ export default function App() {
|
|||||||
await account.deleteSession("current");
|
await account.deleteSession("current");
|
||||||
} catch {}
|
} catch {}
|
||||||
setStatus({ loading: false, authed: false, error: "" });
|
setStatus({ loading: false, authed: false, error: "" });
|
||||||
setAuthUser(null);
|
setAuthUser(null);
|
||||||
setHasUserDoc(false);
|
setHasUserDoc(false);
|
||||||
setOnboardingError("");
|
setUserExtensionLoad(null);
|
||||||
|
setHasAccounts(false);
|
||||||
|
setOnboardingError("");
|
||||||
|
|
||||||
// Extension informieren: Token weg
|
// Extension informieren: Token weg
|
||||||
sendToExtension({ type: "AUTH_CLEARED" });
|
sendToExtension({ type: "AUTH_CLEARED" });
|
||||||
@@ -202,6 +396,7 @@ export default function App() {
|
|||||||
icon: (
|
icon: (
|
||||||
<IconShoppingBag className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />
|
<IconShoppingBag className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />
|
||||||
),
|
),
|
||||||
|
disabled: scanning,
|
||||||
onClick: (e) => {
|
onClick: (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navigate("/accounts");
|
navigate("/accounts");
|
||||||
@@ -288,6 +483,7 @@ export default function App() {
|
|||||||
onStart={handleOnboardingStart}
|
onStart={handleOnboardingStart}
|
||||||
loading={onboardingLoading}
|
loading={onboardingLoading}
|
||||||
error={onboardingError}
|
error={onboardingError}
|
||||||
|
initialPhase={userExtensionLoad === false ? "extension" : "welcome"}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div style={{ display: showGate ? "none" : "block" }}>
|
<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",
|
"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"
|
"h-screen relative z-10"
|
||||||
)}>
|
)}>
|
||||||
<Sidebar open={sidebarOpen} setOpen={setSidebarOpen} animate={true}>
|
{!scanning && (
|
||||||
<SidebarBody className="justify-between gap-10">
|
<Sidebar open={sidebarOpen} setOpen={setSidebarOpen} animate={true}>
|
||||||
<div className="flex flex-1 flex-col overflow-x-hidden overflow-y-auto">
|
<SidebarBody className="justify-between gap-10">
|
||||||
<SidebarHeader />
|
<div className="flex flex-1 flex-col overflow-x-hidden overflow-y-auto">
|
||||||
<div className="mt-8 flex flex-col gap-2">
|
<SidebarHeader />
|
||||||
{links.map((link, idx) => (
|
<div className="mt-8 flex flex-col gap-2">
|
||||||
<SidebarLink key={idx} link={link} />
|
{links.map((link, idx) => (
|
||||||
))}
|
<SidebarLink key={idx} link={link} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
<div>
|
<LogoutButton onClick={(e) => {
|
||||||
<LogoutButton onClick={(e) => {
|
e.preventDefault();
|
||||||
e.preventDefault();
|
logout();
|
||||||
logout();
|
}} />
|
||||||
}} />
|
</div>
|
||||||
</div>
|
</SidebarBody>
|
||||||
</SidebarBody>
|
</Sidebar>
|
||||||
</Sidebar>
|
)}
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</div>
|
</div>
|
||||||
|
{scanning && (
|
||||||
|
<ScanningLoader percent={scanProgress?.percent ?? 0} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { DataTable } from "../ui/DataTable";
|
import { DataTable } from "../ui/DataTable";
|
||||||
import { Pagination } from "../ui/Pagination";
|
import { Pagination } from "../ui/Pagination";
|
||||||
import { Filters } from "../ui/Filters";
|
import { Filters } from "../ui/Filters";
|
||||||
import { cn } from "../../../lib/utils";
|
import { cn } from "../../../lib/utils";
|
||||||
import { getProductsPage, getProductPreview } from "../../../services/dashboardService";
|
import { getProductsPage, getProductPreview } from "../../../services/dashboardService";
|
||||||
import { scanProductsForAccount } from "../../../services/productsService";
|
import { scanProductsForAccount } from "../../../services/productsService";
|
||||||
|
import { getScanProgress } from "../../../services/ebayParserService";
|
||||||
|
import { useScan } from "../../../context/ScanContext";
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
|
|
||||||
|
const SCAN_POLL_MS = 300;
|
||||||
|
|
||||||
export const ProductsSection = ({ onJumpToSection, activeAccountId }) => {
|
export const ProductsSection = ({ onJumpToSection, activeAccountId }) => {
|
||||||
const [statusFilter, setStatusFilter] = useState("all");
|
const [statusFilter, setStatusFilter] = useState("all");
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
@@ -18,8 +22,9 @@ export const ProductsSection = ({ onJumpToSection, activeAccountId }) => {
|
|||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [selectedProduct, setSelectedProduct] = useState(null);
|
const [selectedProduct, setSelectedProduct] = useState(null);
|
||||||
const [previewLoading, setPreviewLoading] = useState(false);
|
const [previewLoading, setPreviewLoading] = useState(false);
|
||||||
const [scanning, setScanning] = useState(false);
|
|
||||||
const [scanToast, setScanToast] = useState({ show: false, message: "", type: "success" });
|
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
|
// Lade Products wenn activeAccountId oder Filter sich ändern
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -121,17 +126,39 @@ export const ProductsSection = ({ onJumpToSection, activeAccountId }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setScanning(true);
|
|
||||||
setError(null);
|
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 {
|
try {
|
||||||
// Führe Scan aus
|
|
||||||
const result = await scanProductsForAccount(activeAccountId);
|
const result = await scanProductsForAccount(activeAccountId);
|
||||||
|
stopPolling();
|
||||||
|
|
||||||
// Refresh Products-Liste
|
|
||||||
await loadProducts();
|
await loadProducts();
|
||||||
|
|
||||||
// Zeige Erfolgs-Toast
|
|
||||||
const updated = result.updated ?? 0;
|
const updated = result.updated ?? 0;
|
||||||
setScanToast({
|
setScanToast({
|
||||||
show: true,
|
show: true,
|
||||||
@@ -142,23 +169,45 @@ export const ProductsSection = ({ onJumpToSection, activeAccountId }) => {
|
|||||||
setScanToast({ show: false, message: "", type: "success" });
|
setScanToast({ show: false, message: "", type: "success" });
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
stopPolling();
|
||||||
console.error("Fehler beim Scannen der Produkte:", e);
|
console.error("Fehler beim Scannen der Produkte:", e);
|
||||||
|
|
||||||
// Logge meta für Dev-Debugging
|
|
||||||
const meta = e.meta || {};
|
const meta = e.meta || {};
|
||||||
if (Object.keys(meta).length > 0) {
|
if (Object.keys(meta).length > 0) {
|
||||||
console.log("[scan meta]", meta);
|
console.log("[scan meta]", meta);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfe auf no_items_found oder empty_items
|
|
||||||
const errorMsg = e.message || "Fehler beim Scannen der Produkte";
|
const errorMsg = e.message || "Fehler beim Scannen der Produkte";
|
||||||
const isNoItems = errorMsg.includes("no_items_found") || errorMsg.includes("empty_items");
|
const isNoItems = errorMsg.includes("no_items_found") || errorMsg.includes("empty_items");
|
||||||
|
const isParsingFailed = errorMsg.includes("Parsing failed");
|
||||||
// Zeige Toast mit spezifischer Meldung für 0 Items
|
|
||||||
const toastMessage = isNoItems
|
let toastMessage = errorMsg;
|
||||||
? "0 Produkte gefunden. Bitte pruefe, ob die URL auf den Shop/Artikel-Bereich des Sellers zeigt."
|
if (isNoItems) {
|
||||||
: errorMsg;
|
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({
|
setScanToast({
|
||||||
show: true,
|
show: true,
|
||||||
message: toastMessage,
|
message: toastMessage,
|
||||||
@@ -166,9 +215,13 @@ export const ProductsSection = ({ onJumpToSection, activeAccountId }) => {
|
|||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setScanToast({ show: false, message: "", type: "success" });
|
setScanToast({ show: false, message: "", type: "success" });
|
||||||
}, 3000);
|
}, 5000);
|
||||||
} finally {
|
} 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
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { open, animate } = useSidebar();
|
const { open, animate } = useSidebar();
|
||||||
|
const disabled = !!link.disabled;
|
||||||
|
const handleClick = (e) => {
|
||||||
|
if (disabled) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
link.onClick?.(e);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
href={link.href}
|
href={link.href}
|
||||||
onClick={link.onClick}
|
onClick={handleClick}
|
||||||
className={cn("flex items-center justify-start gap-2 group/sidebar py-2", className)}
|
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}>
|
{...props}>
|
||||||
{link.icon}
|
{link.icon}
|
||||||
<motion.span
|
<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 React from "react";
|
||||||
import { motion } from "motion/react";
|
import { motion } from "motion/react";
|
||||||
|
|
||||||
export default function ColourfulText({ text }) {
|
export default function ColourfulText({ text, usePurpleTheme = false }) {
|
||||||
const colors = [
|
const defaultColors = [
|
||||||
"rgb(131, 179, 32)",
|
"rgb(131, 179, 32)",
|
||||||
"rgb(47, 195, 106)",
|
"rgb(47, 195, 106)",
|
||||||
"rgb(42, 169, 210)",
|
"rgb(42, 169, 210)",
|
||||||
@@ -15,6 +15,21 @@ export default function ColourfulText({ text }) {
|
|||||||
"rgb(232, 98, 63)",
|
"rgb(232, 98, 63)",
|
||||||
"rgb(249, 129, 47)",
|
"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 [currentColors, setCurrentColors] = React.useState(colors);
|
||||||
const [count, setCount] = React.useState(0);
|
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 { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import App from './App.jsx'
|
import App from './App.jsx'
|
||||||
|
import { ScanProvider } from './context/ScanContext'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')).render(
|
createRoot(document.getElementById('root')).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<ScanProvider>
|
||||||
|
<App />
|
||||||
|
</ScanProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -101,16 +101,15 @@ export const AccountsPage = () => {
|
|||||||
|
|
||||||
// Shop-Name kann auch leer sein (optional)
|
// Shop-Name kann auch leer sein (optional)
|
||||||
updatePayload.account_shop_name = parsedData.shopName || null;
|
updatePayload.account_shop_name = parsedData.shopName || null;
|
||||||
// account_sells existiert nicht im Schema - wurde entfernt
|
updatePayload.account_sells = parsedData.stats?.itemsSold ?? null;
|
||||||
|
|
||||||
// #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:'AccountsPage.jsx:93',message:'handleRefreshAccount: update payload',data:{payload:updatePayload},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'A'})}).catch(()=>{});
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
// account_status wird weggelassen (wie beim Erstellen)
|
// account_status wird weggelassen (wie beim Erstellen)
|
||||||
// Grund: Schema-Konflikt - Enum-Feld akzeptiert weder String noch Array im Update
|
// Grund: Schema-Konflikt - Enum-Feld akzeptiert weder String noch Array im Update
|
||||||
// TODO: Schema in Appwrite prüfen und korrigieren, dann account_status wieder hinzufügen
|
// TODO: Schema in Appwrite prüfen und korrigieren, dann account_status wieder hinzufügen
|
||||||
|
|
||||||
|
// Setze account_updated_at auf aktuelle Zeit
|
||||||
|
updatePayload.account_updated_at = new Date().toISOString();
|
||||||
|
|
||||||
await updateManagedAccount(accountId, updatePayload);
|
await updateManagedAccount(accountId, updatePayload);
|
||||||
|
|
||||||
// Accounts-Liste neu laden (in-place Update)
|
// Accounts-Liste neu laden (in-place Update)
|
||||||
@@ -206,9 +205,6 @@ export const AccountsPage = () => {
|
|||||||
|
|
||||||
// Payload aus parsedData zusammenstellen
|
// Payload aus parsedData zusammenstellen
|
||||||
const accountSellsValue = parsedData.stats?.itemsSold ?? null;
|
const accountSellsValue = parsedData.stats?.itemsSold ?? null;
|
||||||
// #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:'AccountsPage.jsx:193',message:'handleFormSubmit: parsedData before save',data:{hasStats:!!parsedData.stats,itemsSold:parsedData.stats?.itemsSold,accountSellsValue},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'D'})}).catch(()=>{});
|
|
||||||
// #endregion
|
|
||||||
const newAccount = await createManagedAccount(authUser.$id, {
|
const newAccount = await createManagedAccount(authUser.$id, {
|
||||||
account_url: formData.account_url.trim(),
|
account_url: formData.account_url.trim(),
|
||||||
account_platform_account_id: parsedData.sellerId,
|
account_platform_account_id: parsedData.sellerId,
|
||||||
@@ -247,7 +243,7 @@ export const AccountsPage = () => {
|
|||||||
"Platform Account ID",
|
"Platform Account ID",
|
||||||
"Market",
|
"Market",
|
||||||
"Account URL",
|
"Account URL",
|
||||||
"Status",
|
"Sales",
|
||||||
"Last Scan",
|
"Last Scan",
|
||||||
...(showAdvanced ? ["Owner User ID"] : []),
|
...(showAdvanced ? ["Owner User ID"] : []),
|
||||||
"Action",
|
"Action",
|
||||||
@@ -310,12 +306,15 @@ export const AccountsPage = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (col === "Status") {
|
if (col === "Sales") {
|
||||||
return row.account_status || "-";
|
const sales = row.account_sells;
|
||||||
|
if (sales === null || sales === undefined) return "-";
|
||||||
|
// Format number with thousand separators
|
||||||
|
return new Intl.NumberFormat("de-DE").format(sales);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (col === "Last Scan") {
|
if (col === "Last Scan") {
|
||||||
const lastScan = row.account_last_scan_at;
|
const lastScan = row.account_updated_at;
|
||||||
if (!lastScan) return "-";
|
if (!lastScan) return "-";
|
||||||
try {
|
try {
|
||||||
const date = new Date(lastScan);
|
const date = new Date(lastScan);
|
||||||
@@ -400,9 +399,9 @@ export const AccountsPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span className="font-medium text-[var(--text)]">
|
<span className="font-medium text-[var(--text)]">
|
||||||
Status (Auto)
|
Sales (Auto)
|
||||||
</span>
|
</span>
|
||||||
: Wird automatisch auf "active" gesetzt. Du musst nichts eingeben.
|
: Anzahl der verkauften Artikel wird automatisch aus dem eBay-Profil gelesen.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 rounded-lg border border-[var(--line)] bg-white/2 p-3 text-xs text-[var(--muted)]">
|
<div className="mt-4 rounded-lg border border-[var(--line)] bg-white/2 p-3 text-xs text-[var(--muted)]">
|
||||||
@@ -587,31 +586,16 @@ export const AccountsPage = () => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1.5 block text-xs font-medium text-[var(--muted)]">
|
<label className="mb-1.5 block text-xs font-medium text-[var(--muted)]">
|
||||||
Artikel verkauft (Auto)
|
Sales (Auto)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={parsedData.stats?.itemsSold ?? "-"}
|
value={parsedData.stats?.itemsSold ? new Intl.NumberFormat("de-DE").format(parsedData.stats.itemsSold) : "-"}
|
||||||
readOnly
|
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"
|
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)]">
|
<p className="mt-1 text-xs text-[var(--muted)]">
|
||||||
Automatisch aus dem eBay-Profil gelesen.
|
Gesamtzahl der verkauften Artikel.
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label className="mb-1.5 block text-xs font-medium text-[var(--muted)]">
|
|
||||||
Status (Auto)
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={parsedData.status}
|
|
||||||
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)]">
|
|
||||||
Interner Status. Normalerweise "active".
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -200,11 +200,8 @@ export async function createManagedAccount(authUserId, accountData) {
|
|||||||
account_platform_account_id,
|
account_platform_account_id,
|
||||||
account_shop_name: accountData.account_shop_name || null,
|
account_shop_name: accountData.account_shop_name || null,
|
||||||
account_url: accountData.account_url || null,
|
account_url: accountData.account_url || null,
|
||||||
// account_sells und account_managed existieren nicht im Schema - wurden entfernt
|
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
|
// account_status ist optional - aufgrund Schema-Konflikt vorerst weglassen
|
||||||
// TODO: Schema in Appwrite prüfen und korrigieren (Enum-Feld sollte String akzeptieren, nicht Array)
|
// TODO: Schema in Appwrite prüfen und korrigieren (Enum-Feld sollte String akzeptieren, nicht Array)
|
||||||
@@ -260,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(
|
const document = await databases.updateDocument(
|
||||||
databaseId,
|
databaseId,
|
||||||
accountsCollectionId,
|
accountsCollectionId,
|
||||||
@@ -271,15 +264,9 @@ export async function updateManagedAccount(accountId, accountData) {
|
|||||||
payload
|
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;
|
return document;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Fehler beim Aktualisieren des Accounts:", 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;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,9 +129,6 @@ async function getExtensionId() {
|
|||||||
try {
|
try {
|
||||||
// Methode 1: Verwende gecachte Extension-ID (via postMessage vom Content Script empfangen)
|
// Methode 1: Verwende gecachte Extension-ID (via postMessage vom Content Script empfangen)
|
||||||
if (cachedExtensionId) {
|
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;
|
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
|
return null; // Nicht verfügbar
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
@@ -186,25 +180,30 @@ async function parseViaExtension(url) {
|
|||||||
// Check for Chrome runtime errors
|
// Check for Chrome runtime errors
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
const errorMsg = chrome.runtime.lastError.message || "Extension communication error";
|
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));
|
reject(new Error(errorMsg));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response && response.ok && response.data) {
|
if (response && response.ok && response.data) {
|
||||||
// Ensure stats object is included (even if empty)
|
// 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 = {
|
const data = {
|
||||||
sellerId: response.data.sellerId || "",
|
sellerId: sellerId,
|
||||||
shopName: response.data.shopName || "",
|
shopName: response.data.shopName || "",
|
||||||
market: response.data.market || "US",
|
market: response.data.market || "US",
|
||||||
status: response.data.status || "unknown",
|
status: response.data.status || "unknown",
|
||||||
stats: response.data.stats || {}
|
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:160',message:'parseViaExtension: response data from extension',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);
|
resolve(data);
|
||||||
} else {
|
} else {
|
||||||
reject(new Error(response?.error || "Extension parsing failed"));
|
reject(new Error(response?.error || "Extension parsing failed"));
|
||||||
@@ -213,9 +212,6 @@ async function parseViaExtension(url) {
|
|||||||
|
|
||||||
// Benötigt Extension-ID (sendMessage von Webseiten aus erfordert immer Extension-ID)
|
// Benötigt Extension-ID (sendMessage von Webseiten aus erfordert immer Extension-ID)
|
||||||
if (!extensionId) {
|
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"));
|
reject(new Error("Extension ID not available"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -250,8 +246,19 @@ async function parseViaExtension(url) {
|
|||||||
window.removeEventListener('message', responseHandler);
|
window.removeEventListener('message', responseHandler);
|
||||||
|
|
||||||
if (event.data?.ok && event.data?.data) {
|
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 = {
|
const data = {
|
||||||
sellerId: event.data.data.sellerId || "",
|
sellerId: sellerId,
|
||||||
shopName: event.data.data.shopName || "",
|
shopName: event.data.data.shopName || "",
|
||||||
market: event.data.data.market || "US",
|
market: event.data.data.market || "US",
|
||||||
status: event.data.data.status || "unknown",
|
status: event.data.data.status || "unknown",
|
||||||
@@ -315,8 +322,11 @@ export async function parseViaExtensionScanProducts(url, accountId) {
|
|||||||
accountId: accountId
|
accountId: accountId
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SCAN_TIMEOUT_MS = 300000;
|
||||||
|
|
||||||
// SendMessage-Callback
|
// SendMessage-Callback
|
||||||
const sendMessageCallback = (response) => {
|
const sendMessageCallback = (response) => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
// Check for Chrome runtime errors
|
// Check for Chrome runtime errors
|
||||||
if (chrome.runtime.lastError) {
|
if (chrome.runtime.lastError) {
|
||||||
const errorMsg = chrome.runtime.lastError.message || "Extension communication error";
|
const errorMsg = chrome.runtime.lastError.message || "Extension communication error";
|
||||||
@@ -324,11 +334,12 @@ export async function parseViaExtensionScanProducts(url, accountId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response && response.ok && response.data) {
|
console.log("[ebayParserService] SCAN_PRODUCTS response:", response);
|
||||||
const items = response.data.items || [];
|
|
||||||
const meta = response.data.meta || response.meta || {};
|
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) {
|
if (!Array.isArray(items) || items.length === 0) {
|
||||||
const errorMsg = response?.error || "no_items_found";
|
const errorMsg = response?.error || "no_items_found";
|
||||||
const error = new Error(`SCAN_PRODUCTS failed: ${errorMsg} (${meta.pageType || "unknown"})`);
|
const error = new Error(`SCAN_PRODUCTS failed: ${errorMsg} (${meta.pageType || "unknown"})`);
|
||||||
@@ -337,30 +348,27 @@ export async function parseViaExtensionScanProducts(url, accountId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erfolg: gib items zurück
|
|
||||||
resolve(items);
|
resolve(items);
|
||||||
} else {
|
} else {
|
||||||
// Fehler: sende error + meta
|
|
||||||
const meta = response?.meta || {};
|
const meta = response?.meta || {};
|
||||||
const errorMsg = response?.error || "Extension scanning failed";
|
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"})`);
|
const error = new Error(`SCAN_PRODUCTS failed: ${errorMsg} (${meta.pageType || "unknown"})`);
|
||||||
error.meta = meta;
|
error.meta = meta;
|
||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Benötigt Extension-ID (sendMessage von Webseiten aus erfordert immer Extension-ID)
|
|
||||||
if (!extensionId) {
|
if (!extensionId) {
|
||||||
reject(new Error("Extension ID not available"));
|
reject(new Error("Extension ID not available"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
chrome.runtime.sendMessage(extensionId, message, sendMessageCallback);
|
const timeoutId = setTimeout(() => {
|
||||||
|
|
||||||
// Timeout nach 20s (Extension hat intern 20s)
|
|
||||||
setTimeout(() => {
|
|
||||||
reject(new Error("Extension timeout"));
|
reject(new Error("Extension timeout"));
|
||||||
}, 20000);
|
}, SCAN_TIMEOUT_MS);
|
||||||
|
|
||||||
|
chrome.runtime.sendMessage(extensionId, message, sendMessageCallback);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Chrome API Fehler: weiter zu Methode 2
|
// Chrome API Fehler: weiter zu Methode 2
|
||||||
@@ -373,45 +381,51 @@ export async function parseViaExtensionScanProducts(url, accountId) {
|
|||||||
|
|
||||||
// Methode 2: Window flag + postMessage (falls Content Script Relay vorhanden)
|
// Methode 2: Window flag + postMessage (falls Content Script Relay vorhanden)
|
||||||
if (typeof window !== 'undefined' && window.__EBAY_EXTENSION__ === true) {
|
if (typeof window !== 'undefined' && window.__EBAY_EXTENSION__ === true) {
|
||||||
|
const SCAN_TIMEOUT_MS = 300000;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const messageId = `scan_${Date.now()}_${Math.random()}`;
|
const messageId = `scan_${Date.now()}_${Math.random()}`;
|
||||||
|
let settled = false;
|
||||||
// Listener für Antwort
|
|
||||||
|
const finish = (fn) => {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
window.removeEventListener('message', responseHandler);
|
||||||
|
fn();
|
||||||
|
};
|
||||||
|
|
||||||
const responseHandler = (event) => {
|
const responseHandler = (event) => {
|
||||||
if (event.data?.source !== 'eship-extension' || event.data?.messageId !== messageId) {
|
if (event.data?.source !== 'eship-extension' || event.data?.messageId !== messageId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.removeEventListener('message', responseHandler);
|
|
||||||
|
|
||||||
if (event.data?.ok && event.data?.data) {
|
if (event.data?.ok && event.data?.data) {
|
||||||
const items = event.data.data.items || event.data.items || [];
|
const items = event.data.data.items || event.data.items || [];
|
||||||
const meta = event.data.data.meta || event.data.meta || {};
|
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) {
|
if (!Array.isArray(items) || items.length === 0) {
|
||||||
const errorMsg = event.data?.error || "no_items_found";
|
const errorMsg = event.data?.error || "no_items_found";
|
||||||
const error = new Error(`SCAN_PRODUCTS failed: ${errorMsg} (${meta.pageType || "unknown"})`);
|
const error = new Error(`SCAN_PRODUCTS failed: ${errorMsg} (${meta.pageType || "unknown"})`);
|
||||||
error.meta = meta;
|
error.meta = meta;
|
||||||
reject(error);
|
finish(() => reject(error));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
finish(() => resolve(items));
|
||||||
// Erfolg: gib items zurück
|
|
||||||
resolve(items);
|
|
||||||
} else {
|
} else {
|
||||||
// Fehler: sende error + meta
|
|
||||||
const meta = event.data?.meta || event.data?.data?.meta || {};
|
const meta = event.data?.meta || event.data?.data?.meta || {};
|
||||||
const errorMsg = event.data?.error || "Extension scanning failed";
|
const errorMsg = event.data?.error || "Extension scanning failed";
|
||||||
const error = new Error(`SCAN_PRODUCTS failed: ${errorMsg} (${meta.pageType || "unknown"})`);
|
const error = new Error(`SCAN_PRODUCTS failed: ${errorMsg} (${meta.pageType || "unknown"})`);
|
||||||
error.meta = meta;
|
error.meta = meta;
|
||||||
reject(error);
|
finish(() => reject(error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('message', responseHandler);
|
window.addEventListener('message', responseHandler);
|
||||||
|
|
||||||
// Sende Request via postMessage
|
const timeoutId = setTimeout(() => {
|
||||||
|
finish(() => reject(new Error("Extension timeout")));
|
||||||
|
}, SCAN_TIMEOUT_MS);
|
||||||
|
|
||||||
window.postMessage({
|
window.postMessage({
|
||||||
source: 'eship-webapp',
|
source: 'eship-webapp',
|
||||||
action: 'SCAN_PRODUCTS',
|
action: 'SCAN_PRODUCTS',
|
||||||
@@ -419,12 +433,6 @@ export async function parseViaExtensionScanProducts(url, accountId) {
|
|||||||
accountId: accountId,
|
accountId: accountId,
|
||||||
messageId: messageId
|
messageId: messageId
|
||||||
}, '*');
|
}, '*');
|
||||||
|
|
||||||
// Timeout
|
|
||||||
setTimeout(() => {
|
|
||||||
window.removeEventListener('message', responseHandler);
|
|
||||||
reject(new Error("Extension timeout"));
|
|
||||||
}, 20000);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,6 +440,31 @@ export async function parseViaExtensionScanProducts(url, accountId) {
|
|||||||
throw new Error("Extension not available");
|
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)
|
* Parst eine eBay-URL mit Stub-Logik (deterministisch, keine Network-Calls)
|
||||||
* @param {string} url - eBay-Verkäuferprofil oder Shop-URL
|
* @param {string} url - eBay-Verkäuferprofil oder Shop-URL
|
||||||
@@ -457,9 +490,18 @@ async function parseViaStub(url) {
|
|||||||
const hash = stableHash(url);
|
const hash = stableHash(url);
|
||||||
const market = extractMarketFromUrl(url);
|
const market = extractMarketFromUrl(url);
|
||||||
|
|
||||||
// Seller ID: Deterministic aus URL-Hash
|
// Seller ID: Versuche zuerst aus URL zu extrahieren (_ssn Parameter)
|
||||||
// Format: "ebay_" + hash (first 10 chars)
|
let sellerId = "";
|
||||||
const sellerId = `ebay_${hash.slice(0, 10)}`;
|
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)
|
// Shop Name: Generiert aus Hash (last 4 chars als Suffix)
|
||||||
const shopNameSuffix = hash.slice(-4);
|
const shopNameSuffix = hash.slice(-4);
|
||||||
@@ -489,27 +531,14 @@ async function parseViaStub(url) {
|
|||||||
export async function parseEbayAccount(url) {
|
export async function parseEbayAccount(url) {
|
||||||
// Versuche IMMER Extension-Pfad zuerst (auch wenn Flag nicht gesetzt)
|
// Versuche IMMER Extension-Pfad zuerst (auch wenn Flag nicht gesetzt)
|
||||||
// parseViaExtension prüft selbst, ob Extension verfügbar ist
|
// 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 {
|
try {
|
||||||
// Versuche Extension zu nutzen (auch wenn Flag nicht gesetzt - parseViaExtension prüft selbst)
|
// Versuche Extension zu nutzen (auch wenn Flag nicht gesetzt - parseViaExtension prüft selbst)
|
||||||
return await parseViaExtension(url);
|
return await parseViaExtension(url);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Extension-Fehler: Fallback zu Stub
|
// Extension-Fehler: Fallback zu Stub
|
||||||
console.warn("Extension parsing failed, falling back to stub:", e.message);
|
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
|
// Fallback: Stub-Implementierung
|
||||||
const stubResult = await parseViaStub(url);
|
return 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;
|
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,46 @@ import { parseViaExtensionScanProducts } from "./ebayParserService";
|
|||||||
|
|
||||||
const productsCollectionId = import.meta.env.VITE_APPWRITE_PRODUCTS_COLLECTION_ID || "products";
|
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
|
* Lädt alle Products für einen bestimmten Account
|
||||||
* @param {string} accountId - ID des Accounts (product_account_id)
|
* @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
|
* Lädt ein einzelnes Product nach ID
|
||||||
* @param {string} productId - ID des Products
|
* @param {string} productId - ID des Products
|
||||||
@@ -118,33 +183,97 @@ export async function scanProductsForAccount(accountId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Extension aufrufen: scanne Produkte
|
// 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
|
// 3. Response-Items zu Product-Schema mappen
|
||||||
const mappedProducts = items.map(item => {
|
const mappedProducts = items.map(item => {
|
||||||
// Validiere, dass platformProductId vorhanden ist
|
|
||||||
if (!item.platformProductId) {
|
if (!item.platformProductId) {
|
||||||
console.warn("Item ohne platformProductId übersprungen:", item);
|
console.warn("Item ohne platformProductId übersprungen:", item);
|
||||||
return null;
|
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 {
|
return {
|
||||||
product_platform_product_id: item.platformProductId,
|
product_platform_product_id: item.platformProductId,
|
||||||
product_title: item.title || "",
|
product_title: item.title || "",
|
||||||
product_price: item.price ?? undefined, // undefined statt null für Appwrite
|
product_price: item.price ?? undefined,
|
||||||
product_currency: item.currency ?? undefined, // auto-fill from market if undefined
|
product_currency: item.currency ?? undefined,
|
||||||
product_url: item.url || "",
|
product_url: item.url || "",
|
||||||
product_status: item.status ?? "unknown",
|
product_status: item.status ?? "unknown",
|
||||||
product_category: item.category ?? "unknown",
|
product_category: String(item.category ?? "unknown").slice(0, 255),
|
||||||
product_condition: item.condition ?? "unknown"
|
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
|
}).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
|
// 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
|
// 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 }
|
// 6. Return { created, updated }
|
||||||
return result;
|
return result;
|
||||||
@@ -186,11 +315,11 @@ export async function upsertProductsForAccount(accountId, products) {
|
|||||||
const currency = deriveCurrencyFromMarket(market);
|
const currency = deriveCurrencyFromMarket(market);
|
||||||
const platform = "ebay"; // Enum-Werte sind lowercase gemäß Fehlermeldung: [amazon], [ebay]
|
const platform = "ebay"; // Enum-Werte sind lowercase gemäß Fehlermeldung: [amazon], [ebay]
|
||||||
|
|
||||||
// Lade bestehende Produkte für Duplikat-Prüfung
|
// Lade alle bestehenden Produkte (paginiert, Appwrite max 100/Request)
|
||||||
const existingProducts = await listProductsByAccount(accountId, {
|
const existingProducts = await fetchAllProductsByAccount(accountId);
|
||||||
limit: 1000,
|
// #region agent log
|
||||||
offset: 0,
|
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
|
// Erstelle Map von platform_product_id -> bestehendes Produkt
|
||||||
const existingProductsMap = new Map();
|
const existingProductsMap = new Map();
|
||||||
@@ -234,22 +363,43 @@ export async function upsertProductsForAccount(accountId, products) {
|
|||||||
);
|
);
|
||||||
updated++;
|
updated++;
|
||||||
} else {
|
} else {
|
||||||
// Erstelle neues Produkt
|
// Erstelle neues Produkt (product_first_fullscan_at = heutiges Datum)
|
||||||
await databases.createDocument(
|
await databases.createDocument(
|
||||||
databaseId,
|
databaseId,
|
||||||
productsCollectionId,
|
productsCollectionId,
|
||||||
ID.unique(),
|
ID.unique(),
|
||||||
fullProductData
|
{
|
||||||
|
...fullProductData,
|
||||||
|
product_first_fullscan_at: new Date().toISOString()
|
||||||
|
}
|
||||||
);
|
);
|
||||||
created++;
|
created++;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Fehler beim Upsert des Produkts "${platformProductId}":`, 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;
|
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 };
|
return { created, updated };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Fehler beim Upsert der Produkte:", e);
|
console.error("Fehler beim Upsert der Produkte:", e);
|
||||||
|
|||||||
268
setup/PRODUCTS_COLLECTION_SCHEMA.md
Normal file
268
setup/PRODUCTS_COLLECTION_SCHEMA.md
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
# Products Collection Schema
|
||||||
|
|
||||||
|
Diese Datei beschreibt alle Attribute, die in der `products` Collection benötigt werden, damit das Programm funktioniert.
|
||||||
|
|
||||||
|
## Collection: `products`
|
||||||
|
|
||||||
|
**Collection ID:** `products` (oder über `VITE_APPWRITE_PRODUCTS_COLLECTION_ID` konfiguriert)
|
||||||
|
|
||||||
|
## Erforderliche Attribute
|
||||||
|
|
||||||
|
### 1. `product_account_id` (String, Required)
|
||||||
|
- **Typ:** String
|
||||||
|
- **Required:** Ja
|
||||||
|
- **Verwendung:**
|
||||||
|
- Filterung nach Account (Query.equal)
|
||||||
|
- Verknüpfung zu Accounts-Collection
|
||||||
|
- **Beispiel:** `"account_123"`
|
||||||
|
|
||||||
|
### 2. `product_platform` (Enum, Required)
|
||||||
|
- **Typ:** Enum
|
||||||
|
- **Required:** Ja
|
||||||
|
- **Werte:** `["amazon", "ebay"]`
|
||||||
|
- **Verwendung:**
|
||||||
|
- Speichert die Plattform (aktuell nur "ebay" wird verwendet)
|
||||||
|
- **Beispiel:** `"ebay"`
|
||||||
|
|
||||||
|
### 3. `product_platform_market` (String, Required)
|
||||||
|
- **Typ:** String
|
||||||
|
- **Required:** Ja
|
||||||
|
- **Verwendung:**
|
||||||
|
- Speichert den Marktplatz (z.B. "DE", "US", "UK")
|
||||||
|
- Wird aus Account abgeleitet
|
||||||
|
- **Beispiel:** `"DE"`, `"US"`, `"UK"`
|
||||||
|
|
||||||
|
### 4. `product_platform_product_id` (String, Required, Unique)
|
||||||
|
- **Typ:** String
|
||||||
|
- **Required:** Ja
|
||||||
|
- **Unique:** Ja (für Duplikat-Prüfung)
|
||||||
|
- **Verwendung:**
|
||||||
|
- Eindeutige Produkt-ID von der Plattform (z.B. eBay Item-ID)
|
||||||
|
- Wird für Duplikat-Prüfung verwendet
|
||||||
|
- Wird für Mapping zwischen Extension und Datenbank verwendet
|
||||||
|
- **Beispiel:** `"123456789"` (eBay Item-ID)
|
||||||
|
|
||||||
|
### 5. `product_title` (String, Optional)
|
||||||
|
- **Typ:** String
|
||||||
|
- **Required:** Nein
|
||||||
|
- **Verwendung:**
|
||||||
|
- Produkttitel
|
||||||
|
- Wird für Anzeige in UI verwendet
|
||||||
|
- Wird für Suchfilter verwendet (client-side)
|
||||||
|
- **Beispiel:** `"iPhone 13 Pro Max 256GB"`
|
||||||
|
|
||||||
|
### 6. `product_price` (Float, Optional)
|
||||||
|
- **Typ:** Float
|
||||||
|
- **Required:** Nein
|
||||||
|
- **Verwendung:**
|
||||||
|
- Produktpreis
|
||||||
|
- Wird für KPI-Berechnungen verwendet (Durchschnittspreis)
|
||||||
|
- Wird für Price Spread Insights verwendet
|
||||||
|
- **Beispiel:** `99.99`
|
||||||
|
|
||||||
|
### 7. `product_currency` (String, Optional)
|
||||||
|
- **Typ:** String
|
||||||
|
- **Required:** Nein
|
||||||
|
- **Verwendung:**
|
||||||
|
- Währung (z.B. "EUR", "USD", "GBP")
|
||||||
|
- Wird aus Market abgeleitet, falls nicht vorhanden
|
||||||
|
- **Beispiel:** `"EUR"`, `"USD"`, `"GBP"`
|
||||||
|
|
||||||
|
### 8. `product_url` (String, Optional)
|
||||||
|
- **Typ:** String
|
||||||
|
- **Required:** Nein
|
||||||
|
- **Verwendung:**
|
||||||
|
- URL zum Produkt auf der Plattform
|
||||||
|
- **Beispiel:** `"https://www.ebay.de/itm/123456789"`
|
||||||
|
|
||||||
|
### 9. `product_status` (Enum, Optional)
|
||||||
|
- **Typ:** Enum
|
||||||
|
- **Required:** Nein
|
||||||
|
- **Werte:** `["active", "ended", "unknown"]`
|
||||||
|
- **Verwendung:**
|
||||||
|
- Status des Produkts
|
||||||
|
- Wird für Filterung verwendet (Overview KPIs, Products Page)
|
||||||
|
- Wird für Status-Filter in UI verwendet
|
||||||
|
- **Default:** `"unknown"`
|
||||||
|
- **Beispiel:** `"active"`, `"ended"`, `"unknown"`
|
||||||
|
|
||||||
|
### 10. `product_category` (String, Optional)
|
||||||
|
- **Typ:** String
|
||||||
|
- **Required:** Nein
|
||||||
|
- **Verwendung:**
|
||||||
|
- Produktkategorie
|
||||||
|
- Wird für Category Share Insights verwendet
|
||||||
|
- **Default:** `"unknown"`
|
||||||
|
- **Beispiel:** `"Electronics"`, `"Clothing"`
|
||||||
|
|
||||||
|
### 11. `product_condition` (String, Optional)
|
||||||
|
- **Typ:** String
|
||||||
|
- **Required:** Nein
|
||||||
|
- **Verwendung:**
|
||||||
|
- Zustand des Produkts (z.B. "New", "Used")
|
||||||
|
- **Default:** `"unknown"`
|
||||||
|
- **Beispiel:** `"New"`, `"Used"`, `"Refurbished"`
|
||||||
|
|
||||||
|
## Standard-Appwrite-Felder
|
||||||
|
|
||||||
|
Diese Felder werden automatisch von Appwrite bereitgestellt:
|
||||||
|
|
||||||
|
- **`$id`** (String, Required) - Eindeutige Dokument-ID
|
||||||
|
- **`$createdAt`** (DateTime, Required) - Erstellungsdatum (wird für Sortierung verwendet)
|
||||||
|
- **`$updatedAt`** (DateTime, Required) - Aktualisierungsdatum
|
||||||
|
|
||||||
|
## Indexes (Empfohlen)
|
||||||
|
|
||||||
|
Für bessere Performance sollten folgende Indexes erstellt werden:
|
||||||
|
|
||||||
|
1. **Index auf `product_account_id`** (für Filterung)
|
||||||
|
- Attribute: `product_account_id`
|
||||||
|
- Typ: Key
|
||||||
|
|
||||||
|
2. **Index auf `product_platform_product_id`** (für Duplikat-Prüfung)
|
||||||
|
- Attribute: `product_platform_product_id`
|
||||||
|
- Typ: Unique Key
|
||||||
|
|
||||||
|
3. **Index auf `product_account_id` + `$createdAt`** (für Sortierung)
|
||||||
|
- Attribute: `product_account_id`, `$createdAt`
|
||||||
|
- Typ: Composite
|
||||||
|
|
||||||
|
## Berechtigungen
|
||||||
|
|
||||||
|
Die Collection benötigt folgende Berechtigungen:
|
||||||
|
|
||||||
|
- **Read:** Authenticated Users
|
||||||
|
- **Create:** Authenticated Users
|
||||||
|
- **Update:** Authenticated Users
|
||||||
|
- **Delete:** Authenticated Users (optional, falls Löschen benötigt wird)
|
||||||
|
|
||||||
|
## Appwrite CLI Befehle zum Erstellen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Collection erstellen
|
||||||
|
appwrite databases createCollection \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id products \
|
||||||
|
--name "Products"
|
||||||
|
|
||||||
|
# Attribute erstellen
|
||||||
|
appwrite databases createStringAttribute \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id products \
|
||||||
|
--key product_account_id \
|
||||||
|
--required true \
|
||||||
|
--size 255
|
||||||
|
|
||||||
|
appwrite databases createEnumAttribute \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id products \
|
||||||
|
--key product_platform \
|
||||||
|
--elements amazon ebay \
|
||||||
|
--required true
|
||||||
|
|
||||||
|
appwrite databases createStringAttribute \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id products \
|
||||||
|
--key product_platform_market \
|
||||||
|
--required true \
|
||||||
|
--size 10
|
||||||
|
|
||||||
|
appwrite databases createStringAttribute \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id products \
|
||||||
|
--key product_platform_product_id \
|
||||||
|
--required true \
|
||||||
|
--size 255
|
||||||
|
|
||||||
|
appwrite databases createStringAttribute \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id products \
|
||||||
|
--key product_title \
|
||||||
|
--required false \
|
||||||
|
--size 500
|
||||||
|
|
||||||
|
appwrite databases createFloatAttribute \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id products \
|
||||||
|
--key product_price \
|
||||||
|
--required false
|
||||||
|
|
||||||
|
appwrite databases createStringAttribute \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id products \
|
||||||
|
--key product_currency \
|
||||||
|
--required false \
|
||||||
|
--size 10
|
||||||
|
|
||||||
|
appwrite databases createStringAttribute \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id products \
|
||||||
|
--key product_url \
|
||||||
|
--required false \
|
||||||
|
--size 1000
|
||||||
|
|
||||||
|
appwrite databases createEnumAttribute \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id products \
|
||||||
|
--key product_status \
|
||||||
|
--elements active ended unknown \
|
||||||
|
--required false
|
||||||
|
|
||||||
|
appwrite databases createStringAttribute \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id products \
|
||||||
|
--key product_category \
|
||||||
|
--required false \
|
||||||
|
--size 255
|
||||||
|
|
||||||
|
appwrite databases createStringAttribute \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id products \
|
||||||
|
--key product_condition \
|
||||||
|
--required false \
|
||||||
|
--size 100
|
||||||
|
|
||||||
|
# Indexes erstellen
|
||||||
|
appwrite databases createIndex \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id products \
|
||||||
|
--key idx_account_id \
|
||||||
|
--type key \
|
||||||
|
--attributes product_account_id
|
||||||
|
|
||||||
|
appwrite databases createIndex \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id products \
|
||||||
|
--key idx_platform_product_id \
|
||||||
|
--type unique \
|
||||||
|
--attributes product_platform_product_id
|
||||||
|
|
||||||
|
appwrite databases createIndex \
|
||||||
|
--database-id eship-db \
|
||||||
|
--collection-id products \
|
||||||
|
--key idx_account_created \
|
||||||
|
--type key \
|
||||||
|
--attributes product_account_id $createdAt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Zusammenfassung
|
||||||
|
|
||||||
|
**Erforderliche Attribute (Required):**
|
||||||
|
1. `product_account_id` (String)
|
||||||
|
2. `product_platform` (Enum: ["amazon", "ebay"])
|
||||||
|
3. `product_platform_market` (String)
|
||||||
|
4. `product_platform_product_id` (String, Unique)
|
||||||
|
|
||||||
|
**Optionale Attribute:**
|
||||||
|
5. `product_title` (String)
|
||||||
|
6. `product_price` (Float)
|
||||||
|
7. `product_currency` (String)
|
||||||
|
8. `product_url` (String)
|
||||||
|
9. `product_status` (Enum: ["active", "ended", "unknown"])
|
||||||
|
10. `product_category` (String)
|
||||||
|
11. `product_condition` (String)
|
||||||
|
|
||||||
|
**Standard-Felder (automatisch):**
|
||||||
|
- `$id` (String)
|
||||||
|
- `$createdAt` (DateTime)
|
||||||
|
- `$updatedAt` (DateTime)
|
||||||
78
setup/PRODUCTS_SCAN_TROUBLESHOOTING.md
Normal file
78
setup/PRODUCTS_SCAN_TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Products Scan Fehlerbehebung
|
||||||
|
|
||||||
|
## Fehler: "Parsing failed (unknown)"
|
||||||
|
|
||||||
|
Dieser Fehler kann zwei Ursachen haben:
|
||||||
|
|
||||||
|
### 1. Extension-Fehler (Parsing failed)
|
||||||
|
|
||||||
|
**Symptome:**
|
||||||
|
- Fehlermeldung: `Extension-Fehler: Parsing failed (unknown)`
|
||||||
|
- Die Extension kann keine Produkte auf der eBay-Seite finden
|
||||||
|
|
||||||
|
**Mögliche Ursachen:**
|
||||||
|
- Die Account-URL zeigt nicht auf eine Seite mit Produkt-Listings
|
||||||
|
- Die eBay-Seite hat sich geändert und die Extension-Selektoren funktionieren nicht mehr
|
||||||
|
- Die Seite ist noch nicht vollständig geladen
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
- Stelle sicher, dass die Account-URL auf eine Seite mit Produkt-Listings zeigt (z.B. `/str/` Storefront oder `/usr/` Seller Profile mit Items)
|
||||||
|
- Versuche die Extension neu zu laden
|
||||||
|
- Prüfe die Browser-Konsole für weitere Details
|
||||||
|
|
||||||
|
### 2. Datenbank-Fehler (Collection/Attribute fehlt)
|
||||||
|
|
||||||
|
**Symptome:**
|
||||||
|
- Fehlermeldung: `Datenbank-Fehler: Products-Collection existiert nicht` oder
|
||||||
|
- Fehlermeldung: `Datenbank-Fehler: Ein Attribut fehlt oder ist ungültig`
|
||||||
|
|
||||||
|
**Mögliche Ursachen:**
|
||||||
|
- Die `products` Collection wurde noch nicht erstellt
|
||||||
|
- Die Collection existiert, aber es fehlen erforderliche Attribute
|
||||||
|
- Die Berechtigungen für die Collection sind nicht korrekt konfiguriert
|
||||||
|
|
||||||
|
**Lösung:**
|
||||||
|
|
||||||
|
1. **Prüfe ob die Collection existiert:**
|
||||||
|
```bash
|
||||||
|
appwrite databases listCollections --database-id eship-db
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Erstelle die Collection falls sie fehlt:**
|
||||||
|
- Öffne die Appwrite-Konsole
|
||||||
|
- Navigiere zu Databases → eship-db → Collections
|
||||||
|
- Erstelle eine neue Collection mit ID: `products`
|
||||||
|
|
||||||
|
3. **Erstelle die erforderlichen Attribute:**
|
||||||
|
Die Collection benötigt folgende Attribute:
|
||||||
|
- `product_account_id` (string, required)
|
||||||
|
- `product_platform` (enum: ["amazon", "ebay"], required)
|
||||||
|
- `product_platform_market` (string, required)
|
||||||
|
- `product_platform_product_id` (string, required, unique)
|
||||||
|
- `product_title` (string)
|
||||||
|
- `product_price` (float)
|
||||||
|
- `product_currency` (string)
|
||||||
|
- `product_url` (string)
|
||||||
|
- `product_status` (enum: ["active", "ended", "unknown"])
|
||||||
|
- `product_category` (string)
|
||||||
|
- `product_condition` (string)
|
||||||
|
|
||||||
|
4. **Prüfe die Berechtigungen:**
|
||||||
|
- Die Collection muss Lese- und Schreibrechte für authentifizierte Benutzer haben
|
||||||
|
|
||||||
|
## Fehler: "ERR_CONNECTION_REFUSED" auf Port 7242
|
||||||
|
|
||||||
|
Dieser Fehler ist **nicht kritisch** und kann ignoriert werden. Es handelt sich um einen Debug-Logging-Versuch, der fehlschlägt, weil kein Server auf Port 7242 läuft. Dies hat keinen Einfluss auf die Funktionalität.
|
||||||
|
|
||||||
|
## Diagnose-Schritte
|
||||||
|
|
||||||
|
1. **Prüfe die Browser-Konsole** für detaillierte Fehlermeldungen
|
||||||
|
2. **Prüfe ob die Extension geladen ist:**
|
||||||
|
- Öffne `chrome://extensions/`
|
||||||
|
- Stelle sicher, dass die Extension aktiviert ist
|
||||||
|
3. **Prüfe die Datenbank-Struktur:**
|
||||||
|
- Verwende die Appwrite-Konsole oder CLI
|
||||||
|
- Stelle sicher, dass die `products` Collection existiert
|
||||||
|
4. **Teste die Account-URL:**
|
||||||
|
- Öffne die Account-URL manuell im Browser
|
||||||
|
- Stelle sicher, dass Produkt-Listings sichtbar sind
|
||||||
258
setup/appwrite_schema_upright_v1.ps1
Normal file
258
setup/appwrite_schema_upright_v1.ps1
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
# Appwrite schema bootstrap (PowerShell) fuer Appwrite Server 1.8.x
|
||||||
|
# Ziel: Alte Struktur behalten (kompatibel) + neue "upright v1" Felder integrieren.
|
||||||
|
#
|
||||||
|
# Tabellen: users, accounts, products, product_details
|
||||||
|
#
|
||||||
|
# Prereqs:
|
||||||
|
# appwrite login
|
||||||
|
# appwrite init project
|
||||||
|
#
|
||||||
|
# Run:
|
||||||
|
# pwsh .\appwrite_schema_upright_v1.ps1 -DatabaseId "YOUR_DATABASE_ID"
|
||||||
|
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$DatabaseId,
|
||||||
|
|
||||||
|
# Optional: setze auf $true, wenn du Fulltext-Indizes wirklich nutzen willst.
|
||||||
|
[bool]$EnableFulltextIndexes = $false
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# ---------------- CONFIG ----------------
|
||||||
|
$T_USERS = "users"
|
||||||
|
$T_ACCOUNTS = "accounts"
|
||||||
|
$T_PRODUCTS = "products"
|
||||||
|
$T_PRODUCT_DETAILS = "product_details"
|
||||||
|
|
||||||
|
# Minimal offene Defaults (spaeter ggf. locken)
|
||||||
|
$PERMS_ANY_CRUD = @('create(any)','read(any)','update(any)','delete(any)')
|
||||||
|
|
||||||
|
# ---------------- HELPERS ----------------
|
||||||
|
function Try-Cmd {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string[]]$Args
|
||||||
|
)
|
||||||
|
|
||||||
|
$cmd = "appwrite " + ($Args -join " ")
|
||||||
|
Write-Host ("+ " + $cmd)
|
||||||
|
|
||||||
|
try {
|
||||||
|
& appwrite @Args | Out-Host
|
||||||
|
} catch {
|
||||||
|
Write-Host (" (ignored error) " + $_.Exception.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Create-Table {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$TableId,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Name
|
||||||
|
)
|
||||||
|
|
||||||
|
# Note: Permissions für TablesDB werden anders verwaltet (optional)
|
||||||
|
# Hier erstellen wir die Tabelle ohne Permissions
|
||||||
|
$argsList = @(
|
||||||
|
"tables-db","create-table",
|
||||||
|
"--database-id",$DatabaseId,
|
||||||
|
"--table-id",$TableId,
|
||||||
|
"--name",$Name,
|
||||||
|
"--row-security","false"
|
||||||
|
)
|
||||||
|
|
||||||
|
Try-Cmd $argsList
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------- CREATE TABLES ----------------
|
||||||
|
Create-Table -TableId $T_USERS -Name "users"
|
||||||
|
Create-Table -TableId $T_ACCOUNTS -Name "accounts"
|
||||||
|
Create-Table -TableId $T_PRODUCTS -Name "products"
|
||||||
|
Create-Table -TableId $T_PRODUCT_DETAILS -Name "product_details"
|
||||||
|
|
||||||
|
# ---------------- USERS COLUMNS ----------------
|
||||||
|
# Legacy kompatibel: user_note
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_USERS,"--key","user_note","--size","255","--required","false","--array","false")
|
||||||
|
|
||||||
|
# Upright v1: created/updated timestamps (optional)
|
||||||
|
Try-Cmd @("tables-db","create-datetime-column","--database-id",$DatabaseId,"--table-id",$T_USERS,"--key","user_created_at","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-datetime-column","--database-id",$DatabaseId,"--table-id",$T_USERS,"--key","user_updated_at","--required","false","--array","false")
|
||||||
|
|
||||||
|
# Optional index: user_created_at (wenn du sorting/listing brauchst)
|
||||||
|
Try-Cmd @("tables-db","create-index","--database-id",$DatabaseId,"--table-id",$T_USERS,"--key","users_by_created_at","--type","key","--columns","user_created_at")
|
||||||
|
|
||||||
|
# ---------------- ACCOUNTS COLUMNS ----------------
|
||||||
|
# Upright v1:
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_ACCOUNTS,"--key","account_owner_user_id","--size","64","--required","false","--array","false")
|
||||||
|
|
||||||
|
# Legacy: account_managed (alte scripts)
|
||||||
|
Try-Cmd @("tables-db","create-boolean-column","--database-id",$DatabaseId,"--table-id",$T_ACCOUNTS,"--key","account_managed","--required","false","--array","false")
|
||||||
|
|
||||||
|
# Upright v1: account_team (true = managed/team, false = scanned)
|
||||||
|
Try-Cmd @("tables-db","create-boolean-column","--database-id",$DatabaseId,"--table-id",$T_ACCOUNTS,"--key","account_team","--required","true","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-enum-column","--database-id",$DatabaseId,"--table-id",$T_ACCOUNTS,"--key","account_platform","--elements",'["amazon","ebay"]',"--required","true","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_ACCOUNTS,"--key","account_platform_account_id","--size","255","--required","true","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_ACCOUNTS,"--key","account_platform_market","--size","32","--required","true","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_ACCOUNTS,"--key","account_shop_name","--size","255","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-url-column","--database-id",$DatabaseId,"--table-id",$T_ACCOUNTS,"--key","account_url","--required","false","--array","false")
|
||||||
|
|
||||||
|
# Statistik: Anzahl verkaufter Items
|
||||||
|
Try-Cmd @("tables-db","create-integer-column","--database-id",$DatabaseId,"--table-id",$T_ACCOUNTS,"--key","account_sells","--required","false","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-enum-column","--database-id",$DatabaseId,"--table-id",$T_ACCOUNTS,"--key","account_status","--elements",'["active","unknown","disabled"]',"--required","false","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-datetime-column","--database-id",$DatabaseId,"--table-id",$T_ACCOUNTS,"--key","account_created_at","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-datetime-column","--database-id",$DatabaseId,"--table-id",$T_ACCOUNTS,"--key","account_updated_at","--required","false","--array","false")
|
||||||
|
|
||||||
|
# ---------------- ACCOUNTS INDEXES ----------------
|
||||||
|
Try-Cmd @("tables-db","create-index","--database-id",$DatabaseId,"--table-id",$T_ACCOUNTS,"--key","uniq_plat_mkt_accountid","--type","unique","--columns","account_platform","account_platform_market","account_platform_account_id")
|
||||||
|
Try-Cmd @("tables-db","create-index","--database-id",$DatabaseId,"--table-id",$T_ACCOUNTS,"--key","accounts_by_owner_user","--type","key","--columns","account_owner_user_id")
|
||||||
|
|
||||||
|
# Optional: account_team index
|
||||||
|
Try-Cmd @("tables-db","create-index","--database-id",$DatabaseId,"--table-id",$T_ACCOUNTS,"--key","accounts_by_team_flag","--type","key","--columns","account_team")
|
||||||
|
|
||||||
|
# ---------------- PRODUCTS COLUMNS ----------------
|
||||||
|
# Stable basis (legacy + upright v1)
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_account_id","--size","64","--required","true","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-enum-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_platform","--elements",'["amazon","ebay"]',"--required","true","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_platform_market","--size","32","--required","true","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_platform_product_id","--size","255","--required","true","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-url-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_url","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_title","--size","1024","--required","true","--array","false")
|
||||||
|
|
||||||
|
# Legacy: product_category
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_category","--size","255","--required","false","--array","false")
|
||||||
|
|
||||||
|
# Condition/status (upright v1 erweitert unknown)
|
||||||
|
Try-Cmd @("tables-db","create-enum-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_condition","--elements",'["new","used_like_new","used_good","used_ok","parts","unknown"]',"--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-enum-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_status","--elements",'["active","ended","unknown"]',"--required","false","--array","false")
|
||||||
|
|
||||||
|
# Volatile (Sparscan) - neue Felder
|
||||||
|
Try-Cmd @("tables-db","create-float-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_price","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_currency","--size","8","--required","false","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-integer-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_quantity_available","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-integer-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_quantity_sold","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-integer-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_watch_count","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-integer-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_in_carts_count","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-datetime-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_last_seen_at","--required","false","--array","false")
|
||||||
|
|
||||||
|
# Legacy kompatibel: product_quantity (alt)
|
||||||
|
Try-Cmd @("tables-db","create-integer-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_quantity","--required","false","--array","false")
|
||||||
|
|
||||||
|
# Scan-Steuerung
|
||||||
|
Try-Cmd @("tables-db","create-datetime-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_first_fullscan_at","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-datetime-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_last_fullscan_at","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-datetime-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_last_sparscan_at","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-integer-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","product_details_version","--required","false","--array","false")
|
||||||
|
|
||||||
|
# ---------------- PRODUCTS INDEXES ----------------
|
||||||
|
Try-Cmd @("tables-db","create-index","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","products_by_account","--type","key","--columns","product_account_id")
|
||||||
|
Try-Cmd @("tables-db","create-index","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","uniq_acct_platformprod_id","--type","unique","--columns","product_account_id","product_platform_product_id")
|
||||||
|
|
||||||
|
# Optional indices
|
||||||
|
Try-Cmd @("tables-db","create-index","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","products_by_platform_market","--type","key","--columns","product_platform","product_platform_market")
|
||||||
|
Try-Cmd @("tables-db","create-index","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","products_by_status","--type","key","--columns","product_status")
|
||||||
|
|
||||||
|
if ($EnableFulltextIndexes) {
|
||||||
|
# Hinweis: Fulltext kann je nach Appwrite/CLI Build abweichen. Falls es bei dir anders heisst:
|
||||||
|
# appwrite tables-db create-index --help
|
||||||
|
Try-Cmd @("tables-db","create-index","--database-id",$DatabaseId,"--table-id",$T_PRODUCTS,"--key","products_fulltext_title","--type","fulltext","--columns","product_title")
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------- PRODUCT_DETAILS COLUMNS ----------------
|
||||||
|
# Upright v1 minimal (neu)
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_id","--size","64","--required","true","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","details_category_path","--size","2048","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","details_brand","--size","255","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","details_mpn","--size","64","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","details_gtin","--size","32","--required","false","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","details_description_text","--size","8192","--required","false","--array","false")
|
||||||
|
|
||||||
|
# Array of image urls
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","details_image_urls","--size","2048","--required","false","--array","true")
|
||||||
|
|
||||||
|
# JSON-as-string
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","details_item_specifics_json","--size","8192","--required","false","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-boolean-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","details_has_variations","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","details_variations_json","--size","8192","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-datetime-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","details_last_updated_at","--required","false","--array","false")
|
||||||
|
|
||||||
|
# Legacy detail schema (alt) - behalten fuer Kompatibilitaet
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_product_id","--size","64","--required","false","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-enum-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_platform","--elements",'["amazon","ebay"]',"--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_platform_market","--size","32","--required","false","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_gtin","--size","32","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_ean","--size","32","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_upc","--size","32","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_isbn","--size","32","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_mpn","--size","64","--required","false","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_amazon_asin","--size","32","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_ebay_epid","--size","64","--required","false","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_brand","--size","255","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_manufacturer","--size","255","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_model_name","--size","255","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_model_number","--size","255","--required","false","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_short_description","--size","2048","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_long_description","--size","8192","--required","false","--array","false")
|
||||||
|
|
||||||
|
for ($i = 1; $i -le 8; $i++) {
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key",("product_detail_bullet_" + $i),"--size","512","--required","false","--array","false")
|
||||||
|
}
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_search_terms","--size","1024","--required","false","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_color","--size","128","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_size","--size","128","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_material","--size","128","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_pattern","--size","128","--required","false","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-float-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_length","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-float-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_width","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-float-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_height","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-enum-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_dimension_unit","--elements",'["mm","cm","m","in"]',"--required","false","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-float-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_weight","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-enum-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_weight_unit","--elements",'["g","kg","oz","lb"]',"--required","false","--array","false")
|
||||||
|
|
||||||
|
Try-Cmd @("tables-db","create-string-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_country_of_origin","--size","64","--required","false","--array","false")
|
||||||
|
Try-Cmd @("tables-db","create-integer-column","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","product_detail_package_quantity","--required","false","--array","false")
|
||||||
|
|
||||||
|
# ---------------- PRODUCT_DETAILS INDEXES ----------------
|
||||||
|
# Upright v1 unique 1:1
|
||||||
|
Try-Cmd @("tables-db","create-index","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","prod_details_uniq_product_id","--type","unique","--columns","product_id")
|
||||||
|
|
||||||
|
# Legacy unique (optional, kann leer bleiben)
|
||||||
|
Try-Cmd @("tables-db","create-index","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","prod_details_legacy_prod_id","--type","unique","--columns","product_detail_product_id")
|
||||||
|
|
||||||
|
# Legacy: by platform (wenn du es brauchst)
|
||||||
|
Try-Cmd @("tables-db","create-index","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","prod_details_by_platform","--type","key","--columns","product_detail_platform")
|
||||||
|
|
||||||
|
if ($EnableFulltextIndexes) {
|
||||||
|
Try-Cmd @("tables-db","create-index","--database-id",$DatabaseId,"--table-id",$T_PRODUCT_DETAILS,"--key","prod_details_ft_description","--type","fulltext","--columns","details_description_text")
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Done. Ensured 4 tables: users, accounts, products, product_details"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "App logic reminder (upright v1):"
|
||||||
|
Write-Host "- If account_team == true => account_owner_user_id must be set"
|
||||||
|
Write-Host "- If account_team == false => account_owner_user_id must be null"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Scan rules reminder:"
|
||||||
|
Write-Host "- If account has 0 products => fullscan (products + product_details)"
|
||||||
|
Write-Host "- Else sparscan updates volatile fields in products (price/qty/watch/carts/status/last_seen)"
|
||||||
|
Write-Host "- Fullscan a product if product_details missing OR product_last_fullscan_at older than your threshold (e.g. 30 days)"
|
||||||
328
setup/create-products-collection.ps1
Normal file
328
setup/create-products-collection.ps1
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
# PowerShell Script zum Erstellen der Products Collection
|
||||||
|
# Erstellt die Collection mit allen erforderlichen Attributen
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Continue"
|
||||||
|
|
||||||
|
Write-Host "=== Products Collection Setup ===" -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Konfiguration
|
||||||
|
$DATABASE_ID = "eship-db"
|
||||||
|
$COLLECTION_ID = "products"
|
||||||
|
$COLLECTION_NAME = "Products"
|
||||||
|
|
||||||
|
# 1. Prüfe Login-Status
|
||||||
|
Write-Host "1. Prüfe Appwrite Login-Status..." -ForegroundColor Yellow
|
||||||
|
$loginCheck = appwrite databases list 2>&1
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0 -and $loginCheck -like "*Session not found*") {
|
||||||
|
Write-Host " [WARNUNG] Nicht eingeloggt. Bitte zuerst einloggen:" -ForegroundColor Red
|
||||||
|
Write-Host " appwrite login" -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Führe diesen Befehl jetzt aus..." -ForegroundColor Yellow
|
||||||
|
appwrite login
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Erstelle Collection
|
||||||
|
Write-Host "2. Erstelle Collection '$COLLECTION_ID'..." -ForegroundColor Yellow
|
||||||
|
$createCollection = appwrite databases createCollection `
|
||||||
|
--database-id $DATABASE_ID `
|
||||||
|
--collection-id $COLLECTION_ID `
|
||||||
|
--name $COLLECTION_NAME `
|
||||||
|
--document-security false `
|
||||||
|
2>&1
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host " [OK] Collection '$COLLECTION_ID' erfolgreich erstellt!" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
if ($createCollection -like "*already exists*" -or $createCollection -like "*duplicate*") {
|
||||||
|
Write-Host " [INFO] Collection '$COLLECTION_ID' existiert bereits." -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [FEHLER] Fehler beim Erstellen der Collection:" -ForegroundColor Red
|
||||||
|
Write-Host " $createCollection" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# 3. Erstelle Attribute
|
||||||
|
Write-Host "3. Erstelle Attribute..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# 3.1 product_account_id (String, Required)
|
||||||
|
Write-Host " 3.1 product_account_id..." -ForegroundColor Gray
|
||||||
|
$attr1 = appwrite databases createStringAttribute `
|
||||||
|
--database-id $DATABASE_ID `
|
||||||
|
--collection-id $COLLECTION_ID `
|
||||||
|
--key product_account_id `
|
||||||
|
--required true `
|
||||||
|
--size 255 `
|
||||||
|
2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host " [OK] product_account_id erstellt" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
if ($attr1 -like "*already exists*" -or $attr1 -like "*duplicate*") {
|
||||||
|
Write-Host " [INFO] product_account_id existiert bereits" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARNUNG] $attr1" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3.2 product_platform (Enum, Required)
|
||||||
|
Write-Host " 3.2 product_platform..." -ForegroundColor Gray
|
||||||
|
$attr2 = appwrite databases createEnumAttribute `
|
||||||
|
--database-id $DATABASE_ID `
|
||||||
|
--collection-id $COLLECTION_ID `
|
||||||
|
--key product_platform `
|
||||||
|
--elements amazon ebay `
|
||||||
|
--required true `
|
||||||
|
2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host " [OK] product_platform erstellt" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
if ($attr2 -like "*already exists*" -or $attr2 -like "*duplicate*") {
|
||||||
|
Write-Host " [INFO] product_platform existiert bereits" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARNUNG] $attr2" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3.3 product_platform_market (String, Required)
|
||||||
|
Write-Host " 3.3 product_platform_market..." -ForegroundColor Gray
|
||||||
|
$attr3 = appwrite databases createStringAttribute `
|
||||||
|
--database-id $DATABASE_ID `
|
||||||
|
--collection-id $COLLECTION_ID `
|
||||||
|
--key product_platform_market `
|
||||||
|
--required true `
|
||||||
|
--size 10 `
|
||||||
|
2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host " [OK] product_platform_market erstellt" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
if ($attr3 -like "*already exists*" -or $attr3 -like "*duplicate*") {
|
||||||
|
Write-Host " [INFO] product_platform_market existiert bereits" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARNUNG] $attr3" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3.4 product_platform_product_id (String, Required)
|
||||||
|
Write-Host " 3.4 product_platform_product_id..." -ForegroundColor Gray
|
||||||
|
$attr4 = appwrite databases createStringAttribute `
|
||||||
|
--database-id $DATABASE_ID `
|
||||||
|
--collection-id $COLLECTION_ID `
|
||||||
|
--key product_platform_product_id `
|
||||||
|
--required true `
|
||||||
|
--size 255 `
|
||||||
|
2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host " [OK] product_platform_product_id erstellt" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
if ($attr4 -like "*already exists*" -or $attr4 -like "*duplicate*") {
|
||||||
|
Write-Host " [INFO] product_platform_product_id existiert bereits" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARNUNG] $attr4" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3.5 product_title (String, Optional)
|
||||||
|
Write-Host " 3.5 product_title..." -ForegroundColor Gray
|
||||||
|
$attr5 = appwrite databases createStringAttribute `
|
||||||
|
--database-id $DATABASE_ID `
|
||||||
|
--collection-id $COLLECTION_ID `
|
||||||
|
--key product_title `
|
||||||
|
--required false `
|
||||||
|
--size 500 `
|
||||||
|
2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host " [OK] product_title erstellt" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
if ($attr5 -like "*already exists*" -or $attr5 -like "*duplicate*") {
|
||||||
|
Write-Host " [INFO] product_title existiert bereits" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARNUNG] $attr5" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3.6 product_price (Float, Optional)
|
||||||
|
Write-Host " 3.6 product_price..." -ForegroundColor Gray
|
||||||
|
$attr6 = appwrite databases createFloatAttribute `
|
||||||
|
--database-id $DATABASE_ID `
|
||||||
|
--collection-id $COLLECTION_ID `
|
||||||
|
--key product_price `
|
||||||
|
--required false `
|
||||||
|
2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host " [OK] product_price erstellt" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
if ($attr6 -like "*already exists*" -or $attr6 -like "*duplicate*") {
|
||||||
|
Write-Host " [INFO] product_price existiert bereits" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARNUNG] $attr6" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3.7 product_currency (String, Optional)
|
||||||
|
Write-Host " 3.7 product_currency..." -ForegroundColor Gray
|
||||||
|
$attr7 = appwrite databases createStringAttribute `
|
||||||
|
--database-id $DATABASE_ID `
|
||||||
|
--collection-id $COLLECTION_ID `
|
||||||
|
--key product_currency `
|
||||||
|
--required false `
|
||||||
|
--size 10 `
|
||||||
|
2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host " [OK] product_currency erstellt" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
if ($attr7 -like "*already exists*" -or $attr7 -like "*duplicate*") {
|
||||||
|
Write-Host " [INFO] product_currency existiert bereits" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARNUNG] $attr7" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3.8 product_url (String, Optional)
|
||||||
|
Write-Host " 3.8 product_url..." -ForegroundColor Gray
|
||||||
|
$attr8 = appwrite databases createStringAttribute `
|
||||||
|
--database-id $DATABASE_ID `
|
||||||
|
--collection-id $COLLECTION_ID `
|
||||||
|
--key product_url `
|
||||||
|
--required false `
|
||||||
|
--size 1000 `
|
||||||
|
2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host " [OK] product_url erstellt" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
if ($attr8 -like "*already exists*" -or $attr8 -like "*duplicate*") {
|
||||||
|
Write-Host " [INFO] product_url existiert bereits" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARNUNG] $attr8" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3.9 product_status (Enum, Optional)
|
||||||
|
Write-Host " 3.9 product_status..." -ForegroundColor Gray
|
||||||
|
$attr9 = appwrite databases createEnumAttribute `
|
||||||
|
--database-id $DATABASE_ID `
|
||||||
|
--collection-id $COLLECTION_ID `
|
||||||
|
--key product_status `
|
||||||
|
--elements active ended unknown `
|
||||||
|
--required false `
|
||||||
|
2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host " [OK] product_status erstellt" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
if ($attr9 -like "*already exists*" -or $attr9 -like "*duplicate*") {
|
||||||
|
Write-Host " [INFO] product_status existiert bereits" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARNUNG] $attr9" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3.10 product_category (String, Optional)
|
||||||
|
Write-Host " 3.10 product_category..." -ForegroundColor Gray
|
||||||
|
$attr10 = appwrite databases createStringAttribute `
|
||||||
|
--database-id $DATABASE_ID `
|
||||||
|
--collection-id $COLLECTION_ID `
|
||||||
|
--key product_category `
|
||||||
|
--required false `
|
||||||
|
--size 255 `
|
||||||
|
2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host " [OK] product_category erstellt" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
if ($attr10 -like "*already exists*" -or $attr10 -like "*duplicate*") {
|
||||||
|
Write-Host " [INFO] product_category existiert bereits" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARNUNG] $attr10" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 3.11 product_condition (String, Optional)
|
||||||
|
Write-Host " 3.11 product_condition..." -ForegroundColor Gray
|
||||||
|
$attr11 = appwrite databases createStringAttribute `
|
||||||
|
--database-id $DATABASE_ID `
|
||||||
|
--collection-id $COLLECTION_ID `
|
||||||
|
--key product_condition `
|
||||||
|
--required false `
|
||||||
|
--size 100 `
|
||||||
|
2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host " [OK] product_condition erstellt" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
if ($attr11 -like "*already exists*" -or $attr11 -like "*duplicate*") {
|
||||||
|
Write-Host " [INFO] product_condition existiert bereits" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARNUNG] $attr11" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "4. Erstelle Indexes..." -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# 4.1 Index auf product_account_id
|
||||||
|
Write-Host " 4.1 Index auf product_account_id..." -ForegroundColor Gray
|
||||||
|
$idx1 = appwrite databases createIndex `
|
||||||
|
--database-id $DATABASE_ID `
|
||||||
|
--collection-id $COLLECTION_ID `
|
||||||
|
--key idx_account_id `
|
||||||
|
--type key `
|
||||||
|
--attributes product_account_id `
|
||||||
|
2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host " [OK] Index idx_account_id erstellt" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
if ($idx1 -like "*already exists*" -or $idx1 -like "*duplicate*") {
|
||||||
|
Write-Host " [INFO] Index idx_account_id existiert bereits" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARNUNG] $idx1" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4.2 Unique Index auf product_platform_product_id
|
||||||
|
Write-Host " 4.2 Unique Index auf product_platform_product_id..." -ForegroundColor Gray
|
||||||
|
$idx2 = appwrite databases createIndex `
|
||||||
|
--database-id $DATABASE_ID `
|
||||||
|
--collection-id $COLLECTION_ID `
|
||||||
|
--key idx_platform_product_id `
|
||||||
|
--type unique `
|
||||||
|
--attributes product_platform_product_id `
|
||||||
|
2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host " [OK] Index idx_platform_product_id erstellt" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
if ($idx2 -like "*already exists*" -or $idx2 -like "*duplicate*") {
|
||||||
|
Write-Host " [INFO] Index idx_platform_product_id existiert bereits" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARNUNG] $idx2" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4.3 Composite Index auf product_account_id + $createdAt
|
||||||
|
Write-Host " 4.3 Composite Index auf product_account_id + `$createdAt..." -ForegroundColor Gray
|
||||||
|
$idx3 = appwrite databases createIndex `
|
||||||
|
--database-id $DATABASE_ID `
|
||||||
|
--collection-id $COLLECTION_ID `
|
||||||
|
--key idx_account_created `
|
||||||
|
--type key `
|
||||||
|
--attributes product_account_id `$createdAt `
|
||||||
|
2>&1
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Host " [OK] Index idx_account_created erstellt" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
if ($idx3 -like "*already exists*" -or $idx3 -like "*duplicate*") {
|
||||||
|
Write-Host " [INFO] Index idx_account_created existiert bereits" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
Write-Host " [WARNUNG] $idx3" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== Setup abgeschlossen ===" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Nächste Schritte:" -ForegroundColor Cyan
|
||||||
|
Write-Host "1. Prüfe die Berechtigungen in der Appwrite-Konsole" -ForegroundColor White
|
||||||
|
Write-Host "2. Stelle sicher, dass authentifizierte Benutzer Read/Write-Rechte haben" -ForegroundColor White
|
||||||
|
Write-Host "3. Teste die Collection mit einem Produkt-Scan" -ForegroundColor White
|
||||||
|
Write-Host ""
|
||||||
Reference in New Issue
Block a user