Files
2026-04-10 18:30:04 -04:00

1619 lines
54 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Pokemon Stock Monitor Dashboard - Frontend JavaScript
const API_BASE = '/api';
// State
let currentPage = 'dashboard';
let charts = {};
let currentEventFilter = null; // { type: 'new_drop'|'restock', period: 'today'|'week' }
let currentProductPage = 1;
const PRODUCTS_PER_PAGE = 50;
// Initialize
document.addEventListener('DOMContentLoaded', () => {
initNavigation();
loadDashboard();
loadSuggestions();
initEventListeners();
// Auto-refresh dashboard every 30 seconds
setInterval(() => {
if (currentPage === 'dashboard') {
loadDashboard();
}
}, 30000);
});
// Navigation
function initNavigation() {
document.querySelectorAll('.nav-links a').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const page = e.target.dataset.page;
navigateTo(page);
});
});
}
function navigateTo(page) {
// Update active nav
document.querySelectorAll('.nav-links a').forEach(a => a.classList.remove('active'));
document.querySelector(`[data-page="${page}"]`).classList.add('active');
// Show page
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
document.getElementById(`page-${page}`).classList.add('active');
currentPage = page;
// Load page content
switch (page) {
case 'dashboard':
loadDashboard();
break;
case 'products':
loadProducts();
break;
case 'news':
loadNews();
break;
case 'analytics':
loadAnalytics();
break;
case 'favorites':
loadFavorites();
break;
case 'control':
loadControlPanel();
break;
case 'users':
loadUsers();
break;
case 'settings':
loadSettings();
break;
}
}
// Event Listeners
function initEventListeners() {
// Product filters - clear event filter when user changes regular filters
['filterSite', 'filterCategory', 'filterStock', 'filterFavorites'].forEach(id => {
const el = document.getElementById(id);
if (el) {
el.addEventListener('change', () => {
currentEventFilter = null; // Clear event filter when using regular filters
currentProductPage = 1;
loadProducts();
});
}
});
// Clear filters button
document.getElementById('clearFiltersBtn')?.addEventListener('click', clearAllFilters);
// Add favorite button
document.getElementById('addFavoriteBtn')?.addEventListener('click', addFavorite);
// Modal close
document.querySelector('.modal-close')?.addEventListener('click', closeModal);
document.getElementById('productModal')?.addEventListener('click', (e) => {
if (e.target.id === 'productModal') closeModal();
});
}
// Clear all product filters
function clearAllFilters() {
document.getElementById('filterSite').value = '';
document.getElementById('filterCategory').value = '';
document.getElementById('filterStock').value = '';
document.getElementById('filterFavorites').checked = false;
currentEventFilter = null;
currentProductPage = 1;
loadProducts();
}
// API Helpers
async function api(endpoint, options = {}) {
try {
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
return await response.json();
} catch (error) {
console.error('API Error:', error);
return null;
}
}
// Store display names and branding
const STORE_INFO = {
pokemoncenter: {
name: 'Pokemon Center',
logo: 'https://www.google.com/s2/favicons?domain=pokemoncenter.com&sz=128',
fallback: 'PC',
color: '#ffcb05',
bgColor: '#1d1d42'
},
target: {
name: 'Target',
logo: 'https://www.google.com/s2/favicons?domain=target.com&sz=128',
fallback: '◎',
color: '#cc0000',
bgColor: '#ffffff'
},
walmart: {
name: 'Walmart',
logo: 'https://www.google.com/s2/favicons?domain=walmart.com&sz=128',
fallback: '✱',
color: '#0071ce',
bgColor: '#ffffff'
},
bestbuy: {
name: 'Best Buy',
logo: 'https://www.google.com/s2/favicons?domain=bestbuy.com&sz=128',
fallback: 'BB',
color: '#fff200',
bgColor: '#0046be'
},
gamestop: {
name: 'GameStop',
logo: 'https://www.google.com/s2/favicons?domain=gamestop.com&sz=128',
fallback: 'GS',
color: '#ff0000',
bgColor: '#000000'
}
};
// All stores to show (even without data)
const ALL_STORES = ['pokemoncenter', 'target', 'gamestop', 'walmart', 'bestbuy'];
// Dashboard
async function loadDashboard() {
const stats = await api('/stats');
if (!stats) return;
// Update stats cards
document.getElementById('newDropsToday').textContent = stats.new_drops_today || 0;
document.getElementById('restocksToday').textContent = stats.restocks_today || 0;
document.getElementById('newDropsWeek').textContent = stats.new_drops_week || 0;
document.getElementById('restocksWeek').textContent = stats.restocks_week || 0;
// Update store cards
loadStoreCards(stats.sites || []);
// Update status
const statusDot = document.getElementById('monitorStatus');
const statusText = document.getElementById('statusText');
if (stats.last_check) {
statusDot.classList.add('active');
const lastCheck = new Date(stats.last_check.checked_at);
statusText.textContent = `Last check: ${formatTime(lastCheck)}`;
} else {
statusDot.classList.remove('active');
statusText.textContent = 'No checks yet';
}
// Load activity feed
loadActivityFeed();
}
function loadStoreCards(sites) {
const container = document.getElementById('storeCards');
// Create a map of site data for quick lookup
const siteDataMap = {};
(sites || []).forEach(s => {
siteDataMap[s.site] = s;
});
// Show all stores, even without data
container.innerHTML = ALL_STORES.map(storeKey => {
const info = STORE_INFO[storeKey];
const data = siteDataMap[storeKey] || { total_products: 0, in_stock_count: 0 };
const outOfStock = data.total_products - data.in_stock_count;
const hasData = data.total_products > 0;
return `
<div class="store-card ${hasData ? '' : 'no-data'}" onclick="goToStoreProducts('${storeKey}')">
<div class="store-logo-container" style="background: ${info.bgColor};">
<img src="${info.logo}" alt="${info.name}" class="store-logo-img"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<span class="store-icon-fallback" style="display:none; color: ${info.color};">${info.fallback}</span>
</div>
<div class="store-info">
<div class="store-name">${info.name}</div>
<div class="store-stats">
${hasData ? `
<span class="store-stat in-stock">
<span>&#9679;</span> ${data.in_stock_count} in stock
</span>
<span class="store-stat out-of-stock">
<span>&#9675;</span> ${outOfStock} sold out
</span>
` : `
<span class="store-stat no-data">Not tracking yet</span>
`}
</div>
</div>
<div class="store-arrow">&#8250;</div>
</div>
`;
}).join('');
}
function goToStoreProducts(store) {
// Navigate to products page and filter by store
navigateTo('products');
document.getElementById('filterSite').value = store;
loadProducts();
}
async function loadActivityFeed() {
const data = await api('/events?limit=20');
if (!data || !data.events) return;
const feed = document.getElementById('activityFeed');
if (data.events.length === 0) {
feed.innerHTML = '<div class="loading">No recent activity</div>';
return;
}
feed.innerHTML = data.events.map(event => {
const storeInfo = STORE_INFO[event.site] || { name: event.site, color: '#333', bgColor: '#666', logo: '', fallback: '?' };
const badgeClass = event.event_type === 'restock' ? 'restock' : 'new-drop';
const badgeText = event.event_type === 'restock' ? 'Restock' : 'New';
const hasProductImage = event.image_url && !event.image_url.includes('data:') && event.image_url.length > 10;
// Build store logo HTML with fallback
const storeLogoHtml = `
<div class="activity-store-icon" style="background:${storeInfo.bgColor};">
<img src="${storeInfo.logo}" alt="${storeInfo.name}" class="activity-store-logo"
onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
<span class="activity-store-fallback" style="display:none; color:${storeInfo.color};">${storeInfo.fallback}</span>
</div>
`;
return `
<div class="activity-item">
${hasProductImage
? `<img src="${event.image_url}" class="activity-thumb" alt="" onerror="handleActivityImageError(this, '${event.site}')">`
: storeLogoHtml
}
<div class="activity-details">
<div class="activity-name">
<a href="${event.url}" target="_blank">${cleanProductName(event.name)}</a>
<span class="activity-badge ${badgeClass}">${badgeText}</span>
</div>
<div class="activity-meta">${storeInfo.name} &bull; ${event.current_price || 'N/A'}</div>
</div>
<div class="activity-time">${formatTime(new Date(event.recorded_at))}</div>
</div>
`;
}).join('');
}
function handleActivityImageError(img, site) {
const storeInfo = STORE_INFO[site] || { name: site, color: '#333', bgColor: '#666', logo: '', fallback: '?' };
const div = document.createElement('div');
div.className = 'activity-store-icon';
div.style.background = storeInfo.bgColor;
div.innerHTML = `
<img src="${storeInfo.logo}" alt="${storeInfo.name}" class="activity-store-logo"
onerror="this.style.display='none'; this.nextElementSibling.style.display='block';">
<span class="activity-store-fallback" style="display:none; color:${storeInfo.color};">${storeInfo.fallback}</span>
`;
img.replaceWith(div);
}
// Navigate to products filtered by event type
function goToFilteredProducts(eventType, period) {
currentEventFilter = { type: eventType, period: period };
currentProductPage = 1;
navigateTo('products');
}
// Clear event filter
function clearEventFilter() {
currentEventFilter = null;
currentProductPage = 1;
loadProducts();
}
// Products
async function loadProducts() {
const site = document.getElementById('filterSite')?.value || '';
const category = document.getElementById('filterCategory')?.value || '';
const inStock = document.getElementById('filterStock')?.value || '';
const favoritesOnly = document.getElementById('filterFavorites')?.checked || false;
const offset = (currentProductPage - 1) * PRODUCTS_PER_PAGE;
let url = `/products?limit=${PRODUCTS_PER_PAGE}&offset=${offset}`;
if (site) url += `&site=${site}`;
if (category) url += `&category=${encodeURIComponent(category)}`;
if (inStock) url += `&in_stock=${inStock}`;
if (favoritesOnly) url += '&favorites_only=true';
// Apply event type filter if set
if (currentEventFilter) {
url += `&event_type=${currentEventFilter.type}&period=${currentEventFilter.period}`;
}
// Update filter badge visibility
updateFilterBadge();
const data = await api(url);
if (!data || !data.products) return;
const grid = document.getElementById('productsGrid');
if (data.products.length === 0) {
grid.innerHTML = '<div class="loading">No products found</div>';
renderPagination(0);
return;
}
grid.innerHTML = data.products.map(product => {
const storeInfo = STORE_INFO[product.site] || { name: product.site, color: '#333', bgColor: '#666', logo: '', fallback: '?' };
const cleanName = cleanProductName(product.name);
const hasImage = product.image_url && !product.image_url.includes('data:') && product.image_url.length > 10;
return `
<div class="product-card ${product.in_stock ? 'in-stock' : 'out-of-stock'} ${product.is_favorite ? 'favorite' : ''}"
onclick="showProductDetails(${product.id})">
<div class="product-image-container">
${hasImage
? `<img src="${product.image_url}" class="product-image" alt="" onerror="handleProductImageError(this, '${product.site}')">`
: `<div class="product-store-icon" style="background:${storeInfo.bgColor};">
<img src="${storeInfo.logo}" alt="${storeInfo.name}" class="product-store-logo"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<span class="product-store-fallback" style="display:none; color:${storeInfo.color};">${storeInfo.fallback}</span>
</div>`
}
<div class="product-store-badge" style="background:${storeInfo.bgColor};">
<img src="${storeInfo.logo}" alt="${storeInfo.name}" class="store-badge-logo"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<span class="store-badge-fallback" style="display:none; color:${storeInfo.color};">${storeInfo.fallback}</span>
</div>
</div>
<div class="product-name">${cleanName}</div>
<div class="product-meta">
<span class="product-price">${product.current_price || 'N/A'}</span>
<span class="product-stock ${product.in_stock ? 'in-stock' : 'out-of-stock'}">
${product.in_stock ? 'IN STOCK' : 'OUT OF STOCK'}
</span>
</div>
<div class="product-actions">
<span class="product-site">${storeInfo.name}</span>
<button class="btn-favorite ${product.is_favorite ? 'active' : ''}"
onclick="event.stopPropagation(); toggleFavorite('${product.url}', '${cleanName.replace(/'/g, "\\'")}', ${product.favorite_id || 'null'})">
${product.is_favorite ? '&#9733;' : '&#9734;'}
</button>
</div>
</div>
`;
}).join('');
renderPagination(data.total || 0);
}
function renderPagination(total) {
const container = document.getElementById('productsPagination');
if (!container) return;
const totalPages = Math.ceil(total / PRODUCTS_PER_PAGE);
if (totalPages <= 1) {
container.innerHTML = '';
return;
}
const start = (currentProductPage - 1) * PRODUCTS_PER_PAGE + 1;
const end = Math.min(currentProductPage * PRODUCTS_PER_PAGE, total);
container.innerHTML = `
<div class="pagination">
<button class="btn-page" onclick="changeProductPage(${currentProductPage - 1})" ${currentProductPage === 1 ? 'disabled' : ''}>&laquo; Prev</button>
<span class="page-info">Page ${currentProductPage} of ${totalPages} &nbsp;(${start}${end} of ${total})</span>
<button class="btn-page" onclick="changeProductPage(${currentProductPage + 1})" ${currentProductPage === totalPages ? 'disabled' : ''}>Next &raquo;</button>
</div>
`;
}
function changeProductPage(page) {
currentProductPage = page;
loadProducts();
document.getElementById('page-products').scrollIntoView({ behavior: 'smooth' });
}
// Handle product image errors by showing store icon
function handleProductImageError(img, site) {
const storeInfo = STORE_INFO[site] || { name: site, color: '#333', bgColor: '#666', logo: '', fallback: '?' };
const container = img.parentElement;
img.style.display = 'none';
// Create fallback element
const fallback = document.createElement('div');
fallback.className = 'product-store-icon';
fallback.style.background = storeInfo.bgColor;
fallback.innerHTML = `
<img src="${storeInfo.logo}" alt="${storeInfo.name}" class="product-store-logo"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<span class="product-store-fallback" style="display:none; color:${storeInfo.color};">${storeInfo.fallback}</span>
`;
container.insertBefore(fallback, img);
}
async function showProductDetails(productId) {
const product = await api(`/products/${productId}`);
const history = await api(`/products/${productId}/history`);
if (!product) return;
document.getElementById('modalProductName').textContent = cleanProductName(product.name);
const storeInfo = STORE_INFO[product.site] || { name: product.site, color: '#333', bgColor: '#666', icon: '?' };
document.getElementById('modalProductDetails').innerHTML = `
<p><strong>Store:</strong> ${storeInfo.name}</p>
<p><strong>Category:</strong> ${product.category || 'Unknown'}</p>
<p><strong>Price:</strong> ${product.current_price || 'N/A'}</p>
<p><strong>Status:</strong> ${product.in_stock ? '<span style="color: #00c853;">In Stock</span>' : '<span style="color: #c62828;">Out of Stock</span>'}</p>
<p><strong>First Seen:</strong> ${formatDate(product.first_seen)}</p>
<p><strong>Last Seen:</strong> ${formatDate(product.last_seen)}</p>
<p><a href="${product.url}" target="_blank" style="color: #ffcb05;">View on ${storeInfo.name} &#8594;</a></p>
${history && history.price_history && history.price_history.length > 0 ? `
<h4>Price History</h4>
<ul>
${history.price_history.map(p => `<li>${p.price} - ${formatDate(p.recorded_at)}</li>`).join('')}
</ul>
` : ''}
`;
document.getElementById('productModal').classList.add('show');
}
function closeModal() {
document.getElementById('productModal').classList.remove('show');
}
// Analytics
async function loadAnalytics() {
await Promise.all([
loadDropTimingChart(),
loadStockDurationChart(),
loadSiteChart()
]);
}
async function loadDropTimingChart() {
const data = await api('/analytics/drops');
if (!data || !data.drop_timing) return;
const ctx = document.getElementById('dropTimingChart');
if (!ctx) return;
// Destroy existing chart
if (charts.dropTiming) charts.dropTiming.destroy();
// Fill in missing hours
const hourlyData = new Array(24).fill(0);
data.drop_timing.forEach(d => {
hourlyData[parseInt(d.hour)] = d.count;
});
charts.dropTiming = new Chart(ctx, {
type: 'bar',
data: {
labels: Array.from({length: 24}, (_, i) => `${i}:00`),
datasets: [{
label: 'Drops/Restocks',
data: hourlyData,
backgroundColor: 'rgba(255, 203, 5, 0.6)',
borderColor: 'rgba(255, 203, 5, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false }
},
scales: {
y: { beginAtZero: true, ticks: { color: '#aaa' } },
x: { ticks: { color: '#aaa' } }
}
}
});
}
async function loadStockDurationChart() {
const data = await api('/analytics/stock');
if (!data || !data.stock_duration) return;
const ctx = document.getElementById('stockDurationChart');
if (!ctx) return;
if (charts.stockDuration) charts.stockDuration.destroy();
const labels = data.stock_duration.map(d => d.category || 'Unknown');
const values = data.stock_duration.map(d => d.avg_minutes_in_stock || 0);
charts.stockDuration = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'Avg Minutes In Stock',
data: values,
backgroundColor: 'rgba(0, 200, 83, 0.6)',
borderColor: 'rgba(0, 200, 83, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
indexAxis: 'y',
plugins: {
legend: { display: false }
},
scales: {
x: { beginAtZero: true, ticks: { color: '#aaa' } },
y: { ticks: { color: '#aaa' } }
}
}
});
}
async function loadSiteChart() {
const data = await api('/analytics/sites');
if (!data || !data.sites) return;
const ctx = document.getElementById('siteChart');
if (!ctx) return;
if (charts.site) charts.site.destroy();
charts.site = new Chart(ctx, {
type: 'doughnut',
data: {
labels: data.sites.map(s => s.site),
datasets: [{
data: data.sites.map(s => s.total_products),
backgroundColor: [
'rgba(255, 203, 5, 0.8)',
'rgba(0, 153, 255, 0.8)',
'rgba(0, 200, 83, 0.8)',
'rgba(198, 40, 40, 0.8)'
]
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
labels: { color: '#eee' }
}
}
}
});
}
// Favorites
async function loadFavorites() {
const data = await api('/favorites');
if (!data || !data.favorites) return;
const list = document.getElementById('favoritesList');
if (data.favorites.length === 0) {
list.innerHTML = '<div class="loading">No favorites yet. Add some above!</div>';
return;
}
list.innerHTML = data.favorites.map(fav => `
<div class="favorite-item">
<div class="favorite-icon">${fav.type === 'category' ? '&#128193;' : '&#128279;'}</div>
<div class="favorite-details">
<div class="favorite-name">${escapeHtml(fav.display_name || fav.value)}</div>
<div class="favorite-type">${fav.type === 'category' ? 'Category' : 'Product'}</div>
</div>
<span class="favorite-priority ${fav.priority}">${fav.priority.toUpperCase()}</span>
<button class="btn-delete" onclick="deleteFavorite(${fav.id})">&#128465;</button>
</div>
`).join('');
}
async function loadSuggestions() {
const data = await api('/favorites/suggestions');
if (!data || !data.suggestions) return;
const container = document.getElementById('suggestions');
container.innerHTML = data.suggestions.slice(0, 10).map(s => `
<span class="suggestion-tag" onclick="document.getElementById('favValue').value = '${s}'">${s}</span>
`).join('');
}
async function addFavorite() {
const type = document.getElementById('favType').value;
const value = document.getElementById('favValue').value.trim();
const priority = document.getElementById('favPriority').value;
if (!value) {
alert('Please enter a value');
return;
}
await api('/favorites', {
method: 'POST',
body: JSON.stringify({ type, value, priority })
});
document.getElementById('favValue').value = '';
loadFavorites();
}
async function deleteFavorite(id) {
if (!confirm('Remove this favorite?')) return;
await api(`/favorites/${id}`, { method: 'DELETE' });
loadFavorites();
}
async function toggleFavorite(url, name, favoriteId) {
if (favoriteId) {
await api(`/favorites/${favoriteId}`, { method: 'DELETE' });
} else {
await api('/favorites', {
method: 'POST',
body: JSON.stringify({
type: 'product',
value: url,
display_name: name,
priority: 'high'
})
});
}
loadProducts();
}
// Utilities
function updateFilterBadge() {
const badge = document.getElementById('eventFilterBadge');
if (!badge) return;
if (currentEventFilter) {
const typeLabel = currentEventFilter.type === 'new_drop' ? 'New Drops' : 'Restocks';
const periodLabel = currentEventFilter.period === 'today' ? 'Today' : '7 Days';
badge.innerHTML = `
<span class="filter-badge-text">${typeLabel} - ${periodLabel}</span>
<span class="filter-badge-close" onclick="clearEventFilter()">&times;</span>
`;
badge.style.display = 'flex';
} else {
badge.style.display = 'none';
}
}
function formatTime(date) {
const now = new Date();
const diff = (now - date) / 1000;
if (diff < 60) return 'Just now';
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
return date.toLocaleDateString();
}
function formatDate(dateStr) {
if (!dateStr) return 'N/A';
return new Date(dateStr).toLocaleString();
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function cleanProductName(name) {
if (!name) return '';
// Fix common UTF-8 encoding issues (mojibake)
return name
// Remove SVG/CSS class garbage (pikachu_svg__st1{fill:#7c888f}.review-full-...)
.replace(/[\w-]*_svg__[\w\s{}:#.;-]+/gi, '')
.replace(/\.review-full-[\w\s{}:#.;-]+/gi, '')
.replace(/\{[^}]*\}/g, '') // Remove any remaining {css} blocks
.replace(/\d+\s*Reviews?$/i, '') // Remove trailing "X Reviews"
// Fix Pokémon - multiple encoding patterns
.replace(/Pok[éÃ\u00c3\u00a9\ufffd]+mon/gi, 'Pokémon')
.replace(/Pokémon/gi, 'Pokémon')
.replace(/Pok&eacute;mon/gi, 'Pokémon')
.replace(/Pokテゥmon/gi, 'Pokémon')
// Keep "Pokemon" as-is for non-encoded versions
// Fix dashes - â€" is UTF-8 em-dash mangled
.replace(/â€"/g, '')
.replace(/â€"/g, '-')
.replace(/â€""/g, '')
// Fix apostrophes and quotes
.replace(/’/g, "'")
.replace(/‘/g, "'")
.replace(/“/g, '"')
.replace(/â€[^a-zA-Z0-9]/g, '"')
.replace(/'/g, "'")
.replace(/'/g, "'")
.replace(/"/g, '"')
.replace(/"/g, '"')
// Fix accented characters
.replace(/é/g, 'é')
.replace(/è/g, 'è')
.replace(/Ã /g, 'à')
.replace(/â/g, 'â')
.replace(/î/g, 'î')
.replace(/ô/g, 'ô')
.replace(/û/g, 'û')
// Fix trademark symbols
.replace(/â„¢/g, '™')
.replace(/®/g, '®')
// Clean up any remaining garbled chars
.replace(/Â/g, '')
// Clean up any double spaces
.replace(/\s+/g, ' ')
.trim();
}
// ==================== Control Panel ====================
let controlPanelInterval = null;
async function loadControlPanel() {
const state = await api('/scrapers/state');
if (!state) return;
// Update monitor status
updateMonitorStatus(state);
// Update check interval input
document.getElementById('checkIntervalInput').value = state.check_interval || 60;
// Render scrapers grid
renderScrapersGrid(state.scrapers);
// Load Chrome Extension status
loadExtensionStatus();
// Setup event listeners for control panel buttons
setupControlPanelListeners();
// Auto-refresh control panel every 5 seconds
if (controlPanelInterval) clearInterval(controlPanelInterval);
controlPanelInterval = setInterval(async () => {
if (currentPage === 'control') {
const newState = await api('/scrapers/state');
if (newState) {
updateMonitorStatus(newState);
renderScrapersGrid(newState.scrapers);
}
loadExtensionStatus();
}
}, 5000);
}
async function loadExtensionStatus() {
const badge = document.getElementById('extensionBadge');
const productCount = document.getElementById('extProductCount');
const skuCount = document.getElementById('extSkuCount');
const lastSync = document.getElementById('extLastSync');
const stats = await api('/extension/stats');
if (stats && stats.last_sync) {
// Extension has synced data
badge.textContent = 'Connected';
badge.className = 'extension-badge connected';
productCount.textContent = stats.total_products || 0;
skuCount.textContent = stats.total_skus || 0;
lastSync.textContent = formatTime(new Date(stats.last_sync));
} else {
// No sync yet
badge.textContent = 'No Data';
badge.className = 'extension-badge disconnected';
productCount.textContent = '-';
skuCount.textContent = '-';
lastSync.textContent = 'Never';
}
}
function updateMonitorStatus(state) {
const badge = document.getElementById('monitorBadge');
const statusText = document.getElementById('monitorStatusText');
const lastCheck = document.getElementById('controlLastCheck');
const startBtn = document.getElementById('startMonitorBtn');
const stopBtn = document.getElementById('stopMonitorBtn');
if (state.monitor_running) {
badge.textContent = 'Running';
badge.className = 'monitor-badge running';
statusText.textContent = `Monitor is running (PID: ${state.monitor_pid})`;
startBtn.disabled = true;
stopBtn.disabled = false;
} else {
badge.textContent = 'Stopped';
badge.className = 'monitor-badge stopped';
statusText.textContent = 'Monitor is not running';
startBtn.disabled = false;
stopBtn.disabled = true;
}
if (state.last_check) {
lastCheck.textContent = formatTime(new Date(state.last_check));
} else {
lastCheck.textContent = 'Never';
}
}
function renderScrapersGrid(scrapers) {
const grid = document.getElementById('scrapersGrid');
grid.innerHTML = Object.entries(scrapers).map(([key, data]) => {
const info = STORE_INFO[key] || { name: key, logo: '', fallback: key.substring(0, 2).toUpperCase(), bgColor: '#333', color: '#fff' };
const enabledClass = data.enabled ? 'enabled' : 'disabled';
const runningClass = data.running ? 'running' : '';
return `
<div class="scraper-card ${enabledClass} ${runningClass}">
<div class="scraper-logo" style="background: ${info.bgColor};">
<img src="${info.logo}" alt="${info.name}" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<span class="scraper-logo-fallback" style="display:none; color: ${info.color};">${info.fallback}</span>
</div>
<div class="scraper-info">
<div class="scraper-name">${info.name}</div>
<div class="scraper-status">
${data.running ? '<span class="status-running">&#9679; Running</span>' :
data.enabled ? '<span class="status-enabled">&#9679; Enabled</span>' :
'<span class="status-disabled">&#9675; Disabled</span>'}
</div>
<div class="scraper-last-run">Last: ${data.last_run ? formatTime(new Date(data.last_run)) : 'Never'}</div>
${data.last_error ? `<div class="scraper-error">${data.last_error}</div>` : ''}
</div>
<label class="toggle-switch">
<input type="checkbox" ${data.enabled ? 'checked' : ''} onchange="toggleScraper('${key}', this.checked)">
<span class="toggle-slider"></span>
</label>
</div>
`;
}).join('');
}
function setupControlPanelListeners() {
// Start/Stop buttons
document.getElementById('startMonitorBtn')?.addEventListener('click', startMonitor);
document.getElementById('stopMonitorBtn')?.addEventListener('click', stopMonitor);
// Save interval button
document.getElementById('saveIntervalBtn')?.addEventListener('click', saveCheckInterval);
// Quick actions
document.getElementById('runCheckNowBtn')?.addEventListener('click', runCheckNow);
document.getElementById('viewLogsBtn')?.addEventListener('click', viewLogs);
}
async function toggleScraper(scraper, enabled) {
const result = await api(`/scrapers/${scraper}/toggle`, {
method: 'POST',
body: JSON.stringify({ enabled })
});
if (result && result.success) {
// Refresh the control panel
loadControlPanel();
} else {
alert('Failed to toggle scraper: ' + (result?.error || 'Unknown error'));
}
}
async function startMonitor() {
const btn = document.getElementById('startMonitorBtn');
btn.disabled = true;
btn.innerHTML = '<span class="btn-icon">&#8987;</span> Starting...';
const result = await api('/scrapers/start', { method: 'POST' });
if (result && result.success) {
loadControlPanel();
} else {
alert('Failed to start monitor: ' + (result?.error || 'Unknown error'));
btn.disabled = false;
btn.innerHTML = '<span class="btn-icon">&#9654;</span> Start Monitor';
}
}
async function stopMonitor() {
const btn = document.getElementById('stopMonitorBtn');
btn.disabled = true;
btn.innerHTML = '<span class="btn-icon">&#8987;</span> Stopping...';
const result = await api('/scrapers/stop', { method: 'POST' });
if (result && result.success) {
loadControlPanel();
} else {
alert('Failed to stop monitor: ' + (result?.error || 'Unknown error'));
btn.disabled = false;
btn.innerHTML = '<span class="btn-icon">&#9632;</span> Stop Monitor';
}
}
async function saveCheckInterval() {
const input = document.getElementById('checkIntervalInput');
const interval = parseInt(input.value);
if (isNaN(interval) || interval < 10) {
alert('Interval must be at least 10 seconds');
return;
}
const result = await api('/scrapers/interval', {
method: 'POST',
body: JSON.stringify({ interval })
});
if (result && result.success) {
alert('Check interval saved!');
} else {
alert('Failed to save interval: ' + (result?.error || 'Unknown error'));
}
}
async function runCheckNow() {
const result = await api('/check', { method: 'POST' });
if (result) {
alert(result.message || 'Check triggered');
}
}
function viewLogs() {
// Open monitor.log in a new window or show in modal
alert('View monitor.log file in the project directory for detailed logs.');
}
// ==================== News ====================
let newsInitialized = false;
async function loadNews() {
// Initialize event listeners once
if (!newsInitialized) {
initNewsEventListeners();
newsInitialized = true;
}
// Load stats and articles
await Promise.all([
loadNewsStats(),
loadNewsArticles()
]);
}
function initNewsEventListeners() {
// Filter changes
document.getElementById('filterNewsSource')?.addEventListener('change', loadNewsArticles);
document.getElementById('filterNewsSentiment')?.addEventListener('change', loadNewsArticles);
document.getElementById('filterDropRelated')?.addEventListener('change', loadNewsArticles);
// Refresh button
document.getElementById('refreshNewsBtn')?.addEventListener('click', refreshNews);
// Manual news input
document.getElementById('addManualNewsBtn')?.addEventListener('click', addManualNews);
}
async function loadNewsStats() {
const stats = await api('/news/stats');
if (!stats) return;
// Update sentiment bar
const total = (stats.by_sentiment?.positive || 0) +
(stats.by_sentiment?.neutral || 0) +
(stats.by_sentiment?.negative || 0);
if (total > 0) {
const positivePercent = (stats.by_sentiment?.positive || 0) / total * 100;
const neutralPercent = (stats.by_sentiment?.neutral || 0) / total * 100;
const negativePercent = (stats.by_sentiment?.negative || 0) / total * 100;
document.getElementById('sentimentPositiveBar').style.width = positivePercent + '%';
document.getElementById('sentimentNeutralBar').style.width = neutralPercent + '%';
document.getElementById('sentimentNegativeBar').style.width = negativePercent + '%';
document.getElementById('sentimentText').textContent =
`${Math.round(positivePercent)}% Positive, ${Math.round(neutralPercent)}% Neutral, ${Math.round(negativePercent)}% Negative`;
} else {
document.getElementById('sentimentText').textContent = 'No sentiment data yet';
}
// Update today count
document.getElementById('todayArticles').textContent = stats.today || 0;
}
async function loadNewsArticles() {
const source = document.getElementById('filterNewsSource')?.value || '';
const sentiment = document.getElementById('filterNewsSentiment')?.value || '';
const dropRelated = document.getElementById('filterDropRelated')?.checked || false;
let url = '/news?limit=50';
if (source) url += `&source=${source}`;
if (sentiment) url += `&sentiment=${sentiment}`;
if (dropRelated) url += '&drop_related=true';
const data = await api(url);
if (!data || !data.articles) return;
const feed = document.getElementById('newsFeed');
if (data.articles.length === 0) {
feed.innerHTML = '<div class="loading">No news articles yet. Click "Refresh News" to fetch from sources.</div>';
return;
}
feed.innerHTML = data.articles.map(article => {
const sourceIcon = getSourceIcon(article.source);
const sentimentBadge = getSentimentBadge(article.sentiment_label, article.sentiment_score);
const timeAgo = formatTime(new Date(article.published_at || article.fetched_at));
const keywords = article.keywords || [];
const hasCorrelation = article.is_drop_related || article.is_restock_related;
return `
<div class="news-card">
<div class="news-header">
<span class="news-source">
<span class="source-icon">${sourceIcon}</span>
${escapeHtml(article.source_account)}
</span>
<span class="news-time">${timeAgo}</span>
${sentimentBadge}
</div>
${article.title ? `<div class="news-title">${escapeHtml(article.title)}</div>` : ''}
<div class="news-content">${escapeHtml(article.content)}</div>
${article.url ? `<a href="${article.url}" target="_blank" class="news-link">View Original</a>` : ''}
${keywords.length > 0 ? `
<div class="news-keywords">
${keywords.slice(0, 5).map(kw => `<span class="keyword-tag">${escapeHtml(kw)}</span>`).join('')}
</div>
` : ''}
${hasCorrelation ? `
<div class="news-correlation">
<span class="correlation-badge">
${article.is_drop_related ? 'Drop Related' : ''}
${article.is_restock_related ? 'Restock Related' : ''}
</span>
</div>
` : ''}
<div class="news-actions">
<button class="btn-delete-small" onclick="deleteNewsArticle(${article.id})">Delete</button>
</div>
</div>
`;
}).join('');
}
function getSourceIcon(source) {
switch (source) {
case 'twitter':
return '<span style="color: #1DA1F2;">X</span>';
case 'pokemon_official':
return '<span style="color: #FFCB05;">&#9889;</span>';
case 'discord_manual':
return '<span style="color: #5865F2;">&#128172;</span>';
default:
return '<span>&#128196;</span>';
}
}
function getSentimentBadge(label, score) {
if (!label) return '';
let badgeClass = 'neutral';
let symbol = '&#9679;';
if (label === 'positive') {
badgeClass = 'positive';
symbol = '&#43;';
} else if (label === 'negative') {
badgeClass = 'negative';
symbol = '&#8722;';
}
const scoreText = score !== null ? ` (${score.toFixed(2)})` : '';
return `<span class="sentiment-badge ${badgeClass}">${symbol} ${label}${scoreText}</span>`;
}
async function refreshNews() {
const btn = document.getElementById('refreshNewsBtn');
btn.disabled = true;
btn.innerHTML = '<span class="btn-icon">&#8987;</span> Fetching...';
const result = await api('/news/refresh', { method: 'POST' });
btn.disabled = false;
btn.innerHTML = '<span class="btn-icon">&#8635;</span> Refresh News';
if (result && result.success) {
const msg = `Fetched ${result.fetched} articles.`;
if (result.errors && result.errors.length > 0) {
alert(msg + '\n\nErrors:\n' + result.errors.join('\n'));
}
// Reload news
loadNews();
} else {
alert('Failed to refresh news');
}
}
async function addManualNews() {
const source = document.getElementById('manualNewsSource')?.value || 'Other';
const title = document.getElementById('manualNewsTitle')?.value.trim() || null;
const content = document.getElementById('manualNewsContent')?.value.trim();
const url = document.getElementById('manualNewsUrl')?.value.trim() || null;
if (!content) {
alert('Please enter the message content');
return;
}
const result = await api('/news/manual', {
method: 'POST',
body: JSON.stringify({
source_account: source,
title: title,
content: content,
url: url
})
});
if (result && result.success) {
// Clear form
document.getElementById('manualNewsTitle').value = '';
document.getElementById('manualNewsContent').value = '';
document.getElementById('manualNewsUrl').value = '';
// Show analysis result
const analysis = result.analysis;
alert(`News added!\n\nSentiment: ${analysis.sentiment_label} (${analysis.sentiment_score.toFixed(2)})\nDrop related: ${analysis.is_drop_related}\nRestock related: ${analysis.is_restock_related}`);
// Reload news
loadNews();
} else {
alert('Failed to add news: ' + (result?.error || 'Unknown error'));
}
}
async function deleteNewsArticle(articleId) {
if (!confirm('Delete this news article?')) return;
const result = await api(`/news/${articleId}`, { method: 'DELETE' });
if (result && result.success) {
loadNewsArticles();
} else {
alert('Failed to delete article');
}
}
// ==================== Users Page ====================
let usersInitialized = false;
async function loadUsers() {
// Initialize event listeners once
if (!usersInitialized) {
document.getElementById('addUserBtn')?.addEventListener('click', addUser);
usersInitialized = true;
}
const data = await api('/users');
if (!data) return;
await renderUsersList(data.users || []);
}
async function renderUsersList(users) {
const container = document.getElementById('usersList');
if (!container) return;
if (users.length === 0) {
container.innerHTML = '<p class="empty-state">No users yet. Add one above!</p>';
return;
}
// Fetch profile status for all users in parallel
const profileStatuses = await Promise.all(
users.map(u => api(`/users/${u.id}/profile`).catch(() => null))
);
container.innerHTML = users.map((user, i) => {
const ps = profileStatuses[i] || {};
const hasProfile = ps.has_profile;
const isUnlocked = ps.is_unlocked;
const maskedCard = ps.masked_card || '';
const profileBadge = isUnlocked
? `<span class="badge badge-success">&#128275; Unlocked ${maskedCard ? '· ' + escapeHtml(maskedCard) : ''}</span>`
: hasProfile
? `<span class="badge badge-warning">&#128274; Locked</span>`
: `<span class="badge badge-muted">No profile</span>`;
const profileBtns = hasProfile
? isUnlocked
? `<button class="btn-secondary btn-small" onclick="openProfileModal(${user.id}, '${escapeHtml(user.name)}')">Edit Profile</button>
<button class="btn-secondary btn-small" onclick="lockProfile(${user.id})">Lock</button>
<button class="btn-danger btn-small" onclick="deleteProfile(${user.id})">Delete Profile</button>`
: `<button class="btn-primary btn-small" onclick="openUnlockModal(${user.id}, '${escapeHtml(user.name)}')">Unlock</button>
<button class="btn-secondary btn-small" onclick="openProfileModal(${user.id}, '${escapeHtml(user.name)}')">Edit Profile</button>
<button class="btn-danger btn-small" onclick="deleteProfile(${user.id})">Delete Profile</button>`
: `<button class="btn-primary btn-small" onclick="openProfileModal(${user.id}, '${escapeHtml(user.name)}')">Set Up Profile</button>`;
return `
<div class="user-card" data-user-id="${user.id}">
<div class="user-header">
<span class="user-name">${escapeHtml(user.name)}</span>
<span class="user-location">${user.zip_code ? `${user.zip_code} (${user.radius_miles}mi)` : 'No location set'}</span>
${profileBadge}
</div>
<div class="user-details">
<div class="form-inline">
<input type="text" class="user-zip-input" placeholder="Zip Code" value="${user.zip_code || ''}" maxlength="10">
<input type="number" class="user-radius-input" placeholder="Radius" value="${user.radius_miles || 25}" min="5" max="100" style="width: 80px;">
<button class="btn-secondary btn-small" onclick="updateUserLocation(${user.id})">Update Location</button>
<button class="btn-secondary btn-small" onclick="viewUserStores(${user.id})">View Stores</button>
<button class="btn-danger btn-small" onclick="deleteUser(${user.id})">Delete User</button>
</div>
<div class="form-inline" style="margin-top:.5rem;">
${profileBtns}
</div>
</div>
<div class="user-stores" id="userStores-${user.id}" style="display: none;"></div>
</div>`;
}).join('');
}
// ---- Profile modal ----
let _profileUserId = null;
async function openProfileModal(userId, userName) {
_profileUserId = userId;
document.getElementById('profileModalTitle').textContent = `Checkout Profile — ${userName}`;
document.getElementById('profileModalError').style.display = 'none';
// Clear all fields first
['pf-first-name','pf-last-name','pf-address1','pf-address2','pf-city','pf-state',
'pf-zip','pf-phone','pf-email','pf-card-number','pf-expiry-month','pf-expiry-year',
'pf-cvv','pf-card-name','pf-target-email','pf-target-password',
'pf-bestbuy-email','pf-bestbuy-password','pf-gamestop-email','pf-gamestop-password',
'pf-password','pf-password-confirm'].forEach(id => {
const el = document.getElementById(id);
if (el) el.value = '';
});
// Pre-fill from unlocked profile if available
const data = await api(`/users/${userId}/profile/data`).catch(() => null);
if (data && data.shipping) {
const s = data.shipping;
const set = (id, val) => { const el = document.getElementById(id); if (el && val) el.value = val; };
set('pf-first-name', s.first_name);
set('pf-last-name', s.last_name);
set('pf-address1', s.address1);
set('pf-address2', s.address2);
set('pf-city', s.city);
set('pf-state', s.state);
set('pf-zip', s.zip);
set('pf-phone', s.phone);
set('pf-email', s.email);
set('pf-target-email', data.site_credentials?.target_email);
set('pf-bestbuy-email', data.site_credentials?.bestbuy_email);
set('pf-gamestop-email', data.site_credentials?.gamestop_email);
// Show masked card as placeholder so user knows card is saved
if (data.masked_card) {
const cardEl = document.getElementById('pf-card-number');
if (cardEl) cardEl.placeholder = `${data.masked_card} (leave blank to keep)`;
}
}
document.getElementById('profileModal').style.display = 'flex';
}
function closeProfileModal() {
document.getElementById('profileModal').style.display = 'none';
_profileUserId = null;
}
async function saveProfile() {
const errEl = document.getElementById('profileModalError');
errEl.style.display = 'none';
const password = document.getElementById('pf-password').value;
const confirm = document.getElementById('pf-password-confirm').value;
if (password.length < 8) {
errEl.textContent = 'Password must be at least 8 characters.';
errEl.style.display = 'block';
return;
}
if (password !== confirm) {
errEl.textContent = 'Passwords do not match.';
errEl.style.display = 'block';
return;
}
const shipping = {
first_name: document.getElementById('pf-first-name').value.trim(),
last_name: document.getElementById('pf-last-name').value.trim(),
address1: document.getElementById('pf-address1').value.trim(),
address2: document.getElementById('pf-address2').value.trim(),
city: document.getElementById('pf-city').value.trim(),
state: document.getElementById('pf-state').value.trim().toUpperCase(),
zip: document.getElementById('pf-zip').value.trim(),
phone: document.getElementById('pf-phone').value.trim(),
email: document.getElementById('pf-email').value.trim(),
};
const payment = {
card_number: document.getElementById('pf-card-number').value.replace(/\s/g, ''),
expiry_month: document.getElementById('pf-expiry-month').value.trim(),
expiry_year: document.getElementById('pf-expiry-year').value.trim(),
cvv: document.getElementById('pf-cvv').value.trim(),
name_on_card: document.getElementById('pf-card-name').value.trim(),
};
const site_credentials = {
target_email: document.getElementById('pf-target-email')?.value.trim(),
target_password: document.getElementById('pf-target-password')?.value,
bestbuy_email: document.getElementById('pf-bestbuy-email')?.value.trim(),
bestbuy_password: document.getElementById('pf-bestbuy-password')?.value,
gamestop_email: document.getElementById('pf-gamestop-email')?.value.trim(),
gamestop_password: document.getElementById('pf-gamestop-password')?.value,
};
const result = await api(`/users/${_profileUserId}/profile`, {
method: 'POST',
body: JSON.stringify({ password, shipping, payment, site_credentials })
});
if (result && result.success) {
closeProfileModal();
loadUsers();
} else {
errEl.textContent = result?.error || 'Failed to save profile.';
errEl.style.display = 'block';
}
}
// ---- Unlock modal ----
let _unlockUserId = null;
function openUnlockModal(userId, userName) {
_unlockUserId = userId;
document.getElementById('unlockModalTitle').textContent = `Unlock Profile — ${userName}`;
document.getElementById('unlock-password').value = '';
document.getElementById('unlockModalError').style.display = 'none';
document.getElementById('unlockModal').style.display = 'flex';
setTimeout(() => document.getElementById('unlock-password').focus(), 100);
}
function closeUnlockModal() {
document.getElementById('unlockModal').style.display = 'none';
_unlockUserId = null;
}
async function submitUnlock() {
const errEl = document.getElementById('unlockModalError');
const password = document.getElementById('unlock-password').value;
const result = await api(`/users/${_unlockUserId}/profile/unlock`, {
method: 'POST',
body: JSON.stringify({ password })
});
if (result && result.success) {
closeUnlockModal();
loadUsers();
} else {
errEl.textContent = result?.error || 'Incorrect password.';
errEl.style.display = 'block';
}
}
async function lockProfile(userId) {
await api(`/users/${userId}/profile/lock`, { method: 'POST' });
loadUsers();
}
async function deleteProfile(userId) {
if (!confirm('Delete this checkout profile? This cannot be undone.')) return;
await api(`/users/${userId}/profile`, { method: 'DELETE' });
loadUsers();
}
async function addUser() {
const nameInput = document.getElementById('newUserName');
const zipInput = document.getElementById('newUserZip');
const radiusInput = document.getElementById('newUserRadius');
const name = nameInput.value.trim();
const zip_code = zipInput.value.trim() || null;
const radius_miles = parseInt(radiusInput.value) || 25;
if (!name) {
alert('Please enter a name');
return;
}
const result = await api('/users', {
method: 'POST',
body: JSON.stringify({ name, zip_code, radius_miles })
});
if (result && result.success) {
nameInput.value = '';
zipInput.value = '';
radiusInput.value = '25';
loadUsers();
} else {
alert('Failed to add user: ' + (result?.error || 'Unknown error'));
}
}
async function updateUserLocation(userId) {
const card = document.querySelector(`[data-user-id="${userId}"]`);
const zip_code = card.querySelector('.user-zip-input').value.trim();
const radius_miles = parseInt(card.querySelector('.user-radius-input').value) || 25;
if (!zip_code) {
alert('Please enter a zip code');
return;
}
const result = await api(`/users/${userId}/location`, {
method: 'PUT',
body: JSON.stringify({ zip_code, radius_miles })
});
if (result && result.success) {
loadUsers();
} else {
alert('Failed to update location: ' + (result?.error || 'Unknown error'));
}
}
async function viewUserStores(userId) {
const storesDiv = document.getElementById(`userStores-${userId}`);
// Toggle visibility
if (storesDiv.style.display !== 'none') {
storesDiv.style.display = 'none';
return;
}
storesDiv.innerHTML = '<p class="loading">Loading stores...</p>';
storesDiv.style.display = 'block';
const result = await api(`/users/${userId}/stores`);
if (!result || result.error) {
storesDiv.innerHTML = `<p class="error">${result?.error || 'Failed to load stores'}</p>`;
return;
}
let html = `<div class="stores-grid">`;
for (const [retailer, stores] of Object.entries(result.stores)) {
if (stores.length > 0) {
html += `<div class="retailer-section">
<h4>${retailer} (${stores.length} stores)</h4>
<ul class="store-list">
${stores.slice(0, 5).map(s => `
<li>
<strong>${escapeHtml(s.name)}</strong> (${s.distance_miles.toFixed(1)} mi)<br>
<span class="store-address">${escapeHtml(s.address)}, ${escapeHtml(s.city)}, ${s.state}</span>
</li>
`).join('')}
</ul>
</div>`;
}
}
html += '</div>';
storesDiv.innerHTML = html;
}
async function deleteUser(userId) {
if (!confirm('Delete this user?')) return;
const result = await api(`/users/${userId}`, { method: 'DELETE' });
if (result && result.success) {
loadUsers();
} else {
alert('Failed to delete user: ' + (result?.error || 'Unknown error'));
}
}
// ==================== Settings Page ====================
let settingsInitialized = false;
async function loadSettings() {
// Initialize event listeners once
if (!settingsInitialized) {
document.getElementById('checkBrokenUrlsBtn')?.addEventListener('click', checkBrokenUrls);
document.getElementById('cleanupBrokenUrlsBtn')?.addEventListener('click', cleanupBrokenUrls);
document.getElementById('deduplicateBtn')?.addEventListener('click', deduplicateProducts);
document.getElementById('deduplicateEventsBtn')?.addEventListener('click', deduplicateEvents);
settingsInitialized = true;
}
}
async function checkBrokenUrls() {
const resultEl = document.getElementById('brokenUrlsResult');
resultEl.textContent = 'Checking...';
const result = await api('/extension/broken-urls');
if (result) {
resultEl.textContent = `Found ${result.count} products with broken Pokemon Center URLs.`;
} else {
resultEl.textContent = 'Failed to check broken URLs';
}
}
async function cleanupBrokenUrls() {
if (!confirm('This will delete all Pokemon Center products with broken URLs. Continue?')) return;
const resultEl = document.getElementById('brokenUrlsResult');
resultEl.textContent = 'Cleaning up...';
const result = await api('/extension/broken-urls/cleanup', { method: 'POST' });
if (result && result.success) {
resultEl.textContent = `Deleted ${result.deleted} products with broken URLs.`;
} else {
resultEl.textContent = 'Failed to cleanup broken URLs';
}
}
async function deduplicateProducts() {
if (!confirm('This will remove duplicate products. Continue?')) return;
const resultEl = document.getElementById('deduplicateResult');
resultEl.textContent = 'Removing duplicates...';
const result = await api('/products/deduplicate', { method: 'POST' });
if (result && result.success) {
let message = `Found ${result.duplicates_found} duplicate groups, removed ${result.products_removed} products.`;
if (Object.keys(result.by_site).length > 0) {
message += ' By site: ' + Object.entries(result.by_site).map(([site, count]) => `${site}: ${count}`).join(', ');
}
resultEl.textContent = message;
} else {
resultEl.textContent = 'Failed to deduplicate products';
}
}
async function deduplicateEvents() {
if (!confirm('This will remove duplicate events from the activity feed. Continue?')) return;
const resultEl = document.getElementById('deduplicateEventsResult');
resultEl.textContent = 'Cleaning up events...';
const result = await api('/events/deduplicate', { method: 'POST' });
if (result && result.success) {
resultEl.textContent = `Removed ${result.events_removed} duplicate events.`;
} else {
resultEl.textContent = 'Failed to clean up events';
}
}