Update Extension files and add deploy script

This commit is contained in:
2026-01-18 17:31:18 +01:00
parent ed9a75a1dc
commit 86d2191a25
4 changed files with 664 additions and 16 deletions

View File

@@ -52,3 +52,9 @@
{"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":"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":"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":"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"}

View File

@@ -218,10 +218,34 @@ async function handleScanProductsRequest(url, accountId, sendResponse) {
// Send parse message to content script // Send parse message to content script
chrome.tabs.sendMessage(tabId, { action: "PARSE_PRODUCT_LIST" }) chrome.tabs.sendMessage(tabId, { action: "PARSE_PRODUCT_LIST" })
.then(response => { .then(response => {
if (response && response.ok && response.data) { if (response && response.ok) {
handleScanComplete(tabId, response.data); // 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 { } else {
cleanupScanRequest(tabId, null, { ok: false, error: response?.error || "Parsing failed" }); // 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 => { .catch(err => {
@@ -273,8 +297,10 @@ async function cleanupScanRequest(tabId, data, error) {
// Send response // Send response
if (request.sendResponse) { if (request.sendResponse) {
if (error) { if (error) {
// error kann bereits meta enthalten
request.sendResponse(error); request.sendResponse(error);
} else if (data) { } else if (data) {
// data kann items + meta enthalten
request.sendResponse({ ok: true, data: data }); request.sendResponse({ ok: true, data: data });
} else { } else {
request.sendResponse({ ok: false, error: "Unknown error" }); request.sendResponse({ ok: false, error: "Unknown error" });

View File

@@ -26,17 +26,39 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
} }
if (message.action === "PARSE_PRODUCT_LIST") { if (message.action === "PARSE_PRODUCT_LIST") {
try { // async function, need to return promise
const items = parseProductList(); parseProductList()
sendResponse({ ok: true, data: { items } }); .then(result => {
} catch (error) { // result hat bereits die Struktur { ok, items?, error?, meta }
// Niemals unhandled throws - immer graceful response if (result.ok) {
console.error("Error parsing product list:", error); sendResponse({
sendResponse({ ok: true,
ok: false, items: result.items || [],
error: error.message || "Failed to parse product list" meta: result.meta || {}
});
} else {
sendResponse({
ok: false,
error: result.error || "Failed to parse product list",
meta: result.meta || {}
});
}
})
.catch(error => {
// Niemals unhandled throws - immer graceful response
console.error("Error parsing product list:", error);
const pageType = detectPageType();
sendResponse({
ok: false,
error: error.message || "Failed to parse product list",
meta: {
pageType,
finalUrl: window.location.href,
attempts: 1,
reason: "unhandled_error"
}
});
}); });
}
return true; // async response return true; // async response
} }
}); });
@@ -471,10 +493,259 @@ function parseEbayPage() {
} }
/** /**
* Parst Produktliste von eBay Storefront oder Seller Listings * Erkennt den Seitentyp basierend auf URL und DOM
* @returns {Array} Array von Produkt-Items * @returns {string} Page Type: "storefront" | "seller_profile" | "feedback" | "search_results" | "unknown"
*/ */
function parseProductList() { function detectPageType() {
try {
const pathname = window.location.pathname.toLowerCase();
const search = window.location.search.toLowerCase();
// Storefront
if (pathname.includes("/str/")) {
return "storefront";
}
// Seller Profile
if (pathname.includes("/usr/")) {
return "seller_profile";
}
// Feedback
if (search.includes("_tab=feedback") || pathname.includes("feedback")) {
return "feedback";
}
// Search Results
if (document.querySelector("ul.srp-results")) {
return "search_results";
}
return "unknown";
} catch (e) {
return "unknown";
}
}
/**
* Wartet auf Item-Links, bis minLinks gefunden wurden oder Timeout
* @param {number} maxMs - Maximale Wartezeit in ms (default: 4000)
* @param {number} intervalMs - Intervall zwischen Checks in ms (default: 250)
* @param {number} minLinks - Minimale Anzahl Links (default: 5)
* @returns {Promise<Array>} Array von Link-Elementen (kann 0 sein)
*/
async function waitForItemLinks(maxMs = 4000, intervalMs = 250, minLinks = 5) {
const startTime = Date.now();
while (Date.now() - startTime < maxMs) {
const links = findItemLinks();
if (links.length >= minLinks) {
return links;
}
// Warte auf nächsten Check
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
// Timeout: gib gefundene Links zurück (kann 0 sein)
return findItemLinks();
}
/**
* Findet Navigations-Link zu Items-Seite (Angebote/Artikel)
* @returns {string|null} Absolute URL oder null
*/
function findItemsNavigationLink() {
try {
const allLinks = Array.from(document.querySelectorAll("a"));
const candidates = [];
for (const link of allLinks) {
const href = link.href || link.getAttribute("href");
const text = (link.textContent || "").toLowerCase().trim();
if (!href) continue;
const hrefLower = href.toLowerCase();
// Prüfe Anchor-Text
const textMatches =
text.includes("artikel") ||
text.includes("angebote") ||
text.includes("items") ||
text.includes("items for sale") ||
text.includes("shop") ||
text.includes("shop anzeigen");
// Prüfe href-Patterns
const hrefMatches =
hrefLower.includes("/sch/") ||
hrefLower.includes("/s/i.html") ||
hrefLower.includes("/str/") ||
hrefLower.includes("?_tab=selling");
if (textMatches || hrefMatches) {
// Berechne Priorität
let priority = 0;
if (hrefLower.includes("/sch/")) priority = 1;
else if (hrefLower.includes("/str/")) priority = 2;
else if (hrefLower.includes("/s/i.html")) priority = 3;
else if (text.includes("items")) priority = 4;
else priority = 5;
candidates.push({ link, href, priority });
}
}
if (candidates.length === 0) {
return null;
}
// Sortiere nach Priorität (niedrigste Zahl = höchste Priorität)
candidates.sort((a, b) => a.priority - b.priority);
const bestCandidate = candidates[0].href;
const currentUrl = window.location.href;
// Prüfe ob URL sich unterscheidet
if (bestCandidate === currentUrl) {
return null; // Gleiche URL, keine Navigation nötig
}
// Erstelle absolute URL falls nötig
try {
const url = new URL(bestCandidate, window.location.origin);
return url.href;
} catch (e) {
return null;
}
} catch (e) {
return null;
}
}
/**
* Parst Produktliste von eBay Storefront oder Seller Listings
* @returns {Promise<{ok: boolean, items?: Array, error?: string, meta: object}>}
*/
async function parseProductList() {
let attempts = 1;
let pageType = detectPageType();
let finalUrl = window.location.href;
let reason = null;
try {
// Schritt 1: Warte auf Item-Links (Polling)
let links = await waitForItemLinks(4000, 250, 5);
// Schritt 2: Wenn keine Links gefunden, versuche Auto-Navigation (max. 1 Retry)
if (links.length === 0 && attempts === 1) {
const itemsLink = findItemsNavigationLink();
if (itemsLink && itemsLink !== window.location.href) {
// Navigiere zur Items-Seite
window.location.href = itemsLink;
// Warte auf DOM-Ready nach Navigation
await new Promise(resolve => setTimeout(resolve, 1200));
// Warte erneut auf Item-Links (längeres Timeout nach Navigation)
links = await waitForItemLinks(5000, 250, 5);
attempts = 2;
pageType = detectPageType();
finalUrl = window.location.href;
reason = "navigated_to_items_page";
// Wenn immer noch keine Links, gib Fehler zurück
if (links.length === 0) {
return {
ok: false,
error: "no_items_found",
meta: {
pageType,
finalUrl,
attempts,
reason: "no_items_found_after_navigation"
}
};
}
} else {
// Kein Navigations-Link gefunden
return {
ok: false,
error: "no_items_found",
meta: {
pageType,
finalUrl,
attempts,
reason: links.length === 0 ? "no_items_links_on_page" : "timed_out_waiting_for_items"
}
};
}
}
// Schritt 3: Parse Items
if (links.length === 0) {
return {
ok: false,
error: "no_items_found",
meta: {
pageType,
finalUrl,
attempts,
reason: reason || "no_items_links_on_page"
}
};
}
// Parse each item
const parsedItems = [];
const seenIds = new Set();
for (const itemLink of links) {
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);
}
}
// Return max 60 items (first page)
const result = parsedItems.slice(0, 60);
return {
ok: true,
items: result,
meta: {
pageType,
finalUrl,
attempts,
reason: reason || (result.length > 0 ? "items_found" : "parsed_zero_items")
}
};
} catch (error) {
// Graceful error handling
return {
ok: false,
error: error.message || "Failed to parse product list",
meta: {
pageType,
finalUrl,
attempts,
reason: "parse_error"
}
};
}
}
try { try {
const url = window.location.href; const url = window.location.href;
const urlLower = url.toLowerCase(); const urlLower = url.toLowerCase();

View File

@@ -0,0 +1,345 @@
<#
Deploy Upright DB Schema for Appwrite 1.8.1 (TablesDB)
- Writes appwrite.config.json
- Runs: appwrite push tables --force
Usage example:
.\deploy-upright-schema.ps1 `
-ProjectId "YOUR_PROJECT_ID" `
-Endpoint "https://YOUR_HOST_OR_REGION/v1" `
-DatabaseId "upright" `
-DatabaseName "upright"
#>
param(
[Parameter(Mandatory=$true)][string]$ProjectId,
[Parameter(Mandatory=$true)][string]$Endpoint,
[Parameter(Mandatory=$true)][string]$DatabaseId,
[Parameter(Mandatory=$false)][string]$DatabaseName = "upright"
)
$ErrorActionPreference = "Stop"
function New-IndexObject {
param(
[Parameter(Mandatory=$true)][string]$Key,
[Parameter(Mandatory=$true)][ValidateSet("key","unique","fulltext")][string]$Type,
[Parameter(Mandatory=$true)][string[]]$Attributes
)
return @{
key = $Key
type = $Type
attributes = $Attributes
}
}
function New-ColumnString {
param(
[Parameter(Mandatory=$true)][string]$Key,
[int]$Size = 255,
[bool]$Required = $false,
[bool]$Array = $false,
$Default = $null
)
return @{
key = $Key
type = "string"
status = "available"
error = ""
required = $Required
array = $Array
size = $Size
default = $Default
}
}
function New-ColumnUrl {
param(
[Parameter(Mandatory=$true)][string]$Key,
[int]$Size = 2048,
[bool]$Required = $false,
[bool]$Array = $false,
$Default = $null
)
# In config, url is usually type "string" with format "url"
return @{
key = $Key
type = "string"
format = "url"
status = "available"
error = ""
required = $Required
array = $Array
size = $Size
default = $Default
}
}
function New-ColumnDatetime {
param(
[Parameter(Mandatory=$true)][string]$Key,
[bool]$Required = $false,
[bool]$Array = $false,
$Default = $null
)
return @{
key = $Key
type = "datetime"
status = "available"
error = ""
required = $Required
array = $Array
default = $Default
}
}
function New-ColumnBoolean {
param(
[Parameter(Mandatory=$true)][string]$Key,
[bool]$Required = $false,
[bool]$Array = $false,
$Default = $null
)
return @{
key = $Key
type = "boolean"
status = "available"
error = ""
required = $Required
array = $Array
default = $Default
}
}
function New-ColumnInteger {
param(
[Parameter(Mandatory=$true)][string]$Key,
[bool]$Required = $false,
[bool]$Array = $false,
$Default = $null
)
return @{
key = $Key
type = "integer"
status = "available"
error = ""
required = $Required
array = $Array
default = $Default
}
}
function New-ColumnFloat {
param(
[Parameter(Mandatory=$true)][string]$Key,
[bool]$Required = $false,
[bool]$Array = $false,
$Default = $null
)
return @{
key = $Key
type = "float"
status = "available"
error = ""
required = $Required
array = $Array
default = $Default
}
}
function New-ColumnEnumString {
param(
[Parameter(Mandatory=$true)][string]$Key,
[Parameter(Mandatory=$true)][string[]]$Elements,
[int]$Size = 50,
[bool]$Required = $false,
[bool]$Array = $false,
$Default = $null
)
# Enum in Appwrite config is typically string with format "enum" and "elements"
return @{
key = $Key
type = "string"
format = "enum"
elements = $Elements
status = "available"
error = ""
required = $Required
array = $Array
size = $Size
default = $Default
}
}
# Tables
$usersTableId = "users"
$accountsTableId = "accounts"
$productsTableId = "products"
$productDetailsTableId = "product_details"
$tables = @()
# users
$tables += @{
'$id' = $usersTableId
'$permissions' = @(
'create("any")','read("any")','update("any")','delete("any")'
)
databaseId = $DatabaseId
name = "users"
enabled = $true
rowSecurity = $false
columns = @(
New-ColumnDatetime -Key "user_created_at"
New-ColumnDatetime -Key "user_updated_at"
)
indexes = @()
}
# accounts
$tables += @{
'$id' = $accountsTableId
'$permissions' = @(
'create("any")','read("any")','update("any")','delete("any")'
)
databaseId = $DatabaseId
name = "accounts"
enabled = $true
rowSecurity = $false
columns = @(
New-ColumnString -Key "account_owner_user_id" -Size 128 -Required $false
New-ColumnBoolean -Key "account_team" -Required $true -Default $false
New-ColumnEnumString -Key "account_platform" -Elements @("amazon","ebay") -Size 20 -Required $true
New-ColumnString -Key "account_platform_account_id" -Size 128 -Required $true
New-ColumnString -Key "account_platform_market" -Size 50 -Required $true
New-ColumnString -Key "account_shop_name" -Size 256 -Required $false
New-ColumnUrl -Key "account_url" -Size 2048 -Required $false
New-ColumnEnumString -Key "account_status" -Elements @("active","unknown","disabled") -Size 20 -Required $false
New-ColumnDatetime -Key "account_created_at"
New-ColumnDatetime -Key "account_updated_at"
)
indexes = @(
(New-IndexObject -Key "uniq_platform_market_accountid" -Type "unique" -Attributes @(
"account_platform","account_platform_market","account_platform_account_id"
)),
(New-IndexObject -Key "idx_owner_user" -Type "key" -Attributes @("account_owner_user_id")),
(New-IndexObject -Key "idx_team" -Type "key" -Attributes @("account_team"))
)
}
# products (sparscan + vollscan in einem)
$tables += @{
'$id' = $productsTableId
'$permissions' = @(
'create("any")','read("any")','update("any")','delete("any")'
)
databaseId = $DatabaseId
name = "products"
enabled = $true
rowSecurity = $false
columns = @(
New-ColumnString -Key "product_account_id" -Size 36 -Required $true
New-ColumnEnumString -Key "product_platform" -Elements @("amazon","ebay") -Size 20 -Required $true
New-ColumnString -Key "product_platform_market" -Size 50 -Required $true
New-ColumnString -Key "product_platform_product_id" -Size 128 -Required $true
New-ColumnUrl -Key "product_url" -Size 2048 -Required $true
New-ColumnString -Key "product_title" -Size 512 -Required $false
New-ColumnEnumString -Key "product_condition" -Elements @(
"new","used_like_new","used_good","used_ok","parts","unknown"
) -Size 30 -Required $false -Default "unknown"
New-ColumnEnumString -Key "product_status" -Elements @(
"active","ended","unknown"
) -Size 20 -Required $false -Default "unknown"
# volatile (sparscan)
New-ColumnFloat -Key "product_price" -Required $false
New-ColumnString -Key "product_currency" -Size 10 -Required $false
New-ColumnInteger -Key "product_quantity_available" -Required $false
New-ColumnInteger -Key "product_quantity_sold" -Required $false
New-ColumnInteger -Key "product_watch_count" -Required $false
New-ColumnInteger -Key "product_in_carts_count" -Required $false
New-ColumnDatetime -Key "product_last_seen_at" -Required $false
# scan bookkeeping
New-ColumnDatetime -Key "product_first_fullscan_at" -Required $false
New-ColumnDatetime -Key "product_last_fullscan_at" -Required $false
New-ColumnDatetime -Key "product_last_sparscan_at" -Required $false
New-ColumnInteger -Key "product_details_version" -Required $false
)
indexes = @(
(New-IndexObject -Key "idx_product_account" -Type "key" -Attributes @("product_account_id")),
(New-IndexObject -Key "uniq_account_platform_product" -Type "unique" -Attributes @(
"product_account_id","product_platform_product_id"
)),
(New-IndexObject -Key "idx_platform_market" -Type "key" -Attributes @(
"product_platform","product_platform_market"
)),
(New-IndexObject -Key "idx_status" -Type "key" -Attributes @("product_status")),
(New-IndexObject -Key "ft_title" -Type "fulltext" -Attributes @("product_title"))
)
}
# product_details (schwer)
# WICHTIG: Alle Attribute verwenden "product_detail_" Präfix (Singular) nicht "details_"
$tables += @{
'$id' = $productDetailsTableId
'$permissions' = @(
'create("any")','read("any")','update("any")','delete("any")'
)
databaseId = $DatabaseId
name = "product_details"
enabled = $true
rowSecurity = $false
columns = @(
New-ColumnString -Key "product_id" -Size 36 -Required $true
New-ColumnString -Key "product_detail_category_path" -Size 512 -Required $false
New-ColumnString -Key "product_detail_brand" -Size 128 -Required $false
New-ColumnString -Key "product_detail_mpn" -Size 128 -Required $false
New-ColumnString -Key "product_detail_gtin" -Size 128 -Required $false
New-ColumnString -Key "product_detail_description_text" -Size 20000 -Required $false
New-ColumnUrl -Key "product_detail_image_urls" -Size 2048 -Required $false -Array $true
New-ColumnString -Key "product_detail_item_specifics_json" -Size 20000 -Required $false
New-ColumnBoolean -Key "product_detail_has_variations" -Required $false
New-ColumnString -Key "product_detail_variations_json" -Size 20000 -Required $false
New-ColumnDatetime -Key "product_detail_last_updated_at" -Required $false
)
indexes = @(
(New-IndexObject -Key "uniq_product_id" -Type "unique" -Attributes @("product_id"))
)
}
# appwrite.config.json root
$config = @{
projectId = $ProjectId
endpoint = $Endpoint
tablesDB = @(
@{
'$id' = $DatabaseId
name = $DatabaseName
enabled = $true
}
)
tables = $tables
}
$configPath = Join-Path -Path (Get-Location) -ChildPath "appwrite.config.json"
($config | ConvertTo-Json -Depth 20) | Set-Content -Path $configPath -Encoding UTF8
Write-Host "Wrote: $configPath"
Write-Host "Now pushing schema via: appwrite push tables --force"
# Push schema (requires: appwrite login, appwrite init project)
& appwrite push tables --force