297cb9b146
Add local Claude permissions file; make background.js call syncToDashboard() after each run when config.syncToDashboard is enabled; improve content extraction in content.js by cloning link nodes, removing svg/style elements and stripping review-related text/counts before trimming; update config.py defaults to enable target and gamestop (remove bestbuy), set HEADLESS=True and STEALTH_HEADLESS=True to favor headless environments.
137 lines
4.4 KiB
JavaScript
137 lines
4.4 KiB
JavaScript
// Pokemon Stock Monitor - Content Script
|
|
// Runs on PokemonCenter pages to extract product data and intercept APIs
|
|
|
|
(function() {
|
|
console.log("[Pokemon Monitor] Content script loaded on:", window.location.href);
|
|
|
|
// Inject the API interceptor into the page context
|
|
function injectApiInterceptor() {
|
|
const script = document.createElement('script');
|
|
script.src = chrome.runtime.getURL('api-interceptor.js');
|
|
script.onload = () => script.remove();
|
|
(document.head || document.documentElement).appendChild(script);
|
|
}
|
|
|
|
// Listen for messages from the injected interceptor
|
|
window.addEventListener('message', (event) => {
|
|
if (event.source !== window) return;
|
|
if (event.data?.source !== 'pokemon-api-interceptor') return;
|
|
|
|
// Forward API data to background script
|
|
chrome.runtime.sendMessage({
|
|
type: 'apiData',
|
|
data: event.data.data
|
|
}).catch(() => {});
|
|
});
|
|
|
|
// Inject the interceptor
|
|
injectApiInterceptor();
|
|
|
|
// Listen for messages from background script
|
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
if (message.type === "extractProducts") {
|
|
console.log("[Pokemon Monitor] Extracting products...");
|
|
const products = extractProducts();
|
|
console.log("[Pokemon Monitor] Found", products.length, "products");
|
|
sendResponse(products);
|
|
}
|
|
return true;
|
|
});
|
|
|
|
function extractProducts() {
|
|
const products = [];
|
|
|
|
// Group all product links by URL
|
|
const urlToLinks = {};
|
|
document.querySelectorAll('a[href*="/product/"]').forEach(link => {
|
|
const url = link.href;
|
|
if (!urlToLinks[url]) {
|
|
urlToLinks[url] = [];
|
|
}
|
|
urlToLinks[url].push(link);
|
|
});
|
|
|
|
console.log("[Pokemon Monitor] Found", Object.keys(urlToLinks).length, "unique product URLs");
|
|
|
|
// Process each unique product URL
|
|
for (const [url, links] of Object.entries(urlToLinks)) {
|
|
// Find the best name from all links to this product
|
|
let bestName = "";
|
|
let isSoldOut = false;
|
|
|
|
for (const link of links) {
|
|
const clone = link.cloneNode(true);
|
|
clone.querySelectorAll('svg, style').forEach(el => el.remove());
|
|
const text = clone.textContent.trim();
|
|
|
|
// Check if any link says "SOLD OUT"
|
|
if (text.toUpperCase().includes("SOLD OUT")) {
|
|
isSoldOut = true;
|
|
}
|
|
|
|
// Pick the longest non-"SOLD OUT" text as the name
|
|
if (text.length > bestName.length &&
|
|
!text.toUpperCase().startsWith("SOLD") &&
|
|
text.length > 10) {
|
|
bestName = text;
|
|
}
|
|
}
|
|
|
|
// Skip if we couldn't find a good name
|
|
if (!bestName || bestName.length < 10) continue;
|
|
|
|
// Clean up name - remove price if embedded
|
|
bestName = bestName.replace(/\$[\d,.]+/g, "").trim();
|
|
// Remove "Add to Cart" etc
|
|
bestName = bestName.replace(/Add to Cart/gi, "").trim();
|
|
// Remove review star CSS/SVG junk and plain review counts
|
|
bestName = bestName.replace(/\s*(review-|\d*\s*reviews?).*$/i, "").trim();
|
|
// Clean whitespace
|
|
bestName = bestName.replace(/\s+/g, " ").trim();
|
|
|
|
// Find price from any of the links' containers
|
|
let price = null;
|
|
for (const link of links) {
|
|
let parent = link.parentElement;
|
|
for (let i = 0; i < 6 && parent && !price; i++) {
|
|
const priceMatch = parent.textContent.match(/\$[\d,]+\.?\d*/);
|
|
if (priceMatch) {
|
|
price = priceMatch[0];
|
|
break;
|
|
}
|
|
parent = parent.parentElement;
|
|
}
|
|
if (price) break;
|
|
}
|
|
|
|
// Find image from any of the links
|
|
let imageUrl = null;
|
|
for (const link of links) {
|
|
const img = link.querySelector("img") ||
|
|
link.closest("[class*='product']")?.querySelector("img");
|
|
if (img) {
|
|
imageUrl = img.src || img.dataset.src;
|
|
if (imageUrl && !imageUrl.startsWith("data:")) break;
|
|
}
|
|
}
|
|
|
|
// Extract product ID from URL
|
|
const idMatch = url.match(/\/product\/([^\/]+)/);
|
|
const productId = idMatch ? idMatch[1] : url;
|
|
|
|
products.push({
|
|
name: bestName,
|
|
url: url,
|
|
price: price,
|
|
inStock: !isSoldOut,
|
|
imageUrl: imageUrl,
|
|
productId: productId,
|
|
site: "pokemoncenter"
|
|
});
|
|
}
|
|
|
|
console.log("[Pokemon Monitor] Extracted products:", products.map(p => ({name: p.name.slice(0,40), inStock: p.inStock})));
|
|
return products;
|
|
}
|
|
})();
|