pliceclogs-og/popup.js
type-two e5b8504bc6 Import pliceclogs as-is — the original Discogs seller extension
Snapshot of the working tree exactly as it stood, no edits. This is the predecessor
PRICEGOD was rewritten from ("kept intact, untouched" per pricegod/README.md), and it
is still the only place the DYMO scale is actually implemented —
rfid-daemon/index.js:1068-1265: HID discovery, parseScaleReport, one-shot read, and a
streaming /weight + /scale/start|stop session whose JSON shape PRICEGOD's daemon.js
already speaks.

Preserved verbatim on purpose (hence -og), including the known bug: DYMO_PIDS at
index.js:1082 is [0x8003, 0x8004], so it cannot see the bench M25 (0x8009). Fix that
in whatever daemon inherits the scale, not here.

node_modules stays ignored (22M of the 24M tree). No credentials in the import: the
two PEM markers in utils.js/sheets.js only strip headers off a key read from settings,
and mrpadmin / johnking are an SSH and a Postgres username, both key/trust auth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 13:47:09 +10:00

4931 lines
212 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.

// Create placeholder functions for missing API functions
async function getStoredDiscogsToken() {
return new Promise((resolve) => {
if (!chrome || !chrome.storage) {
resolve(null);
return;
}
chrome.storage.local.get('discogsToken', (data) => {
resolve((data.discogsToken || '').trim() || null);
});
});
}
async function getReleaseData(releaseId) {
const token = await getStoredDiscogsToken();
if (!token) return null;
const idMatch = String(releaseId || '').match(/\d+/);
const id = idMatch ? idMatch[0] : null;
if (!id) return null;
// Retry up to 3 times — Discogs occasionally rate-limits or drops the first
// request on a freshly-loaded page, which would otherwise crash the popup.
for (let attempt = 0; attempt < 3; attempt++) {
try {
const response = await fetch(`https://api.discogs.com/releases/${id}`, {
headers: {
'Authorization': `Discogs token=${token}`,
'User-Agent': 'PriceClogs/1.0'
}
});
if (response.status === 429) {
// Rate limited — back off and retry
await new Promise(r => setTimeout(r, 1500 * (attempt + 1)));
continue;
}
const data = await response.json();
if (data && data.artists) return data; // success
// API returned an error envelope (e.g. { message: "..." }) — retry
if (attempt < 2) await new Promise(r => setTimeout(r, 1000));
} catch (_) {
if (attempt < 2) await new Promise(r => setTimeout(r, 1000));
}
}
return null;
}
async function getPriceSuggestions(releaseId) {
try {
const token = await getStoredDiscogsToken();
if (!token) {
throw new Error('Discogs token not configured');
}
const idMatch = String(releaseId || '').match(/\d+/);
const id = idMatch ? idMatch[0] : null;
if (!id) {
throw new Error('Invalid release ID');
}
const response = await fetch(`https://api.discogs.com/marketplace/price_suggestions/${id}`, {
headers: {
'Authorization': `Discogs token=${token}`,
'User-Agent': 'PriceClogs/1.0'
}
});
return await response.json();
} catch (error) {
// console.error('Error fetching price suggestions:', error);
return null;
}
}
const state = {
releaseData: null,
originalReleaseData: null,
lastFetchTimestamp: 0,
sku: null, // 14-digit SKU generated at popup open (e.g. 20260313084637)
sheetTimestamp: null, // Full ISO timestamp matching the SKU, used for sheet column A
discogsTabId: null, // Tab ID of the Discogs page, captured at popup open
fetchCooldown: 1000,
collectionBoxIndex: 0, // currently selected collection box index
collectionBoxSkus: [], // SKUs for each box (empty string = no SKU)
collectionBoxCount: 1, // total number of collection boxes on page
// MONSTERWIKI background sync results (populated silently at popup open)
monsterSalesHistory: null,
monsterCurrentListings: null,
geminiAutoTriggered: false,
};
// ================================================================
// MONSTERWIKI — background sync helpers
// ================================================================
// parseSalesHistoryHtml + parseListingsHtml moved to monster_capture.js
// (loaded before popup.js in popup.html) so background.js can share them.
async function getMonsterSettings() {
return new Promise(resolve =>
chrome.storage.local.get(['monsterSettings'], r =>
resolve(r.monsterSettings || { enabled: false, preferLocalhost: false, serverUrl: 'http://100.91.239.7:5002' })
)
);
}
function monsterSetIndicator(msg, color, fadeAfterMs = 0) {
const el = document.getElementById('monster-sync-indicator');
if (!el) return;
el.textContent = msg;
el.style.color = color;
el.style.opacity = '1';
el.style.display = 'block';
if (fadeAfterMs) {
setTimeout(() => {
el.style.transition = 'opacity 1.5s';
el.style.opacity = '0';
setTimeout(() => { el.style.display = 'none'; }, 1600);
}, fadeAfterMs);
}
}
function geminiSetIndicator(msg, color, fadeAfterMs = 0) {
const el = document.getElementById('gemini-sync-indicator');
if (!el) return;
el.textContent = msg;
el.style.color = color;
el.style.opacity = '1';
el.style.display = 'block';
if (fadeAfterMs) {
setTimeout(() => {
el.style.transition = 'opacity 1.5s';
el.style.opacity = '0';
setTimeout(() => { el.style.display = 'none'; }, 1600);
}, fadeAfterMs);
}
}
async function monsterResolveBase(cfg) {
// If we're ON ultra, localhost:5002 will answer immediately — use it directly.
// Otherwise fall back to the configured Tailscale URL.
try {
const r = await fetch('http://localhost:5002/plice/health',
{ signal: AbortSignal.timeout(400) });
if (r.ok) return 'http://localhost:5002';
} catch (_) {}
return (cfg.serverUrl || 'http://100.91.239.7:5002').replace(/\/$/, '');
}
async function monsterBackgroundSync(releaseId) {
const cfg = await getMonsterSettings();
if (!cfg.enabled || !releaseId) return;
const base = await monsterResolveBase(cfg);
monsterSetIndicator('⟳ MONSTER syncing…', '#888');
const [histHtml, listHtml] = await Promise.all([
fetch(`https://www.discogs.com/sell/history/${releaseId}`)
.then(r => r.ok ? r.text() : null).catch(() => null),
fetch(`https://www.discogs.com/sell/list?release_id=${releaseId}`)
.then(r => r.ok ? r.text() : null).catch(() => null),
]);
state.monsterSalesHistory = histHtml ? parseSalesHistoryHtml(histHtml) : null;
state.monsterCurrentListings = listHtml ? parseListingsHtml(listHtml) : null;
if (!state.releaseData) return;
// Scroll to bottom of page so reviews lazy-load, wait 2s, then scrape
let liveReviews = state.releaseData.reviews || [];
if (state.discogsTabId) {
try {
const results = await chrome.scripting.executeScript({
target: { tabId: state.discogsTabId },
func: async () => {
window.scrollTo(0, document.body.scrollHeight);
await new Promise(r => setTimeout(r, 2000));
const reviews = [];
document.querySelectorAll('#release-reviews [data-username], #release-reviews [class*="review_card"]')
.forEach(card => {
const username = card.getAttribute('data-username')
|| card.querySelector('[class*="username"]')?.textContent?.trim() || null;
const dateEl = card.querySelector('time');
const date = dateEl?.getAttribute('datetime')?.split('T')[0]
|| dateEl?.textContent?.trim() || null;
const text = card.querySelector('[class*="review_body"], [class*="reviewBody"]')
?.textContent?.trim() || null;
const rating = card.querySelectorAll('[class*="star_full"],[class*="starFull"],[class*="filled"]').length || null;
const helpful = parseInt(card.querySelector('[class*="helpful"]')?.textContent?.match(/\d+/)?.[0]) || 0;
if (username && (text || rating))
reviews.push({ username, date, rating: rating||null, text, helpful, replies: [] });
});
return reviews;
}
});
if (results?.[0]?.result?.length) liveReviews = results[0].result;
} catch (_) {}
}
// Merge live DOM reviews into releaseData for the POST
const postData = { ...state.releaseData, reviews: liveReviews };
try {
const resp = await fetch(`${base}/plice`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...buildResearchJson(postData),
sales_history: state.monsterSalesHistory,
current_listings: state.monsterCurrentListings,
}),
signal: AbortSignal.timeout(12000),
});
if (resp.ok) {
const d = await resp.json();
const sales = d.sale_rows || 0;
const listings = d.listing_rows || 0;
const reviews = d.review_rows || 0;
monsterSetIndicator(
`✓ MONSTER — ${sales} sales · ${listings} listings · ${reviews} reviews`,
'#2a7a2a', 4000
);
} else {
monsterSetIndicator(`✗ MONSTER ${resp.status}`, '#b00', 4000);
}
} catch (e) {
monsterSetIndicator(
e.name === 'TimeoutError' ? '✗ MONSTER timeout' : '✗ MONSTER unreachable',
'#b00', 4000
);
}
}
// YouTube Cookie Management Functions
function extractCookies() {
const button = document.getElementById('getCookies');
const textarea = document.getElementById('cookieOutput');
if (!button || !textarea) return; // Elements don't exist on this page
// Check if chrome runtime is available
if (typeof chrome === 'undefined' || !chrome.runtime) {
textarea.value = 'Extension functionality not available in preview mode.\nInstall as Chrome extension to use YouTube cookie extraction.';
return;
}
// Show loading state
button.disabled = true;
button.textContent = 'Extracting...';
textarea.value = 'Extracting cookies...';
chrome.runtime.sendMessage({ action: "getCookies" }, async function (response) {
if (response && response.success && response.cookieText) {
textarea.value = response.cookieText;
// Try to copy to clipboard
let copied = false;
try {
await navigator.clipboard.writeText(response.cookieText);
copied = true;
} catch (err) {
// console.error('Failed to copy:', err);
}
button.textContent = copied ? 'Cookies Saved & Copied!' : 'Cookies Saved!';
button.style.backgroundColor = '#45a049';
// Reset button after delay
setTimeout(() => {
button.textContent = 'Extract Cookies';
button.style.backgroundColor = '#4CAF50';
button.disabled = false;
}, 2000);
} else {
// Handle error
console.error('Error response:', response);
textarea.value = response?.error || 'Failed to extract cookies';
button.textContent = 'Error - Try Again';
button.style.backgroundColor = '#f44336';
button.disabled = false;
setTimeout(() => {
button.textContent = 'Extract Cookies';
button.style.backgroundColor = '#4CAF50';
}, 3000);
}
});
}
function initializeYouTubeCookieUI() {
const toggle = document.getElementById('autoUpdate');
const blagginateToggle = document.getElementById('blagginateBridge');
const textarea = document.getElementById('cookieOutput');
const button = document.getElementById('getCookies');
const container = document.querySelector('.youtube-cookie-container');
if (!toggle || !blagginateToggle || !textarea || !button || !container) return; // Elements don't exist
// Check if chrome.storage is available (extension context)
if (typeof chrome !== 'undefined' && chrome.storage) {
// Load initial state
chrome.storage.local.get(['autoUpdate', 'blagginateBridge', 'lastCookie', 'lastUpdate'], function (data) {
// Set toggle states
toggle.checked = data.autoUpdate === true;
blagginateToggle.checked = data.blagginateBridge !== false; // Default to true
// Show last saved cookie if available
if (data.lastCookie) {
const lastUpdateTime = new Date(data.lastUpdate).toLocaleString();
textarea.value = `Last saved on ${lastUpdateTime}:\n\n${data.lastCookie}`;
} else {
textarea.value = 'No cookies saved yet. Click "Extract Cookies" to get YouTube cookies.';
}
// If auto-update is enabled, get fresh cookies
if (data.autoUpdate === true) {
extractCookies();
}
});
// Handle auto-update toggle changes
toggle.addEventListener('change', function (e) {
const isEnabled = e.target.checked;
chrome.storage.local.set({ autoUpdate: isEnabled }, function () {
if (isEnabled) {
extractCookies();
}
});
});
// Handle BLAGGINATE bridge toggle changes
blagginateToggle.addEventListener('change', function (e) {
const isEnabled = e.target.checked;
chrome.storage.local.set({ blagginateBridge: isEnabled }, function () {
// Send message to background script to enable/disable bridge
chrome.runtime.sendMessage({
type: 'toggle_blagginate_bridge',
enabled: isEnabled
});
console.log('BLAGGINATE bridge', isEnabled ? 'enabled' : 'disabled');
});
});
} else {
// Fallback for non-extension context (preview mode)
textarea.value = 'Extension functionality not available in preview mode.\nInstall as Chrome extension to use YouTube cookie extraction.';
toggle.disabled = true;
blagginateToggle.disabled = true;
button.disabled = true;
}
// Handle button click
button.addEventListener('click', extractCookies);
// JDownloader 2 button: pull fresh YT cookies as Cookie-Editor JSON and copy to clipboard.
const jdBtn = document.getElementById('getCookiesJd2');
if (jdBtn) {
jdBtn.addEventListener('click', () => {
const orig = jdBtn.textContent;
jdBtn.disabled = true;
jdBtn.textContent = 'Fetching...';
chrome.runtime.sendMessage({ action: 'getCookiesJson' }, async (response) => {
if (response && response.success && response.cookieJson) {
try {
await navigator.clipboard.writeText(response.cookieJson);
textarea.value = response.cookieJson;
jdBtn.textContent = 'Copied! Paste into JD2 password field';
jdBtn.style.background = '#45a049';
} catch (err) {
textarea.value = response.cookieJson;
jdBtn.textContent = 'Shown below — copy manually';
jdBtn.style.background = '#f39c12';
}
} else {
jdBtn.textContent = 'Failed: ' + ((response && response.error) || 'unknown');
jdBtn.style.background = '#f44336';
}
setTimeout(() => {
jdBtn.textContent = orig;
jdBtn.style.background = '#2196F3';
jdBtn.disabled = false;
}, 2500);
});
});
}
}
// Tab switching functionality
function initializeTabs() {
const tabButtons = document.querySelectorAll('.tab-button');
const tabContents = document.querySelectorAll('.tab-content');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
const targetTab = button.getAttribute('data-tab');
// Remove active class from all buttons and contents
tabButtons.forEach(btn => btn.classList.remove('active'));
tabContents.forEach(content => content.classList.remove('active'));
// Add active class to clicked button and corresponding content
button.classList.add('active');
document.getElementById(`${targetTab}-tab`).classList.add('active');
});
});
}
// Save functions - defined before DOMContentLoaded to ensure availability
function saveDiscogsToken() {
if (!chrome || !chrome.storage) {
alert('Chrome storage not available in preview mode');
return;
}
const token = document.getElementById('discogs-token').value.trim();
console.log('Saving Discogs token:', { hasToken: !!token });
if (token) {
chrome.storage.local.set({ discogsToken: token }, () => {
console.log('Discogs token saved successfully to chrome.storage.local');
if (chrome.runtime.lastError) {
console.error('Error saving Discogs token:', chrome.runtime.lastError);
alert('Error saving Discogs token: ' + chrome.runtime.lastError.message);
}
});
} else {
console.log('No Discogs token to save (field empty)');
}
}
function saveGoogleSettings() {
if (!chrome || !chrome.storage) {
alert('Chrome storage not available in preview mode');
return;
}
const spreadsheetId = document.getElementById('spreadsheet-id').value;
const serviceAccountJson = document.getElementById('google-service-account-json').value;
console.log('Saving Google settings:', { spreadsheetId, hasServiceAccount: !!serviceAccountJson });
let googleSettings = {};
if (spreadsheetId) {
googleSettings.spreadsheetId = spreadsheetId;
}
if (serviceAccountJson) {
try {
const serviceAccount = JSON.parse(serviceAccountJson);
if (serviceAccount.client_email && serviceAccount.private_key) {
googleSettings.clientEmail = serviceAccount.client_email;
googleSettings.privateKey = serviceAccount.private_key;
}
} catch (error) {
console.error('Invalid JSON in service account field:', error);
alert('Invalid JSON format in Service Account JSON field');
return;
}
}
if (Object.keys(googleSettings).length > 0) {
chrome.storage.local.set({ googleSettings: googleSettings }, () => {
console.log('Google settings saved successfully to chrome.storage.local');
if (chrome.runtime.lastError) {
console.error('Error saving Google settings:', chrome.runtime.lastError);
alert('Error saving Google settings: ' + chrome.runtime.lastError.message);
}
});
} else {
console.log('No Google settings to save (all fields empty)');
}
}
document.addEventListener('DOMContentLoaded', function () {
// Initialize tab functionality first
initializeTabs();
// Initialize mapping tab
initializeMappingTab();
// Initialize YouTube cookie functionality
initializeYouTubeCookieUI();
// Load auto-update Discogs price setting
const autoUpdatePriceCheckbox = document.getElementById('autoUpdateDiscogsPrice');
if (autoUpdatePriceCheckbox && chrome && chrome.storage) {
chrome.storage.local.get(['autoUpdateDiscogsPrice'], (result) => {
// Default to true if not set
autoUpdatePriceCheckbox.checked = result.autoUpdateDiscogsPrice !== false;
});
// Save setting when changed
autoUpdatePriceCheckbox.addEventListener('change', () => {
chrome.storage.local.set({ autoUpdateDiscogsPrice: autoUpdatePriceCheckbox.checked });
console.log('Auto-update Discogs price setting saved:', autoUpdatePriceCheckbox.checked);
});
}
// Load and persist QR SKU toggle; show indicator on Discogs tab
const qrUseSkuCheckbox = document.getElementById('qr-use-sku');
const skuIndicator = document.getElementById('sku-mode-indicator');
const skuIndicatorStatus = document.getElementById('sku-mode-status');
function updateSkuIndicator(on) {
if (!skuIndicator) return;
skuIndicator.style.display = 'block';
skuIndicatorStatus.textContent = on ? 'ON' : 'OFF';
skuIndicatorStatus.style.color = on ? '#28a745' : '#dc3545';
}
if (qrUseSkuCheckbox && chrome && chrome.storage) {
chrome.storage.local.get(['qrUseSku'], (result) => {
qrUseSkuCheckbox.checked = result.qrUseSku === true;
updateSkuIndicator(result.qrUseSku === true);
});
qrUseSkuCheckbox.addEventListener('change', () => {
chrome.storage.local.set({ qrUseSku: qrUseSkuCheckbox.checked });
updateSkuIndicator(qrUseSkuCheckbox.checked);
});
}
// Load enable JSON download setting
const enableJsonDownloadCheckbox = document.getElementById('enableJsonDownload');
if (enableJsonDownloadCheckbox && chrome && chrome.storage) {
chrome.storage.local.get(['enableJsonDownload'], (result) => {
// Default to true if not set
enableJsonDownloadCheckbox.checked = result.enableJsonDownload !== false;
});
// Save setting when changed
enableJsonDownloadCheckbox.addEventListener('change', () => {
chrome.storage.local.set({ enableJsonDownload: enableJsonDownloadCheckbox.checked });
console.log('Enable JSON download setting saved:', enableJsonDownloadCheckbox.checked);
});
}
// ── Settings: ALL config + RFID + visibility ─────────────────────────────
const settingsConfig = [
// [storageKey, elementId, defaultValue]
['allDoesLabel', 'allDoesLabel', true],
['allDoesSheet', 'allDoesSheet', true],
['allDoesRfid', 'allDoesRfid', true],
['allDoesJson', 'allDoesJson', true],
['allDoesPrice', 'allDoesPrice', true],
['qrUseSku', 'qrUseSku', false],
['rfidIncludeReleaseId', 'rfidIncludeReleaseId', false],
['rfidAppendR', 'rfidAppendR', false],
['btnShowLabel', 'btnShowLabel', true],
['btnShowRfid', 'btnShowRfid', true],
['btnShowSheet', 'btnShowSheet', true],
['btnShowJson', 'btnShowJson', true],
['btnShowInventory', 'btnShowInventory', true],
['btnShowLabel203', 'btnShowLabel203', false],
['enableJsonDownload', 'enableJsonDownload', true],
['stripSkuSuffix', 'stripSkuSuffix', false],
];
const allKeys = settingsConfig.map(c => c[0]);
chrome.storage.local.get(allKeys, (stored) => {
for (const [key, id, def] of settingsConfig) {
const el = document.getElementById(id);
if (!el) continue;
el.checked = stored[key] !== undefined ? stored[key] : def;
el.addEventListener('change', () => {
chrome.storage.local.set({ [key]: el.checked });
if (id.startsWith('btnShow')) applyButtonVisibility(stored);
if (id === 'stripSkuSuffix') {
window.stripSkuSuffix = el.checked;
if (state.releaseData) updatePreview(state.releaseData);
}
});
}
window.stripSkuSuffix = stored.stripSkuSuffix === true;
applyButtonVisibility(stored);
});
function applyButtonVisibility(stored) {
// Re-read live from checkboxes (they may have just changed)
const showLabel = document.getElementById('btnShowLabel')?.checked ?? true;
const showRfid = document.getElementById('btnShowRfid')?.checked ?? true;
const showSheet = document.getElementById('btnShowSheet')?.checked ?? true;
const showJson = document.getElementById('btnShowJson')?.checked ?? true;
const showInventory = document.getElementById('btnShowInventory')?.checked ?? true;
const showLabel203 = document.getElementById('btnShowLabel203')?.checked ?? false;
const setVis = (id, show) => {
const el = document.getElementById(id);
if (el) el.style.display = show ? '' : 'none';
};
setVis('printLabel', showLabel);
setVis('printLabel203', showLabel203);
setVis('rfidQuickWrite', showRfid);
setVis('updateSheet', showSheet);
setVis('writeJSON', showJson);
setVis('importWordPress', showInventory);
// Hide small row entirely if all buttons hidden
const smallRow = document.getElementById('buttons-small-row');
if (smallRow) smallRow.style.display = (showSheet || showJson || showInventory) ? '' : 'none';
}
function applyLabelPreviewVisibility() {
const size = document.getElementById('labelSize')?.value || 'large';
const previewSmall = document.querySelector('.label-preview');
const previewLarge = document.querySelector('.label-preview-large');
const previewXLarge = document.querySelector('.label-preview-xlarge');
if (previewSmall) previewSmall.style.display = size === 'small' ? '' : 'none';
if (previewLarge) previewLarge.style.display = size === 'large' ? '' : 'none';
if (previewXLarge) previewXLarge.style.display = size === 'xlarge' ? '' : 'none';
}
// ── Label settings (font/bold/italic/size + per-field styles) ────────────
const LFS_FIELDS = ['artist','title','genre','style','format','info','price','condition','comment','notes'];
function applyLabelFieldStyles(lfs) {
window.labelFieldStyles = lfs || {};
LFS_FIELDS.forEach(f => {
const s = (lfs || {})[f] || {};
const bEl = document.getElementById(`lfs-${f}-b`);
const iEl = document.getElementById(`lfs-${f}-i`);
const eEl = document.getElementById(`lfs-${f}-e`);
if (bEl) bEl.checked = !!s.b;
if (iEl) iEl.checked = !!s.i;
if (eEl) eEl.checked = !!s.e;
});
}
function readLabelFieldStyles() {
const lfs = {};
LFS_FIELDS.forEach(f => {
lfs[f] = {
b: !!(document.getElementById(`lfs-${f}-b`)?.checked),
i: !!(document.getElementById(`lfs-${f}-i`)?.checked),
e: !!(document.getElementById(`lfs-${f}-e`)?.checked),
};
});
return lfs;
}
chrome.storage.local.get(['labelFont', 'labelBold', 'labelItalic', 'labelSize', 'labelFieldStyles'], (ls) => {
const fontSel = document.getElementById('label-font-select');
const boldCb = document.getElementById('label-bold-toggle');
const italicCb = document.getElementById('label-italic-toggle');
const sizeSel = document.getElementById('labelSize');
if (fontSel && ls.labelFont) fontSel.value = ls.labelFont;
if (boldCb) boldCb.checked = ls.labelBold === true;
if (italicCb) italicCb.checked = ls.labelItalic === true;
if (sizeSel && ls.labelSize) sizeSel.value = ls.labelSize;
applyLabelPreviewVisibility();
applyLabelFieldStyles(ls.labelFieldStyles);
const onLabelChange = () => {
const lfs = readLabelFieldStyles();
window.labelFieldStyles = lfs;
chrome.storage.local.set({
labelFont: fontSel?.value,
labelBold: boldCb?.checked,
labelItalic: italicCb?.checked,
labelSize: sizeSel?.value,
labelFieldStyles: lfs,
});
if (state.releaseData) updatePreview(state.releaseData);
};
fontSel?.addEventListener('change', onLabelChange);
boldCb?.addEventListener('change', onLabelChange);
italicCb?.addEventListener('change', onLabelChange);
sizeSel?.addEventListener('change', onLabelChange);
// Wire per-field checkboxes
LFS_FIELDS.forEach(f => {
['b','i','e'].forEach(prop => {
document.getElementById(`lfs-${f}-${prop}`)?.addEventListener('change', onLabelChange);
});
});
});
// Check if chrome.tabs is available (extension context)
if (typeof chrome !== 'undefined' && chrome.tabs) {
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
if (!tabs || !tabs[0] || !tabs[0].url) {
console.error('No active tab or URL found');
return;
}
const url = tabs[0].url;
const releaseMatch = url.match(/\/release\/(\d+)/);
const masterMatch = url.match(/\/master\/(\d+)/);
const styleMatch = url.match(/\/style\/([^\/]+)/);
const isYouTube = url.includes('youtube.com');
// Determine which tab to show based on the current page
if (isYouTube) {
document.querySelector('.tab-button[data-tab="youtube"]').click();
} else if (releaseMatch || styleMatch || masterMatch) {
document.querySelector('.tab-button[data-tab="discogs"]').click();
} else {
document.querySelector('.tab-button[data-tab="settings"]').click();
}
if (styleMatch) {
document.getElementById('stylePageContainer').style.display = 'block';
} else if (releaseMatch) {
fetchAndDisplayReleaseData(releaseMatch[1]);
} else if (masterMatch) {
showMasterPageStatus(masterMatch[1], tabs[0].id);
}
});
loadWpSettings();
loadGoogleSettings();
loadInventorySettings();
loadDiscogsToken();
(function () { var el = document.getElementById('extension-id-display'); if (el) { if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.id) { el.textContent = chrome.runtime.id; } } })();
// Copy Extension ID button
(function () {
var btn = document.getElementById('copy-extension-id');
if (btn) {
btn.addEventListener('click', async function () {
var id = (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.id) ? chrome.runtime.id : (document.getElementById('extension-id-display').textContent || '').trim();
try {
await navigator.clipboard.writeText(id);
var status = document.getElementById('copy-extension-id-status');
if (status) {
status.textContent = 'Copied!';
status.style.display = 'inline';
setTimeout(function () { status.style.display = 'none'; }, 1500);
}
} catch (e) {
try {
var temp = document.createElement('textarea');
temp.value = id;
document.body.appendChild(temp);
temp.select();
document.execCommand('copy');
document.body.removeChild(temp);
var status2 = document.getElementById('copy-extension-id-status');
if (status2) {
status2.textContent = 'Copied!';
status2.style.display = 'inline';
setTimeout(function () { status2.style.display = 'none'; }, 1500);
}
} catch (err2) {
console.error('Clipboard copy failed:', err2);
alert('Could not copy extension ID. Please copy manually.');
}
}
});
}
})();
}
// Add all event listeners inside DOMContentLoaded to ensure DOM elements exist
document.getElementById('settings-toggle-connect').addEventListener('click', () => {
document.getElementById('settings-content-connect').style.display = document.getElementById('settings-content-connect').style.display === 'none' ? 'block' : 'none';
});
document.getElementById('save-connect-settings').addEventListener('click', () => {
console.log('Save Connect Settings button clicked!');
// Save Google Settings
if (!chrome || !chrome.storage) {
alert('Chrome storage not available in preview mode');
return;
}
const spreadsheetId = document.getElementById('spreadsheet-id').value;
const serviceAccountJson = document.getElementById('google-service-account-json').value;
console.log('Saving Google settings:', { spreadsheetId, hasServiceAccount: !!serviceAccountJson });
let googleSettings = {};
if (spreadsheetId) {
googleSettings.spreadsheetId = spreadsheetId;
}
if (serviceAccountJson) {
try {
const serviceAccount = JSON.parse(serviceAccountJson);
if (serviceAccount.client_email && serviceAccount.private_key) {
googleSettings.clientEmail = serviceAccount.client_email;
googleSettings.privateKey = serviceAccount.private_key;
}
} catch (error) {
console.error('Invalid JSON in service account field:', error);
alert('Invalid JSON format in Service Account JSON field');
return;
}
}
if (Object.keys(googleSettings).length > 0) {
chrome.storage.local.set({ googleSettings: googleSettings }, () => {
console.log('Google settings saved successfully to chrome.storage.local');
if (chrome.runtime.lastError) {
console.error('Error saving Google settings:', chrome.runtime.lastError);
alert('Error saving Google settings: ' + chrome.runtime.lastError.message);
} else {
alert('Google settings saved successfully!');
}
});
} else {
console.log('No Google settings to save (all fields empty)');
}
// Save Discogs Token
const discogsToken = document.getElementById('discogs-token').value.trim();
if (discogsToken) {
chrome.storage.local.set({ discogsToken: discogsToken }, () => {
console.log('Discogs token saved successfully to chrome.storage.local');
if (chrome.runtime.lastError) {
console.error('Error saving Discogs token:', chrome.runtime.lastError);
alert('Error saving Discogs token: ' + chrome.runtime.lastError.message);
}
});
} else {
console.log('No Discogs token to save (field empty)');
}
// Save Inventory SKU Lookup settings
const inventorySettings = {
enabled: document.getElementById('inventorySkuEnabled').checked,
dbHost: document.getElementById('inventory-db-host').value.trim() || '100.123.123.64',
dbName: document.getElementById('inventory-db-name').value.trim() || 'wp_rmp_disc_',
dbUser: document.getElementById('inventory-db-user').value.trim(),
dbPass: document.getElementById('inventory-db-pass').value.trim(),
sshKeyPath: document.getElementById('inventory-ssh-key').value.trim() || '~/.ssh/id_rsa'
};
chrome.storage.local.set({ inventorySettings }, () => {
if (chrome.runtime.lastError) {
console.error('Error saving inventory settings:', chrome.runtime.lastError);
alert('Error saving inventory settings: ' + chrome.runtime.lastError.message);
} else {
console.log('Inventory settings saved successfully.');
}
});
});
// MONSTERWIKI settings handlers
document.getElementById('settings-toggle-monster').addEventListener('click', () => {
const el = document.getElementById('settings-content-monster');
el.style.display = el.style.display === 'none' ? 'block' : 'none';
});
document.getElementById('save-monster-settings').addEventListener('click', () => {
const enabled = document.getElementById('monster-enabled').checked;
const autoCapture = document.getElementById('monster-autocapture')?.checked === true;
const preferLocalhost = document.getElementById('monster-prefer-localhost')?.checked === true;
const serverUrl = document.getElementById('monster-server-url').value.trim()
|| 'http://100.91.239.7:5002';
chrome.storage.local.set({ monsterSettings: { enabled, autoCapture, preferLocalhost, serverUrl } }, () => {
document.getElementById('monster-status').textContent = 'Saved.';
setTimeout(() => { document.getElementById('monster-status').textContent = ''; }, 2000);
});
});
document.getElementById('test-monster-connection').addEventListener('click', async () => {
const statusEl = document.getElementById('monster-status');
statusEl.textContent = 'Testing…';
const cfg = await getMonsterSettings();
const base = (cfg.serverUrl || 'http://100.91.239.7:5002').replace(/\/$/, '');
try {
const r = await fetch(`${base}/plice/health`, { signal: AbortSignal.timeout(5000) });
statusEl.textContent = r.ok ? '✓ Connected' : `✗ HTTP ${r.status}`;
} catch (e) {
statusEl.textContent = `${e.message.includes('abort') ? 'Timeout' : 'Unreachable'}`;
}
});
// Load saved MONSTERWIKI settings into fields
chrome.storage.local.get(['monsterSettings'], r => {
const cfg = r.monsterSettings || {};
if (document.getElementById('monster-enabled'))
document.getElementById('monster-enabled').checked = cfg.enabled === true;
if (document.getElementById('monster-autocapture'))
document.getElementById('monster-autocapture').checked = cfg.autoCapture === true;
if (document.getElementById('monster-prefer-localhost'))
document.getElementById('monster-prefer-localhost').checked = cfg.preferLocalhost === true;
if (document.getElementById('monster-server-url'))
document.getElementById('monster-server-url').value = cfg.serverUrl || 'http://100.91.239.7:5002';
});
const geminiToggle = document.getElementById('settings-toggle-gemini');
if (geminiToggle) {
geminiToggle.addEventListener('click', () => {
const el = document.getElementById('settings-content-gemini');
el.style.display = el.style.display === 'none' ? 'block' : 'none';
});
}
const geminiTemplateEl = document.getElementById('gemini-prompt-template');
const geminiStatusEl = document.getElementById('gemini-prompt-status');
const geminiCloseTabEl = document.getElementById('gemini-close-tab');
const geminiAutoRunEl = document.getElementById('gemini-auto-run');
const aiProviderEl = document.getElementById('ai-provider');
chrome.storage.local.get(['geminiPromptTemplate', 'geminiCloseTabOnSuccess', 'geminiAutoRunOnPopup', 'aiProvider'], r => {
if (!geminiTemplateEl) return;
geminiTemplateEl.value = (r.geminiPromptTemplate && typeof r.geminiPromptTemplate === 'string')
? r.geminiPromptTemplate
: defaultGeminiPromptTemplate();
if (geminiCloseTabEl) geminiCloseTabEl.checked = r.geminiCloseTabOnSuccess !== false;
if (geminiAutoRunEl) geminiAutoRunEl.checked = r.geminiAutoRunOnPopup === true;
if (aiProviderEl) aiProviderEl.value = (r.aiProvider === 'deepseek') ? 'deepseek' : 'gemini';
});
if (geminiCloseTabEl) {
geminiCloseTabEl.addEventListener('change', () => {
chrome.storage.local.set({ geminiCloseTabOnSuccess: geminiCloseTabEl.checked === true });
});
}
if (geminiAutoRunEl) {
geminiAutoRunEl.addEventListener('change', () => {
chrome.storage.local.set({ geminiAutoRunOnPopup: geminiAutoRunEl.checked === true }, () => {
if (geminiStatusEl) geminiStatusEl.textContent = geminiAutoRunEl.checked ? 'Auto-run enabled.' : 'Auto-run disabled.';
setTimeout(() => { if (geminiStatusEl) geminiStatusEl.textContent = ''; }, 1500);
});
});
}
if (aiProviderEl) {
aiProviderEl.addEventListener('change', () => {
const v = aiProviderEl.value === 'deepseek' ? 'deepseek' : 'gemini';
chrome.storage.local.set({ aiProvider: v }, () => {
if (geminiStatusEl) geminiStatusEl.textContent = `Provider: ${v}`;
setTimeout(() => { if (geminiStatusEl) geminiStatusEl.textContent = ''; }, 1500);
});
});
}
const saveGeminiBtn = document.getElementById('save-gemini-prompt');
if (saveGeminiBtn) {
saveGeminiBtn.addEventListener('click', () => {
if (!geminiTemplateEl) return;
const tpl = geminiTemplateEl.value || '';
const closeOnSuccess = geminiCloseTabEl ? geminiCloseTabEl.checked === true : true;
const autoRun = geminiAutoRunEl ? geminiAutoRunEl.checked === true : false;
chrome.storage.local.set({ geminiPromptTemplate: tpl, geminiCloseTabOnSuccess: closeOnSuccess, geminiAutoRunOnPopup: autoRun }, () => {
if (geminiStatusEl) geminiStatusEl.textContent = 'Saved.';
setTimeout(() => { if (geminiStatusEl) geminiStatusEl.textContent = ''; }, 2000);
});
});
}
const resetGeminiBtn = document.getElementById('reset-gemini-prompt');
if (resetGeminiBtn) {
resetGeminiBtn.addEventListener('click', () => {
if (!geminiTemplateEl) return;
geminiTemplateEl.value = defaultGeminiPromptTemplate();
const closeOnSuccess = geminiCloseTabEl ? geminiCloseTabEl.checked === true : true;
const autoRun = geminiAutoRunEl ? geminiAutoRunEl.checked === true : false;
chrome.storage.local.set({ geminiPromptTemplate: geminiTemplateEl.value, geminiCloseTabOnSuccess: closeOnSuccess, geminiAutoRunOnPopup: autoRun }, () => {
if (geminiStatusEl) geminiStatusEl.textContent = 'Reset.';
setTimeout(() => { if (geminiStatusEl) geminiStatusEl.textContent = ''; }, 2000);
});
});
}
chrome.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== 'local') return;
if (!changes.geminiJob) return;
const job = changes.geminiJob.newValue;
if (!job) return;
if (state.releaseData?.id && job.release_id && job.release_id !== state.releaseData.id) return;
const prov = job.provider === 'deepseek' ? 'DeepSeek' : 'Gemini';
if (job.status === 'posting') {
geminiSetIndicator(`${prov} saving…`, '#888');
} else if (job.status === 'done') {
geminiSetIndicator(`${prov} done`, '#28a745', 2500);
} else if (job.status === 'error') {
geminiSetIndicator(`${prov}: ${job.error || 'error'}`, '#dc3545');
}
});
document.getElementById('settings-toggle-wowplatter').addEventListener('click', () => {
document.getElementById('settings-content-wowplatter').style.display = document.getElementById('settings-content-wowplatter').style.display === 'none' ? 'block' : 'none';
});
document.getElementById('save-wp-settings').addEventListener('click', () => {
saveWpSettings();
// Note: saveWpSettings() already shows an alert, so we don't need another one
});
document.getElementById('test-wp-connection').addEventListener('click', testWpConnection);
// Keep the old save-google-settings for backward compatibility (if needed)
document.getElementById('save-google-settings')?.addEventListener('click', () => {
if (!chrome || !chrome.storage) {
alert('Chrome storage not available in preview mode');
return;
}
const spreadsheetId = document.getElementById('spreadsheet-id').value;
const jsonText = document.getElementById('google-service-account-json').value;
try {
const serviceAccount = JSON.parse(jsonText);
chrome.storage.local.set({
googleSettings: {
spreadsheetId,
clientEmail: serviceAccount.client_email,
privateKey: serviceAccount.private_key
}
}, () => alert('Google settings saved!'));
} catch (error) {
alert(`Error parsing service account JSON: ${error.message}`);
}
});
// Mapping tab event listeners
document.getElementById('save-mappings')?.addEventListener('click', saveMappings);
document.getElementById('load-mappings')?.addEventListener('click', loadMappings);
document.getElementById('reset-mappings')?.addEventListener('click', resetMappings);
document.getElementById('detect-sheet')?.addEventListener('click', detectSheetStructureUI);
document.getElementById('auto-map-headers')?.addEventListener('click', autoMapHeaders);
document.getElementById('toggle-manual-range')?.addEventListener('click', toggleManualRange);
});
function loadGoogleSettings() {
if (!chrome || !chrome.storage) {
console.log('Chrome storage not available in preview mode');
return;
}
chrome.storage.local.get('googleSettings', (data) => {
console.log('Loading Google settings:', data.googleSettings);
if (data.googleSettings) {
document.getElementById('spreadsheet-id').value = data.googleSettings.spreadsheetId || '';
const serviceAccount = {
client_email: data.googleSettings.clientEmail,
private_key: data.googleSettings.privateKey
};
if (data.googleSettings.clientEmail && data.googleSettings.privateKey) {
document.getElementById('google-service-account-json').value = JSON.stringify(serviceAccount, null, 2);
}
console.log('Google settings loaded successfully');
} else {
console.log('No Google settings found in storage');
}
});
}
function loadDiscogsToken() {
if (!chrome || !chrome.storage) {
console.log('Chrome storage not available in preview mode');
return;
}
chrome.storage.local.get('discogsToken', (data) => {
console.log('Loading Discogs token:', { hasToken: !!data.discogsToken });
if (data.discogsToken) {
document.getElementById('discogs-token').value = (data.discogsToken || '').trim();
console.log('Discogs token loaded successfully');
} else {
console.log('No Discogs token found in storage');
}
});
}
function loadInventorySettings() {
if (!chrome || !chrome.storage) {
console.log('Chrome storage not available in preview mode');
return;
}
chrome.storage.local.get(['inventorySettings'], (result) => {
const s = result.inventorySettings || {};
const el = (id) => document.getElementById(id);
if (el('inventorySkuEnabled')) el('inventorySkuEnabled').checked = !!s.enabled;
if (el('inventory-db-host')) el('inventory-db-host').value = s.dbHost || '100.123.123.64';
if (el('inventory-db-name')) el('inventory-db-name').value = s.dbName || 'wp_rmp_disc_';
if (el('inventory-db-user')) el('inventory-db-user').value = s.dbUser || '';
if (el('inventory-db-pass')) el('inventory-db-pass').value = s.dbPass || '';
if (el('inventory-ssh-key')) el('inventory-ssh-key').value = s.sshKeyPath || '~/.ssh/id_rsa';
console.log('Inventory settings loaded successfully');
});
}
function makeEditable(field) {
const valueElement = field.querySelector('.value');
if (valueElement && valueElement.getAttribute('contenteditable') === 'true') {
valueElement.addEventListener('input', () => {
const fieldName = field.dataset.field;
if (state.releaseData) {
state.releaseData[fieldName] = valueElement.textContent;
updatePreview(state.releaseData);
}
});
}
}
function setupEventListeners() {
const elements = {
priceInput: document.getElementById('price'),
priceCheckboxes: document.querySelectorAll('.price-checkbox'),
sleeveCondition: document.getElementById('sleeveCondition'),
mediaCondition: document.getElementById('mediaCondition'),
commentInput: document.getElementById('commentInput'),
labelSize: document.getElementById('labelSize'),
};
const updateState = (key, value) => {
if (state.releaseData) {
state.releaseData[key] = value;
updatePreview(state.releaseData);
}
};
elements.priceInput.addEventListener('input', () => {
document.querySelectorAll('.price-checkbox').forEach(cb => cb.checked = false);
updateState('price', elements.priceInput.value);
});
// Also update on focus/blur/keyup/paste to ensure we catch all changes
elements.priceInput.addEventListener('focus', () => {
updateState('price', elements.priceInput.value);
});
elements.priceInput.addEventListener('blur', () => {
updateState('price', elements.priceInput.value);
});
elements.priceInput.addEventListener('keyup', () => {
updateState('price', elements.priceInput.value);
});
elements.priceInput.addEventListener('paste', () => {
setTimeout(() => updateState('price', elements.priceInput.value), 10);
});
elements.priceCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
elements.priceCheckboxes.forEach(cb => { if (cb !== checkbox) cb.checked = false; });
elements.priceInput.value = checkbox.value;
updateState('price', checkbox.value);
}
});
});
elements.mediaCondition.addEventListener('change', () => updateState('mediaCondition', elements.mediaCondition.value));
elements.sleeveCondition.addEventListener('change', () => updateState('sleeveCondition', elements.sleeveCondition.value));
// Update comment on input, focus, blur, keyup and paste to ensure we catch all changes
elements.commentInput.addEventListener('input', () => updateState('comment', elements.commentInput.textContent));
elements.commentInput.addEventListener('focus', () => updateState('comment', elements.commentInput.textContent));
elements.commentInput.addEventListener('blur', () => updateState('comment', elements.commentInput.textContent));
elements.commentInput.addEventListener('keyup', () => updateState('comment', elements.commentInput.textContent));
elements.commentInput.addEventListener('paste', () => {
setTimeout(() => updateState('comment', elements.commentInput.textContent), 10);
});
elements.labelSize.addEventListener('change', () => { applyLabelPreviewVisibility(); updatePreview(state.releaseData); });
// QR SKU toggle: switch between release_id and SKU for preview
const qrSkuToggle = document.getElementById('qr-use-sku');
if (qrSkuToggle) {
qrSkuToggle.addEventListener('change', () => {
if (!state.releaseData) return;
if (qrSkuToggle.checked) {
// Prefer existing Discogs SKU (reprint), else use the one generated at popup open
state.releaseData.sku = state.releaseData.collectionSku || state.sku;
} else {
delete state.releaseData.sku;
}
updatePreview(state.releaseData);
});
}
document.querySelectorAll('.info-field').forEach(makeEditable);
document.getElementById('printLabel').addEventListener('click', () => generateLabel(state.releaseData));
document.getElementById('printLabel203').addEventListener('click', () => generateLabel203dpi());
document.getElementById('writeJSON').addEventListener('click', async (e) => {
if (e.shiftKey) {
// Shift+click = upload JSON and merge into state
const data = await uploadJSON();
if (data) {
state.releaseData = { ...state.releaseData, ...data };
displayReleaseInfo(state.releaseData);
updatePreview(state.releaseData);
updateResearchTab();
showStatusMessage('allStatus', 'JSON loaded from file', true);
}
} else {
writeJSON(state.releaseData);
}
});
// BP JSON button — copy current Beatport token JSON to clipboard
const bpJsonBtn = document.getElementById('copyBeatportJson');
if (bpJsonBtn) {
bpJsonBtn.addEventListener('click', () => {
chrome.storage.local.get(['beatportToken'], (result) => {
const tokenJson = result.beatportToken;
if (!tokenJson) {
showStatusMessage('allStatus', 'No Beatport token saved — fetch it in Settings first', false);
return;
}
navigator.clipboard.writeText(tokenJson).then(() => {
const orig = bpJsonBtn.textContent;
bpJsonBtn.textContent = 'Copied!';
setTimeout(() => { bpJsonBtn.textContent = orig; }, 1500);
}).catch(() => {
showStatusMessage('allStatus', 'Clipboard write failed', false);
});
});
});
}
document.getElementById('updateSheet').addEventListener('click', async () => {
// console.log('=== GOOGLE SHEETS BUTTON CLICKED ===');
// console.log('Current state.releaseData:', state.releaseData);
if (state.releaseData) {
const result = await updateSheetOnly(state.releaseData);
showStatusMessage('sheetStatus', result.message, result.success);
}
});
document.getElementById('bothActions').addEventListener('click', async () => await handleAllActions(state.releaseData));
// RFID quick-READ from Discogs tab — shows what's currently on the tag without writing
const rfidReadBtn = document.getElementById('rfidQuickRead');
if (rfidReadBtn) {
rfidReadBtn.addEventListener('click', async () => {
const rfidEl = document.getElementById('rfidStatus');
if (rfidEl) {
rfidEl.style.color = '#888';
rfidEl.style.whiteSpace = 'pre-line';
rfidEl.style.fontSize = '11px';
rfidEl.style.lineHeight = '1.4';
rfidEl.textContent = 'Reading tag…';
}
rfidReadBtn.disabled = true;
const orig = rfidReadBtn.textContent;
rfidReadBtn.textContent = '…';
const tag = await readRfidTagOnce();
rfidReadBtn.disabled = false;
rfidReadBtn.textContent = orig;
if (rfidEl) {
if (!tag) {
rfidEl.textContent = '✗ No tag in field (or daemon not running)';
rfidEl.style.color = '#dc3545';
} else {
// Compare against the current Discogs page if available
const expectedSku = (state.releaseData?.collectionSku || state.sku || '').replace(/-[AR]$/i, '');
const expectedRel = state.releaseData?.id || null;
const matchSku = expectedSku && tag.sku === expectedSku;
const matchRel = expectedRel && tag.releaseId == expectedRel;
let line2 = '';
if (expectedSku) {
line2 = `\nexpect: ${expectedSku}${expectedRel ? ' + rel ' + expectedRel : ''}`;
if (matchSku && matchRel) line2 += ' ✓ MATCH';
else if (matchSku) line2 += ' ⚠ sku ok, rel mismatch';
else line2 += ' ✗ DIFFERENT';
}
rfidEl.textContent = `tag: ${formatTag(tag)}${line2}`;
rfidEl.style.color = (matchSku && matchRel) ? '#28a745' : (matchSku ? '#d29922' : '#888');
}
}
});
}
// RFID quick-write from Discogs tab
const rfidQuickBtn = document.getElementById('rfidQuickWrite');
if (rfidQuickBtn) {
rfidQuickBtn.addEventListener('click', async () => {
const rawSku = (state.releaseData?.collectionSku) || state.sku;
if (!rawSku) {
showStatusMessage('allStatus', 'No SKU — open a Discogs release first', false);
return;
}
const sku = rawSku ? rawSku.replace(/-[AR]$/i, '') : rawSku;
rfidQuickBtn.disabled = true;
const orig = rfidQuickBtn.textContent;
rfidQuickBtn.textContent = '…';
const releaseId = state.releaseData?.id || null;
const rfidEl = document.getElementById('rfidStatus');
if (rfidEl) { rfidEl.style.color = '#888'; rfidEl.style.whiteSpace = 'pre-line'; rfidEl.style.fontSize = '11px'; rfidEl.style.lineHeight = '1.4'; }
// 1. PRE-SCAN — what's on the tag right now?
if (rfidEl) rfidEl.textContent = 'Reading current tag…';
const before = await readRfidTagOnce();
if (rfidEl && before) {
rfidEl.textContent = `pre: ${formatTag(before)}\nwriting…`;
}
// 2. WRITE
const result = await writeRfidTag(sku, releaseId);
// 3. POST-SCAN — confirm what's on the tag now
const after = result?.ok ? await readRfidTagOnce() : null;
rfidQuickBtn.disabled = false;
rfidQuickBtn.textContent = orig;
if (rfidEl) {
if (!result) {
rfidEl.textContent = `pre: ${before ? formatTag(before) : '?'}\n✗ RFID: daemon not running`;
rfidEl.style.color = '#dc3545';
} else if (result.ok) {
rfidEl.textContent = `pre: ${before ? formatTag(before) : '?'}\n✓ wrote ${sku}${releaseId ? ' + rel ' + releaseId : ''}\npost: ${after ? formatTag(after) : '?'}`;
rfidEl.style.color = '#28a745';
} else {
rfidEl.textContent = `pre: ${before ? formatTag(before) : '?'}\n✗ RFID: ${result.error}`;
rfidEl.style.color = '#dc3545';
}
}
if (result?.ok) {
const appendR = await new Promise(resolve => chrome.storage.local.get(['rfidAppendR'], r => resolve(r.rfidAppendR === true)));
if (appendR && state.discogsTabId) {
try {
await chrome.tabs.sendMessage(state.discogsTabId, { action: 'updateDiscogsSku', sku: sku + '-R', boxIndex: state.collectionBoxIndex });
} catch (e) { console.warn('[RFID] -R SKU update failed:', e.message); }
}
}
});
}
// Website Inventory Sync
const importBtn = document.getElementById('importWordPress');
if (importBtn) {
importBtn.addEventListener('click', handleImportInventory);
}
}
// Function to update button states based on available settings
async function updateButtonStates() {
if (!chrome || !chrome.storage) {
return; // Skip in preview mode
}
return new Promise((resolve) => {
chrome.storage.local.get(['discogsToken', 'googleSettings', 'wpSettings'], (result) => {
const hasDiscogsToken = result.discogsToken && result.discogsToken.trim() !== '';
const hasGoogleSettings = result.googleSettings &&
result.googleSettings.spreadsheetId &&
result.googleSettings.clientEmail &&
result.googleSettings.privateKey;
const hasWpSettings = result.wpSettings &&
result.wpSettings.endpoint && result.wpSettings.endpoint.trim() !== '' &&
result.wpSettings.key && result.wpSettings.key.trim() !== '' &&
result.wpSettings.secret && result.wpSettings.secret.trim() !== '';
// Update Google Sheets button
const sheetButton = document.getElementById('updateSheet');
const bothButton = document.getElementById('bothActions');
if (sheetButton) {
if (hasGoogleSettings) {
sheetButton.disabled = false;
sheetButton.style.opacity = '1';
sheetButton.title = 'Update Google Sheets';
} else {
sheetButton.disabled = true;
sheetButton.style.opacity = '0.5';
sheetButton.title = 'Google Sheets not configured - go to Settings tab';
}
}
// Update "Both Actions" button behavior
if (bothButton) {
if (hasGoogleSettings) {
bothButton.title = 'Print Label + Update Google Sheets';
} else {
bothButton.title = 'Print Label + Save JSON (Google Sheets not configured)';
}
}
// console.log('Button states updated:', { hasDiscogsToken, hasGoogleSettings, hasWpSettings });
resolve();
});
});
}
// Settings validation functions
async function checkSettingsConfigured() {
// Check if we're in an extension context
if (!chrome || !chrome.storage) {
// console.log('Not in extension context, assuming settings not configured');
return false;
}
return new Promise((resolve) => {
chrome.storage.local.get(['discogsToken', 'googleSettings', 'wpSettings'], (result) => {
// console.log('=== DETAILED SETTINGS DEBUG ===');
// console.log('Raw storage result:', result);
// console.log('discogsToken value:', result.discogsToken);
// console.log('googleSettings value:', result.googleSettings);
// console.log('wpSettings value:', result.wpSettings);
const hasDiscogsToken = result.discogsToken && result.discogsToken.trim() !== '';
// console.log('hasDiscogsToken check:', hasDiscogsToken);
const hasGoogleSettings = result.googleSettings &&
result.googleSettings.spreadsheetId &&
result.googleSettings.clientEmail &&
result.googleSettings.privateKey;
// console.log('hasGoogleSettings check:', hasGoogleSettings);
if (result.googleSettings) {
// console.log('Google settings breakdown:', {
// spreadsheetId: result.googleSettings.spreadsheetId,
// clientEmail: result.googleSettings.clientEmail,
// privateKey: result.googleSettings.privateKey ? 'PRESENT' : 'MISSING'
// });
}
const hasWpSettings = result.wpSettings &&
result.wpSettings.endpoint && result.wpSettings.endpoint.trim() !== '' &&
result.wpSettings.key && result.wpSettings.key.trim() !== '' &&
result.wpSettings.secret && result.wpSettings.secret.trim() !== '';
// console.log('hasWpSettings check:', hasWpSettings);
// Discogs token is essential - that's the minimum requirement
// Google Sheets and WordPress are optional enhancements
const hasValidSettings = hasDiscogsToken;
// console.log('Final validation result:', { hasDiscogsToken, hasGoogleSettings, hasWpSettings, hasValidSettings });
// console.log('Settings validation: Discogs token is the only requirement');
// console.log('=== END SETTINGS DEBUG ===');
resolve(hasValidSettings);
});
});
}
function displayStaticInterface() {
const container = document.querySelector('.container');
if (container) {
container.innerHTML = `
<div style="padding: 20px; text-align: center;">
<h3>⚙️ Settings Required</h3>
<p>Please configure your Discogs token to use the extension:</p>
<ul style="text-align: left; margin: 15px 0;">
<li><strong>Discogs Token:</strong> Required for API access</li>
<li><strong>Google Sheets:</strong> Optional - for logging data to spreadsheets</li>
<li><strong>WordPress:</strong> Optional - for additional integrations</li>
</ul>
<p>Click the settings button below to get started.</p>
<div style="margin-top: 20px;">
<button id="open-settings-btn" style="padding: 10px 20px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">
Open Settings
</button>
</div>
</div>
`;
// Add event listener for the settings button
document.getElementById('open-settings-btn').addEventListener('click', () => {
document.getElementById('settings-toggle-connect').click();
});
}
}
function displayLimitedInterface() {
const container = document.querySelector('.container');
if (container) {
container.innerHTML = `
<div style="padding: 20px; text-align: center;">
<h3>🔧 Extension Ready</h3>
<p>Your settings are configured! However, some features require being on a Discogs release page.</p>
<div style="margin: 20px 0; padding: 15px; background: #f0f8ff; border-radius: 8px; text-align: left;">
<h4>✅ Available Features:</h4>
<ul>
<li>View and edit your settings</li>
<li>YouTube cookie management</li>
</ul>
<h4>📋 To use full features:</h4>
<ul>
<li>Navigate to a Discogs release page</li>
<li>Click the extension icon</li>
<li>Use Print Label, Save JSON, and Update Sheet buttons</li>
</ul>
</div>
<div style="margin-top: 20px;">
<button id="open-settings-btn" style="padding: 10px 20px; background: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px;">
Open Settings
</button>
</div>
</div>
`;
// Add event listener for the settings button
document.getElementById('open-settings-btn').addEventListener('click', () => {
document.getElementById('settings-toggle-connect').click();
});
}
}
async function showMasterPageStatus(masterId, tabId) {
const container = document.querySelector('.container') || document.getElementById('discogs-tab');
if (container) {
container.innerHTML = `
<div style="padding:16px;">
<div style="font-size:13px; font-weight:bold; margin-bottom:8px;">Master Release</div>
<div id="master-status-msg" style="font-size:12px; color:#888;">Checking MONSTERWIKI sync…</div>
</div>`;
}
const cfg = await getMonsterSettings();
const statusEl = document.getElementById('master-status-msg');
if (!cfg.enabled) {
if (statusEl) statusEl.textContent = 'MONSTERWIKI sync off — enable in Settings → MONSTERWIKI';
return;
}
if (statusEl) statusEl.textContent = '⟳ Scraping reviews…';
// Inject directly into the master tab — no content script message timing issues
let reviews = [];
try {
const results = await chrome.scripting.executeScript({
target: { tabId },
func: async () => {
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const isVisible = (el) => {
if (!el) return false;
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
const expandReplyButtons = async () => {
const re = /^see\s+\d+\s+repl/i;
for (let round = 0; round < 6; round++) {
const btns = [...document.querySelectorAll('button, a, div[role="button"]')]
.filter(isVisible)
.filter(el => re.test((el.textContent || '').trim()));
if (!btns.length) return;
for (const b of btns) {
try { b.scrollIntoView({ block: 'center', inline: 'center' }); } catch {}
try { b.click(); } catch {}
await sleep(200);
}
await sleep(600);
}
};
// ── 1. Try __NEXT_DATA__ (fast path) ───────────────────────────
let ndReviews = null;
try {
const nd = document.getElementById('__NEXT_DATA__');
if (nd) {
const d = JSON.parse(nd.textContent);
const pp = d?.props?.pageProps;
// Deep-search for any array that looks like reviews
function findReviews(obj, depth = 0) {
if (!obj || typeof obj !== 'object' || depth > 6) return null;
if (Array.isArray(obj) && obj.length &&
(obj[0]?.body || obj[0]?.text || obj[0]?.rating !== undefined) &&
(obj[0]?.user || obj[0]?.username)) return obj;
for (const v of Object.values(obj)) {
const found = findReviews(v, depth + 1);
if (found) return found;
}
return null;
}
const raw = pp?.reviews?.items
|| (Array.isArray(pp?.reviews) ? pp.reviews : null)
|| pp?.master?.reviews?.items
|| pp?.initialState?.reviews?.items
|| findReviews(pp)
|| [];
console.log('[MONSTER] __NEXT_DATA__ keys:', Object.keys(pp || {}),
'| reviews found via paths:', raw.length);
if (raw.length) {
const mapped = raw.map(r => ({
username: r.user?.username || r.username || null,
date: (r.submitted_at || r.submittedAt || r.date || '').split('T')[0] || null,
rating: r.rating ?? null,
text: (r.body || r.text || '').trim() || null,
helpful: r.helpful_count || r.helpfulCount || 0,
version_ref: r.version?.description || null,
version_id: r.version?.id || null,
replies: (r.replies || []).map(rep => ({
username: rep.user?.username || rep.username || null,
date: (rep.submitted_at || rep.date || '').split('T')[0] || null,
text: (rep.body || rep.text || '').trim() || null,
})),
})).filter(r => r.text || r.rating);
// Discogs rarely embeds replies in __NEXT_DATA__ — only use as fast-path
// if replies are actually present; otherwise fall through for DOM scrape.
if (mapped.some(r => r.replies?.length > 0)) return mapped;
ndReviews = mapped;
}
}
} catch (e) {
console.warn('[MONSTER] __NEXT_DATA__ error:', e.message);
}
// Expand only "See N reply/replies" buttons (no aggressive clicking)
await expandReplyButtons();
// ── 1b. Discogs-specific .review_luKwE scrape (reliable reply nesting) ──
const reviewEls = document.querySelectorAll('.review_luKwE:not(.replies_r6FWL .review_luKwE)');
if (reviewEls.length) {
const classReviews = [];
reviewEls.forEach(reviewEl => {
const username = reviewEl.querySelector('.username_N7O6q')?.innerText?.trim() || null;
const date = reviewEl.querySelector('time')?.getAttribute('datetime')?.split('T')[0] || null;
const text = reviewEl.querySelector('.markup_Cngxi')?.innerText?.trim() || null;
const ratingLbl = reviewEl.querySelector('[class*="rating"]')?.getAttribute('aria-label') || '';
const ratingM = ratingLbl.match(/(\d+)/);
const rating = ratingM ? parseInt(ratingM[1]) : null;
const helpful = parseInt([...reviewEl.querySelectorAll('button,a')]
.map(b => b.textContent || '')
.find(t => /helpful/i.test(t))?.match(/\d+/)?.[0]) || 0;
const replies = [];
const sib = reviewEl.nextElementSibling;
if (sib?.classList?.contains('replies_r6FWL')) {
sib.querySelectorAll('.review_luKwE').forEach(replyEl => {
const ru = replyEl.querySelector('.username_N7O6q')?.innerText?.trim() || null;
const rd = replyEl.querySelector('time')?.getAttribute('datetime')?.split('T')[0] || null;
const rt = replyEl.querySelector('.markup_Cngxi')?.innerText?.trim() || null;
if (ru && rt) replies.push({ username: ru, date: rd, text: rt });
});
}
if (username && (text || rating)) classReviews.push({ username, date, rating, text, helpful, version_ref: null, version_id: null, replies });
});
if (classReviews.length) {
console.log('[MONSTER] .review_luKwE scrape:', classReviews.length, 'reviews,',
classReviews.reduce((s,r) => s + r.replies.length, 0), 'replies');
return classReviews;
}
}
// ── 2. Scroll all scrollable containers + window ───────────────
// Discogs sometimes uses a scrollable div, not window
document.querySelectorAll('main, [class*="content"], [class*="Content"], [class*="layout"], [class*="Layout"]')
.forEach(el => { try { el.scrollTop = el.scrollHeight; } catch(_){} });
window.scrollTo(0, document.body.scrollHeight);
await new Promise(r => setTimeout(r, 3000));
// ── 3. Structural DOM scrape (class-name agnostic) ─────────────
// Find every element that has: a /user/ link + a <time> + >80 chars text
const out = [];
const seen = new Set();
const entryKey = (userLink, timeEl) => {
const dt = timeEl.getAttribute('datetime') || timeEl.textContent || '';
return (userLink?.href || '') + '|' + dt;
};
const extractEntryFromTime = (timeEl, root) => {
let card = timeEl.parentElement;
for (let i = 0; i < 8 && card && card !== root; i++) {
const userLink = card.querySelector('a[href*="/user/"]');
if (!userLink) { card = card.parentElement; continue; }
const fullText = card.textContent.trim();
if (fullText.length < 40) { card = card.parentElement; continue; }
const username = userLink.textContent.trim()
|| userLink.href.split('/user/')[1]?.split('/')[0] || null;
const date = timeEl.getAttribute('datetime')?.split('T')[0]
|| timeEl.textContent.trim() || null;
let text = null;
const paras = [...card.querySelectorAll('p, [class*="body"], [class*="Body"], [class*="markup"], [class*="Markup"]')];
if (paras.length) {
text = paras.sort((a,b) => b.textContent.length - a.textContent.length)[0]
?.textContent?.trim() || null;
}
if (!text) {
text = fullText.replace(username || '', '').replace(date || '', '').trim().slice(0, 5000) || null;
}
return {
username,
date,
text,
_userLink: userLink,
_timeEl: timeEl,
};
}
return null;
};
const collectThreadEntries = (container) => {
const entries = [];
const dedupe = new Set();
const times = [...container.querySelectorAll('time')];
for (const t of times) {
const e = extractEntryFromTime(t, container);
if (!e?.username || !e.text) continue;
const key = `${e.username}|${e.date || ''}|${e.text.slice(0, 40)}`;
if (dedupe.has(key)) continue;
dedupe.add(key);
entries.push(e);
}
return entries;
};
// Walk up from <time> elements to find the review card container
document.querySelectorAll('time').forEach(timeEl => {
// Walk up max 8 levels to find a container with a user link
let card = timeEl.parentElement;
for (let i = 0; i < 8 && card; i++) {
const userLink = card.querySelector('a[href*="/user/"]');
if (!userLink) { card = card.parentElement; continue; }
const fullText = card.textContent.trim();
if (fullText.length < 40) { card = card.parentElement; continue; }
const key = entryKey(userLink, timeEl);
if (seen.has(key)) break;
seen.add(key);
const username = userLink.textContent.trim()
|| userLink.href.split('/user/')[1]?.split('/')[0] || null;
const date = timeEl.getAttribute('datetime')?.split('T')[0]
|| timeEl.textContent.trim() || null;
// Get review text: prefer the largest <p> or <div> that isn't the username/date line
let text = null;
const paras = [...card.querySelectorAll('p, [class*="body"], [class*="Body"]')];
if (paras.length) {
text = paras.sort((a,b) => b.textContent.length - a.textContent.length)[0]
?.textContent?.trim() || null;
}
if (!text) {
// strip username and date from full text as fallback
text = fullText.replace(username || '', '').replace(date || '', '').trim().slice(0, 5000) || null;
}
// Stars: count filled star elements or aria-label
const starAriaEl = card.querySelector('[aria-label*="out of 5"], [aria-label*="/5"]');
let rating = null;
if (starAriaEl) {
const m = starAriaEl.getAttribute('aria-label').match(/(\d+)/);
if (m) rating = parseInt(m[1]);
}
if (!rating) {
rating = card.querySelectorAll(
'[class*="star"][class*="full"], [class*="star"][class*="fill"], ' +
'[class*="starFull"], [class*="starFill"], ' +
'svg[fill="#000"], svg[fill="#ffb300"]'
).length || null;
}
const versionEl = [...card.querySelectorAll('p,span,a')]
.find(el => /referencing/i.test(el.textContent));
const version_ref = versionEl
? versionEl.textContent.replace(/.*referencing\s*/i,'').trim().slice(0,200) || null : null;
const helpfulEl = card.querySelector('[class*="helpful"], [class*="Helpful"]');
const helpful = parseInt(helpfulEl?.textContent?.match(/\d+/)?.[0]) || 0;
const threadContainers = [card];
const sib = card.nextElementSibling;
if (sib && sib.querySelector && (sib.querySelector('time') || /repl/i.test(sib.textContent || ''))) {
threadContainers.push(sib);
}
const threadEntries = threadContainers.flatMap(collectThreadEntries);
const replies = [];
if (threadEntries.length > 1) {
threadEntries.slice(1).forEach(ent => {
replies.push({ username: ent.username, date: ent.date, text: ent.text });
if (ent._userLink && ent._timeEl) seen.add(entryKey(ent._userLink, ent._timeEl));
});
}
if (username && (text || rating)) {
out.push({ username, date, rating, text, helpful, version_ref, version_id: null, replies });
}
break;
}
});
console.log('[MONSTER] structural DOM scrape:', out.length, 'reviews found',
'| time elements:', document.querySelectorAll('time').length,
'| user links:', document.querySelectorAll('a[href*="/user/"]').length);
// Prefer DOM results (may have replies); fall back to __NEXT_DATA__ reviews
if (!out.length && ndReviews?.length) {
console.log('[MONSTER] DOM scrape empty — using __NEXT_DATA__ reviews as fallback');
return ndReviews;
}
return out;
}
});
reviews = results?.[0]?.result || [];
} catch (e) {
if (statusEl) { statusEl.style.color = '#b00'; statusEl.textContent = `✗ Script error: ${e.message}`; }
return;
}
if (!reviews.length) {
if (statusEl) { statusEl.style.color = '#888'; statusEl.textContent = 'No reviews found — check DevTools console for [MONSTER] debug output, then reopen popup'; }
return;
}
const replyCount = reviews.reduce((sum, r) => sum + ((r.replies && r.replies.length) ? r.replies.length : 0), 0);
if (statusEl) statusEl.textContent = `⟳ Posting ${reviews.length} reviews${replyCount ? ` (+${replyCount} replies)` : ''} to MONSTERWIKI…`;
const base = await monsterResolveBase(cfg);
try {
const resp = await fetch(`${base}/plice/master`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ master_id: parseInt(masterId), url: `/master/${masterId}`, reviews }),
signal: AbortSignal.timeout(8000),
});
const d = resp.ok ? await resp.json() : {};
if (statusEl) {
statusEl.style.color = resp.ok ? '#2a7a2a' : '#b00';
statusEl.textContent = resp.ok
? `${d.review_rows ?? reviews.length} reviews synced to MONSTERWIKI`
: `✗ Server error ${resp.status}`;
}
} catch (e) {
if (statusEl) { statusEl.style.color = '#b00'; statusEl.textContent = '✗ MONSTERWIKI unreachable — is plice_server.py running?'; }
}
}
async function fetchAndDisplayReleaseData(releaseId) {
try {
console.log('--- Starting Data Fetch ---');
// Check if we're in an extension context
if (!chrome || !chrome.storage) {
console.log('Not in extension context, showing static interface');
displayStaticInterface();
return;
}
// Check if settings are configured before proceeding
const hasValidSettings = await checkSettingsConfigured();
if (!hasValidSettings) {
console.log('Settings not configured, showing static interface');
displayStaticInterface();
return;
}
// Check if we have the necessary Chrome APIs for tab interaction
if (!chrome.tabs || !chrome.scripting) {
console.log('Chrome APIs not available:', {
tabs: !!chrome.tabs,
scripting: !!chrome.scripting,
url: window.location.href
});
displayLimitedInterface();
return;
}
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
const tab = tabs[0];
// Capture tab ID and generate SKU once at popup open — used for sheet, QR, and Discogs field
state.discogsTabId = tab.id;
const now = new Date();
state.sheetTimestamp = now.toISOString();
state.sku = now.toISOString().replace(/\D/g, '').slice(0, 14);
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] });
const initialValues = await new Promise((resolve) => {
chrome.tabs.sendMessage(tab.id, { action: 'getInitialValues' }, response => {
resolve(chrome.runtime.lastError ? {} : response || {});
});
});
console.log('POPUP: Received initial values from content script:', initialValues);
const [releaseData, priceSuggestions] = await Promise.all([
getReleaseData(releaseId),
getPriceSuggestions(releaseId)
]);
console.log('POPUP: Fetched release data from API:', releaseData);
console.log('POPUP: Fetched price suggestions from API:', priceSuggestions);
if (!releaseData) {
showStatusMessage('allStatus', 'Discogs API unavailable — try reopening the popup', false);
return;
}
// Properly combine API data with content script data - preserve API fields!
state.releaseData = {
// Start with API data (preserve all API fields like id, genres, styles, tracklist, etc.)
...releaseData,
// Add price suggestions
priceSuggestions,
// Add formatted fields from API data
artist: (releaseData.artists || []).map(a => a.name.replace(/\(\d+\)/, '')).join(', '),
genre: (releaseData.genres || []).join(', '),
style: (releaseData.styles || []).join(', '),
label: (releaseData.labels ? [...new Set(releaseData.labels.map(l => l.name.replace(/\(\d+\)/, '').replace(/ Records$/, '')))].join(', ') : ''),
// Only add specific content script fields (don't spread all initialValues)
lowPrice: initialValues.lowPrice,
medianPrice: initialValues.medianPrice,
highPrice: initialValues.highPrice,
lastSold: initialValues.lastSold,
have: initialValues.have,
want: initialValues.want,
artistId: initialValues.artistId,
labelId: initialValues.labelId,
imageUrl: initialValues.imageUrl,
sellerPriceRange: initialValues.sellerPriceRange || '',
mediaCondition: initialValues.mediaCondition || 'Very Good Plus (VG+)',
sleeveCondition: initialValues.sleeveCondition || 'Very Good Plus (VG+)',
price: initialValues.price || '',
comment: initialValues.comment || '',
collectionFolder: initialValues.collectionFolder || '',
appleId: initialValues.appleId || '',
reviews: initialValues.reviews || [],
recommendations: initialValues.recommendations || [],
collectionSku: initialValues.collectionSku || ''
};
// Collection box state
state.collectionBoxCount = initialValues.collectionBoxCount || 1;
state.collectionBoxSkus = initialValues.collectionBoxSkus || [];
state.collectionBoxIndex = state.collectionBoxCount - 1; // default to last (newest) box
state.originalReleaseData = JSON.parse(JSON.stringify(state.releaseData));
console.log('POPUP: Final combined state object:', state.releaseData);
console.log('POPUP: API fields preserved - id:', state.releaseData.id, 'genres:', state.releaseData.genres, 'tracklist length:', state.releaseData.tracklist?.length);
document.getElementById('price').value = state.releaseData.price || '';
document.getElementById('mediaCondition').value = state.releaseData.mediaCondition || 'Very Good Plus (VG+)';
document.getElementById('sleeveCondition').value = state.releaseData.sleeveCondition || 'Very Good Plus (VG+)';
document.getElementById('commentInput').textContent = state.releaseData.comment || '';
// Pass generated SKU as fallback so SKU field shows even before it's committed to Discogs
displayReleaseInfo({ ...state.releaseData, collectionSku: state.releaseData.collectionSku || state.sku });
// Set the QR code value for the preview:
// - If item already has a SKU on Discogs, use that (for reprints)
// - Otherwise if SKU mode is on, use the freshly generated state.sku
const useSkuOnLoad = document.getElementById('qr-use-sku')?.checked || false;
if (useSkuOnLoad) {
state.releaseData.sku = state.releaseData.collectionSku || state.sku;
}
updatePreview(state.releaseData);
// Disable ALL/SHEET if SKU already present on current box
updateAllButtonState();
// Show collection box selector if multiple copies owned
renderCollectionBoxSelector(state.collectionBoxCount, state.collectionBoxIndex, state.collectionBoxSkus);
setupEventListeners();
await updateButtonStates(); // Update button states based on available settings
updateResearchTab(); // Refresh Research tab with all loaded data
console.log('--- Data Fetch and Display Complete ---');
// MONSTERWIKI: kick off silent background sync (sell/history + sell/list)
monsterBackgroundSync(releaseId);
inventorySkuLookup(releaseId);
} catch (error) {
console.error('Error in fetchAndDisplayReleaseData:', error);
document.body.innerHTML = `<div style="padding: 10px; text-align: center;">Error: ${error.message}</div>`;
}
}
function writeJSON(releaseData) {
if (!releaseData) return Promise.reject(new Error('No release data'));
// Check if JSON download is enabled in settings
return new Promise((resolve, reject) => {
chrome.storage.local.get(['enableJsonDownload'], (result) => {
const isEnabled = result.enableJsonDownload !== false; // Default to true
if (!isEnabled) {
console.log('JSON download is disabled in settings');
resolve(); // Resolve without downloading
return;
}
// Proceed with JSON download
const { marketStats, mediaCondition, sleeveCondition, comment, collectionFolder, appleId, price, priceSuggestions, reviews, recommendations, ...baseData } = releaseData;
const fullData = { ...baseData, market: { lastSold: marketStats?.lastSold, lowPrice: marketStats?.lowPrice, medianPrice: marketStats?.medianPrice, highPrice: marketStats?.highPrice, suggestedPrices: priceSuggestions }, collection: { mediaCondition, sleeveCondition, comment, collectionFolder, appleId, price }, reviews, recommendations };
const jsonString = JSON.stringify(fullData, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
chrome.downloads.download({ url, filename: `JSON/${releaseData.id}.json`, saveAs: false }, () => {
URL.revokeObjectURL(url);
resolve();
});
});
});
}
function uploadJSON() {
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json,.json';
input.onchange = (e) => {
const file = e.target.files[0];
if (!file) { resolve(null); return; }
const reader = new FileReader();
reader.onload = (ev) => {
try {
const data = JSON.parse(ev.target.result);
resolve(data);
} catch (err) {
showStatusMessage('allStatus', 'Invalid JSON file', false);
resolve(null);
}
};
reader.readAsText(file);
};
input.click();
});
}
// REMOVED: updateSheetOnly function (now imported from sheets.js)
function showStatusMessage(elementId, message, isSuccess) {
const statusElement = document.getElementById(elementId);
if (statusElement && message) {
statusElement.textContent = message;
statusElement.className = 'status-text show';
statusElement.style.color = isSuccess ? '#28a745' : '#dc3545';
// Hide the message after 3 seconds
setTimeout(() => {
statusElement.classList.remove('show');
}, 3000);
}
}
// Single-shot tag read for pre/post-write verification.
// Returns { sku, releaseId, epc } or null if no tag / daemon down.
async function readRfidTagOnce() {
try {
const resp = await fetch('http://127.0.0.1:7790/read-tag', { signal: AbortSignal.timeout(5000) });
const data = await resp.json();
if (!data || data.ok === false) return null;
return {
sku: data.sku || null,
releaseId: data.releaseId || data.release || null,
epc: data.epc || null,
};
} catch (e) {
return null;
}
}
function formatTag(t) {
if (!t) return '(no tag in field)';
if (t.sku) return `${t.sku}${t.releaseId ? ' + rel ' + t.releaseId : ''}`;
if (t.epc) return `EPC ${String(t.epc).slice(0, 12)}… (factory tag, not programmed)`;
return '(unknown)';
}
async function writeRfidTag(sku, releaseId) {
try {
const body = { sku };
if (releaseId) body.releaseId = String(releaseId);
// Pre-clear any stale SELECT mask before the write so it doesn't
// compete with a mask left by a previous failed attempt.
await fetch('http://127.0.0.1:7790/clear-mask', { method: 'POST', signal: AbortSignal.timeout(3000) }).catch(() => {});
const resp = await fetch('http://127.0.0.1:7790/write-tag', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: AbortSignal.timeout(120000) // 120s: 5 attempts × ~20s each + headroom
});
const data = await resp.json();
if (!data.ok) console.warn('[RFID] write failed:', data.error);
else console.log('[RFID] wrote SKU', data.sku, releaseId ? `+ release ${releaseId}` : '');
return data;
} catch (e) {
// Daemon not running, reader not connected, or timed out
console.warn('[RFID] daemon unavailable:', e.message);
return null;
}
}
function updateAllButtonState() {
const hasSku = !!(state.releaseData && state.releaseData.collectionSku);
for (const id of ['bothActions', 'updateSheet']) {
const btn = document.getElementById(id);
if (!btn) continue;
if (hasSku) {
btn.disabled = true;
btn.title = `SKU already present (${state.releaseData.collectionSku}) — item already logged`;
btn.style.opacity = '0.4';
btn.style.cursor = 'not-allowed';
} else {
btn.disabled = false;
btn.title = '';
btn.style.opacity = '';
btn.style.cursor = '';
}
}
}
function renderCollectionBoxSelector(count, activeIdx, skus) {
const wrapper = document.getElementById('collection-box-selector');
const container = document.getElementById('collection-box-buttons');
if (!wrapper || !container) return;
if (count <= 1) {
wrapper.style.display = 'none';
return;
}
wrapper.style.display = 'flex';
container.innerHTML = '';
for (let i = 0; i < count; i++) {
const hasSku = !!(skus && skus[i]);
const btn = document.createElement('button');
btn.textContent = String(i + 1);
if (hasSku) {
btn.textContent = String(i + 1) + ' ●';
btn.title = `Copy ${i + 1} — SKU: ${skus[i]}`;
} else {
btn.title = `Copy ${i + 1} — no SKU`;
}
btn.style.cssText = [
'padding: 2px 7px',
'font-size: 11px',
'border-radius: 3px',
'border: 1px solid #aaa',
'cursor: pointer',
'background: ' + (i === activeIdx ? '#4a90d9' : '#f0f0f0'),
'color: ' + (i === activeIdx ? '#fff' : '#333'),
hasSku ? 'color: ' + (i === activeIdx ? '#9fffb0' : '#2a8a2a') : '',
].filter(Boolean).join(';');
btn.addEventListener('click', () => switchCollectionBox(i));
container.appendChild(btn);
}
}
async function switchCollectionBox(index) {
const tabId = state.discogsTabId;
if (!tabId) return;
let boxData;
try {
boxData = await new Promise((resolve) => {
chrome.tabs.sendMessage(tabId, { action: 'selectCollectionBox', index }, response => {
resolve(chrome.runtime.lastError ? null : response);
});
});
} catch (e) { return; }
if (!boxData || !boxData.ok) return;
state.collectionBoxIndex = index;
// Update state fields from selected box
state.releaseData.mediaCondition = boxData.mediaCondition || state.releaseData.mediaCondition;
state.releaseData.sleeveCondition = boxData.sleeveCondition || state.releaseData.sleeveCondition;
state.releaseData.price = boxData.price || state.releaseData.price;
state.releaseData.comment = boxData.comment || state.releaseData.comment;
state.releaseData.collectionSku = boxData.collectionSku || '';
// If this box has no SKU, mint a fresh one at click-time so ALL/LABEL/SHEET can use it.
// If it already has a SKU (e.g. from a previous DB lookup), use that — never show the
// popup-generated timestamp when a real SKU already exists.
if (!state.releaseData.collectionSku) {
const now = new Date();
state.sheetTimestamp = now.toISOString();
state.sku = now.toISOString().replace(/\D/g, '').slice(0, 14);
} else {
// Use the existing SKU (stripped of -R/-A) as the active SKU
state.sku = String(state.releaseData.collectionSku).replace(/-[AR]$/i, '');
}
// Update input fields
const priceEl = document.getElementById('price');
const mediaEl = document.getElementById('mediaCondition');
const sleeveEl = document.getElementById('sleeveCondition');
const commentEl = document.getElementById('commentInput');
if (priceEl) priceEl.value = state.releaseData.price || '';
if (mediaEl) mediaEl.value = state.releaseData.mediaCondition || 'Very Good Plus (VG+)';
if (sleeveEl) sleeveEl.value = state.releaseData.sleeveCondition || 'Very Good Plus (VG+)';
if (commentEl) commentEl.textContent = state.releaseData.comment || '';
// Update SKU field in Release Information panel — show existing SKU or freshly generated one
const skuFieldEl = document.querySelector('.info-field[data-field="collectionSku"] .value');
if (skuFieldEl) skuFieldEl.textContent = state.releaseData.collectionSku || state.sku || '';
// Re-render selector with updated active index
renderCollectionBoxSelector(state.collectionBoxCount, index, state.collectionBoxSkus);
updateAllButtonState();
// Update QR preview: use fresh state.sku for empty boxes if SKU mode is on
const useSkuForPreview = document.getElementById('qr-use-sku')?.checked || false;
if (useSkuForPreview) {
state.releaseData.sku = state.releaseData.collectionSku || state.sku;
} else {
delete state.releaseData.sku;
}
updatePreview(state.releaseData);
}
async function handleImportInventory() {
if (!state.releaseData || !state.releaseData.id) {
showStatusMessage('allStatus', 'No release data — open a Discogs release first', false);
return;
}
const importBtn = document.getElementById('importWordPress');
const origText = importBtn.textContent;
importBtn.textContent = '…';
importBtn.disabled = true;
try {
const releaseId = state.releaseData.id;
const cfg = await getMonsterSettings();
const base = await monsterResolveBase(cfg);
const url = `${base}/plice/inventory?release_id=${releaseId}`;
const response = await fetch(url);
const result = await response.json();
if (!result.ok) {
throw new Error(result.error || 'Failed to fetch inventory');
}
const inventory = result.inventory || [];
if (inventory.length === 0) {
showStatusMessage('allStatus', 'No matching records found on website VPS', false);
return;
}
const boxCount = state.collectionBoxCount || 1;
const invCount = inventory.length;
// "fill oldest first" logic (oldest SKU -> first box)
// inventory is already sorted by SKU (timestamp) ASC in plice_server.py
let updatedCount = 0;
let priceMismatch = false;
let lastError = null;
for (let i = 0; i < Math.min(boxCount, invCount); i++) {
const item = inventory[i];
const sku = item.sku;
const price = item.price;
// Update SKU in Discogs box
try {
await chrome.tabs.sendMessage(state.discogsTabId, {
action: 'updateDiscogsSku',
sku: sku,
boxIndex: i
});
// Update price in Discogs box
// According to prompt: "there is collection box price field which should match the inventory.price field?"
// We update it to ensure they match.
await chrome.tabs.sendMessage(state.discogsTabId, {
action: 'updateDiscogsPrice',
price: price
});
updatedCount++;
} catch (e) {
lastError = e.message;
}
}
// Handle mismatches as requested: "if they don tmatch up, give me a text msg"
let msg = `Updated ${updatedCount} box(es) with SKUs from VPS.`;
if (boxCount !== invCount) {
msg += ` WARNING: Mismatch! VPS has ${invCount} records, but Discogs has ${boxCount} collection boxes.`;
}
if (lastError) {
msg += ` Error during update: ${lastError}`;
}
showStatusMessage('allStatus', msg, !lastError && boxCount === invCount);
// Refresh local state by fetching initial values again
const newValues = await new Promise((resolve) => {
chrome.tabs.sendMessage(state.discogsTabId, { action: 'getInitialValues' }, r => resolve(r || {}));
});
if (newValues && !newValues.error) {
state.releaseData = { ...state.releaseData, ...newValues };
// If the current box now has a real SKU, ditch the popup-generated timestamp
const currentBoxSku = (newValues.collectionBoxSkus || [])[state.collectionBoxIndex]
|| newValues.collectionSku || '';
if (currentBoxSku) {
const cleanSku = String(currentBoxSku).replace(/-[AR]$/i, '');
state.sku = cleanSku;
if (document.getElementById('qr-use-sku')?.checked) {
state.releaseData.sku = cleanSku;
}
}
displayReleaseInfo(state.releaseData);
updatePreview(state.releaseData);
}
} catch (e) {
showStatusMessage('allStatus', `Inventory error: ${e.message}`, false);
} finally {
importBtn.textContent = origText;
importBtn.disabled = false;
}
}
function refreshPopupSku(sku) {
if (!state.releaseData) return;
state.releaseData.collectionSku = sku;
// Strip -R/-A for label/QR — the print label SKU is always just the integer.
// Also replace the popup-generated timestamp SKU so the label shows the DB value.
const cleanSku = String(sku || '').replace(/-[AR]$/i, '');
state.sku = cleanSku;
if (document.getElementById('qr-use-sku')?.checked) {
state.releaseData.sku = cleanSku;
}
const skuFieldEl = document.querySelector('.info-field[data-field="collectionSku"] .value');
if (skuFieldEl) skuFieldEl.textContent = sku;
updatePreview(state.releaseData);
}
// Shows DB price next to fill status. Returns true if prices differ.
function showPriceNote(statusEl, panelEl, dbRow, fillLabel) {
const dbPrice = dbRow.price != null ? parseFloat(dbRow.price) : null;
const pagePrice = parseFloat(state.releaseData?.price);
const differ = dbPrice !== null && !isNaN(pagePrice) && Math.abs(dbPrice - pagePrice) >= 0.01;
const sold = dbRow.instock === 'n' || dbRow.instock === false || dbRow.instock === 0;
let html = `<span style="color:#2a7">${fillLabel}</span>`;
if (sold) {
html += `&nbsp;<span style="background:#f8d7da;color:#842029;padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600;">SOLD</span>`;
}
if (dbPrice !== null) {
if (differ) {
html += `&nbsp;<span style="background:#fff3cd;color:#856404;padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600;">` +
`DB $${dbPrice.toFixed(2)} · page $${pagePrice.toFixed(2)}</span>`;
} else {
html += `&nbsp;<span style="color:#888;font-size:10px;">DB $${dbPrice.toFixed(2)}</span>`;
}
}
statusEl.innerHTML = html;
statusEl.style.color = '';
if (!differ && !sold) setTimeout(() => { panelEl.style.display = 'none'; }, 3000);
return differ || sold;
}
async function inventorySkuLookup(releaseId) {
const settings = await new Promise(resolve =>
chrome.storage.local.get(['inventorySettings'], r => resolve(r.inventorySettings || {}))
);
if (!settings.enabled) return;
if (!settings.dbUser || !settings.dbPass || !settings.dbName) return;
if (!state.discogsTabId) return;
const panel = document.getElementById('inventory-sku-panel');
const status = document.getElementById('inventory-sku-status');
if (!panel || !status) return;
panel.style.display = 'block';
status.textContent = 'Looking up inventory SKUs…';
status.style.color = '#888';
let rows;
try {
const params = new URLSearchParams({
release_id: releaseId,
dbHost: settings.dbHost || '100.123.123.64',
dbName: settings.dbName || 'wp_rmp_disc_',
dbUser: settings.dbUser,
dbPass: settings.dbPass,
sshKeyPath: settings.sshKeyPath || '~/.ssh/id_rsa'
});
const resp = await fetch(`http://localhost:7790/inventory-lookup?${params}`);
if (!resp.ok) throw new Error(`Daemon error: HTTP ${resp.status}`);
const data = await resp.json();
if (data.error) throw new Error(data.error);
rows = data.rows || [];
} catch (e) {
status.textContent = `Inventory lookup failed: ${e.message}`;
status.style.color = '#c00';
return;
}
if (rows.length === 0) {
panel.style.display = 'none';
return;
}
const boxCount = state.collectionBoxCount || 1;
const lastBoxIdx = boxCount - 1;
// ── Auto-fill exact matches silently, then always show the row list ───────
const boxIsLocked = idx => (state.collectionBoxSkus[idx] || '').endsWith('-R');
if (rows.length === 1) {
if (boxIsLocked(lastBoxIdx)) {
status.innerHTML = `<span style="color:#888">Box ${lastBoxIdx + 1} locked (${state.collectionBoxSkus[lastBoxIdx]})</span>`;
status.style.color = '';
} else {
try {
await chrome.tabs.sendMessage(state.discogsTabId, {
action: 'updateDiscogsSku',
sku: rows[0].sku,
boxIndex: lastBoxIdx
});
refreshPopupSku(rows[0].sku);
} catch (e) {
status.textContent = `Failed to fill SKU: ${e.message}`;
status.style.color = '#c00';
}
}
} else if (rows.length === boxCount) {
let ok = true;
for (let i = 0; i < rows.length; i++) {
if (boxIsLocked(i)) continue;
try {
await chrome.tabs.sendMessage(state.discogsTabId, {
action: 'updateDiscogsSku',
sku: rows[i].sku,
boxIndex: i
});
} catch (e) {
ok = false;
status.textContent = `Failed to fill box ${i + 1}: ${e.message}`;
status.style.color = '#c00';
break;
}
}
if (ok) {
const currentBoxSku = rows[state.collectionBoxIndex]?.sku || rows[0].sku;
if (!boxIsLocked(state.collectionBoxIndex)) refreshPopupSku(currentBoxSku);
}
} else {
status.innerHTML = `<span style="color:#b60">Mismatch: ${rows.length} DB rows, ${boxCount} box${boxCount !== 1 ? 'es' : ''}</span>`;
status.style.color = '';
}
// ── Always render the row list ────────────────────────────────────────────
renderInventoryRows(rows);
}
function renderInventoryRows(rows) {
const picker = document.getElementById('inventory-sku-picker');
const cardsEl = document.getElementById('inventory-sku-cards');
if (!picker || !cardsEl) return;
// ── Hide unused box-buttons container (no longer needed) ─────────────────
const boxButtonsEl = document.getElementById('inventory-sku-box-buttons');
if (boxButtonsEl) boxButtonsEl.style.display = 'none';
cardsEl.innerHTML = '';
rows.forEach((row, i) => {
const rowEl = document.createElement('div');
rowEl.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:3px;';
// ── [n] button writes this row's SKU to collection box n ─────────────
const btn = document.createElement('button');
btn.textContent = String(i + 1);
btn.style.cssText = [
'padding:2px 7px',
'font-size:12px',
'font-weight:600',
'border-radius:4px',
'border:1px solid #aaa',
'cursor:pointer',
'flex-shrink:0',
'min-width:24px'
].join(';');
const existingSku = state.collectionBoxSkus[i] || '';
const locked = existingSku.endsWith('-R');
if (locked) {
btn.disabled = true;
btn.title = `Locked: ${existingSku}`;
btn.style.opacity = '0.45';
btn.style.cursor = 'default';
btn.style.background = '#f0f0f0';
} else {
btn.addEventListener('click', async () => {
try {
await chrome.tabs.sendMessage(state.discogsTabId, {
action: 'updateDiscogsSku',
sku: row.sku,
boxIndex: i
});
refreshPopupSku(row.sku);
btn.textContent = `${i + 1}`;
btn.style.background = '#e8f8ef';
btn.style.borderColor = '#2a7';
btn.disabled = true;
} catch (e) {
document.getElementById('inventory-sku-status').textContent = `Write failed: ${e.message}`;
document.getElementById('inventory-sku-status').style.color = '#c00';
}
});
}
// ── Row info text ─────────────────────────────────────────────────────
const location = (row.crate_id && row.slot_number) ? `${row.crate_id}/${row.slot_number}` : '—';
const price = row.price ? `$${parseFloat(row.price).toFixed(2)}` : '—';
const cond = [row.media_condition, row.sleeve_condition].filter(Boolean).join('/') || '—';
const weight = (row.actual_weight != null && Number.isFinite(Number(row.actual_weight)))
? `${Number(row.actual_weight)}g`
: '—';
const sold = row.instock === 'n' || row.instock === false || row.instock === 0;
const dbPrice = row.price != null ? parseFloat(row.price) : null;
const pagePx = parseFloat(state.releaseData?.price);
const differ = dbPrice !== null && !isNaN(pagePx) && Math.abs(dbPrice - pagePx) >= 0.01;
let infoHtml = `<span style="font-family:monospace;font-size:11px;">`
+ `${row.sku}&nbsp;&nbsp;${row.instock || '?'}&nbsp;&nbsp;${location}&nbsp;&nbsp;${price}&nbsp;&nbsp;${cond}&nbsp;&nbsp;${weight}`
+ `</span>`;
if (sold) infoHtml += `&nbsp;<span style="background:#f8d7da;color:#842029;padding:1px 4px;border-radius:3px;font-size:10px;font-weight:600;">SOLD</span>`;
if (differ) infoHtml += `&nbsp;<span style="background:#fff3cd;color:#856404;padding:1px 4px;border-radius:3px;font-size:10px;font-weight:600;">page $${pagePx.toFixed(2)}</span>`;
const infoEl = document.createElement('span');
infoEl.innerHTML = infoHtml;
// ── ⚖→DB button: capture current scale weight, UPDATE this SKU in WP ─
const wtBtn = document.createElement('button');
wtBtn.textContent = '⚖→DB';
wtBtn.title = 'Capture current scale reading and update this SKU in wp_rmp_disc_inventory';
wtBtn.style.cssText = [
'padding:2px 6px',
'font-size:11px',
'border-radius:4px',
'border:1px solid #aaa',
'cursor:pointer',
'background:#eef',
'flex-shrink:0',
'margin-left:auto'
].join(';');
wtBtn.addEventListener('click', async () => {
wtBtn.disabled = true;
const origText = wtBtn.textContent;
wtBtn.textContent = '…';
try {
const cfg = await new Promise(resolve =>
chrome.storage.local.get(['inventorySettings', 'scaleRequireStable'], resolve)
);
const settings = cfg.inventorySettings || {};
if (!settings.dbUser || !settings.dbPass || !settings.dbName) {
throw new Error('inventory creds not set in Settings');
}
const requireStable = cfg.scaleRequireStable !== false;
const grams = await getCurrentWeight({ requireStable });
if (grams == null) {
throw new Error(requireStable ? 'no stable reading from scale' : 'scale offline');
}
const resp = await fetch('http://localhost:7790/inventory-update-weight', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
release_id: state.releaseData?.id || row.release_id,
sku: row.sku,
grams: grams,
dbHost: settings.dbHost || '100.123.123.64',
dbName: settings.dbName || 'wp_rmp_disc_',
dbUser: settings.dbUser,
dbPass: settings.dbPass,
sshKeyPath: settings.sshKeyPath || '~/.ssh/id_rsa'
})
});
const data = await resp.json();
if (!resp.ok || data.error) throw new Error(data.error || `HTTP ${resp.status}`);
wtBtn.textContent = `${grams}g`;
wtBtn.style.background = '#e6f4ea';
wtBtn.style.borderColor = '#2a7';
// Reflect in the displayed row immediately
row.actual_weight = grams;
const wtSpan = infoEl.querySelector('span');
if (wtSpan) {
wtSpan.innerHTML = wtSpan.innerHTML.replace(/—(?=(?:&nbsp;)*$)/, `${grams}g`)
.replace(/\b\d+g(?=(?:&nbsp;)*$)/, `${grams}g`);
}
} catch (e) {
wtBtn.textContent = '✗ ' + (e.message || 'failed');
wtBtn.style.background = '#fde2e1';
wtBtn.style.borderColor = '#c33';
setTimeout(() => {
wtBtn.textContent = origText;
wtBtn.style.background = '#eef';
wtBtn.style.borderColor = '#aaa';
wtBtn.disabled = false;
}, 2500);
}
});
rowEl.appendChild(btn);
rowEl.appendChild(infoEl);
rowEl.appendChild(wtBtn);
cardsEl.appendChild(rowEl);
});
picker.style.display = 'block';
}
async function handleAllActions(releaseData) {
if (!releaseData) return;
const discogsTabId = state.discogsTabId;
// Read toggle from DOM; also verify against storage to survive popup re-opens
const skuSettings = await new Promise(resolve => chrome.storage.local.get(['qrUseSku'], resolve));
const useSkuForQr = skuSettings.qrUseSku === true;
const skuToUse = (useSkuForQr && releaseData.collectionSku) ? releaseData.collectionSku : state.sku;
// Load all action toggles
const allCfg = await new Promise(resolve =>
chrome.storage.local.get(['allDoesRfid','allDoesLabel','allDoesSheet','allDoesJson','allDoesPrice','rfidIncludeReleaseId','rfidAppendR'], resolve)
);
const doRfid = allCfg.allDoesRfid !== false;
const doLabel = allCfg.allDoesLabel !== false;
const doSheet = allCfg.allDoesSheet !== false;
const doJson = allCfg.allDoesJson !== false;
const doPrice = allCfg.allDoesPrice !== false;
// RFID — fire-and-forget alongside other actions
if (doRfid) {
const rawRfidSku = releaseData.collectionSku || skuToUse;
const rfidSku = rawRfidSku ? rawRfidSku.replace(/-[AR]$/i, '') : rawRfidSku;
// Always include release_id — see rationale at rfidQuickBtn handler.
const releaseId = releaseData.id || null;
const rfidEl = document.getElementById('rfidStatus');
if (rfidEl) { rfidEl.textContent = 'RFID writing…'; rfidEl.style.color = '#888'; }
writeRfidTag(rfidSku, releaseId).then(async result => {
if (!rfidEl) return;
if (result?.ok) {
rfidEl.textContent = `✓ RFID wrote ${rfidSku}${releaseId ? ' + rel ' + releaseId : ''}`; rfidEl.style.color = '#28a745';
if (allCfg.rfidAppendR && tabId) {
try {
await chrome.tabs.sendMessage(tabId, { action: 'updateDiscogsSku', sku: rfidSku + '-R', boxIndex: state.collectionBoxIndex });
} catch (e) { console.warn('[RFID] -R SKU update failed:', e.message); }
}
} else if (result) { rfidEl.textContent = `✗ RFID: ${result.error}`; rfidEl.style.color = '#dc3545'; }
else { rfidEl.textContent = '✗ RFID: daemon not running'; rfidEl.style.color = '#dc3545'; }
});
}
// Update preview QR before we snapshot it into the print window
if (useSkuForQr) {
releaseData.sku = skuToUse;
updatePreview(releaseData);
}
// Fix the sheet timestamp to match the SKU
if (useSkuForQr && state.sheetTimestamp) {
releaseData._fixedTimestamp = state.sheetTimestamp;
}
// ── Discogs page updates (price + SKU) ───────────────────────────────────
// Resolve tab ID: use value captured at popup-open, fall back to active tab query
let tabId = discogsTabId;
if (!tabId) {
try {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
tabId = tabs[0]?.id || null;
} catch (e) {}
}
if (tabId) {
if (doPrice) {
const price = document.getElementById('price')?.value;
if (price) {
try {
await chrome.tabs.sendMessage(tabId, { action: 'updateDiscogsPrice', price });
} catch (e) { console.error('[POPUP] price error:', e); }
}
}
if (useSkuForQr && !releaseData.collectionSku) {
try {
const skuForDiscogs = (doRfid && allCfg.rfidAppendR) ? skuToUse + '-R' : skuToUse;
console.log('[POPUP] sending updateDiscogsSku', skuForDiscogs, 'tabId', tabId, 'boxIndex', state.collectionBoxIndex);
const r = await chrome.tabs.sendMessage(tabId, { action: 'updateDiscogsSku', sku: skuForDiscogs, boxIndex: state.collectionBoxIndex });
console.log('[POPUP] SKU response:', JSON.stringify(r));
} catch (e) { console.error('[POPUP] SKU error:', e); }
}
}
// ─────────────────────────────────────────────────────────────────────────
// Label + JSON
if (doLabel) generateLabel(releaseData);
if (useSkuForQr) delete releaseData.sku;
if (doJson) writeJSON(releaseData);
// MONSTERWIKI — fire-and-forget POST to ultra Postgres via Tailscale
getMonsterSettings().then(cfg => {
if (!cfg.enabled) return;
const base = (cfg.serverUrl || 'http://100.91.239.7:5002').replace(/\/$/, '');
fetch(`${base}/plice`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...buildResearchJson(releaseData),
sales_history: state.monsterSalesHistory,
current_listings: state.monsterCurrentListings,
apple_id: releaseData.appleId || null,
apple_country: releaseData.appleCountry || null,
pricing: {
media_condition: releaseData.mediaCondition || null,
sleeve_condition: releaseData.sleeveCondition || null,
price: releaseData.price || null,
sku: releaseData.collectionSku || null,
folder: releaseData.collectionFolder || null,
comment: releaseData.comment || null,
},
}),
}).catch(() => {});
});
// Scale weight → attach to releaseData (Sheets reads it via the actualWeight mapping)
const scaleCfg = await new Promise(resolve =>
chrome.storage.local.get(['scaleSaveToSheet', 'scaleRequireStable'], resolve)
);
if (scaleCfg.scaleSaveToSheet === true) {
const requireStable = scaleCfg.scaleRequireStable !== false; // default ON
const grams = await getCurrentWeight({ requireStable });
if (grams != null) {
releaseData.actual_weight = grams;
} else {
console.warn('[scale] weight not captured (offline or unstable) — sheet save proceeds without weight');
}
}
// Google Sheets
if (doSheet) {
const googleSettings = await new Promise(resolve =>
chrome.storage.local.get(['googleSettings'], resolve)
);
const hasGoogleSettings = googleSettings.googleSettings?.spreadsheetId &&
googleSettings.googleSettings?.clientEmail &&
googleSettings.googleSettings?.privateKey;
const parts = [doLabel && 'Label printed', doJson && 'JSON saved'].filter(Boolean);
const prefix = parts.join(', ');
if (hasGoogleSettings) {
const sheetResult = await updateSheetOnly(releaseData);
showStatusMessage('allStatus', `${prefix ? prefix + '. ' : ''}${sheetResult.message}`, sheetResult.success);
} else {
showStatusMessage('allStatus', `${prefix || 'Done'} (Sheets not configured)`, true);
}
} else {
const parts = [doLabel && 'Label printed', doJson && 'JSON saved'].filter(Boolean);
if (parts.length) showStatusMessage('allStatus', parts.join(', '), true);
}
delete releaseData._fixedTimestamp;
}
// Mapping tab functionality
const DEFAULT_COLUMN_MAPPINGS = {
'A': 'timestamp',
'B': 'id',
'C': 'artist',
'D': 'title',
'E': 'genre',
'F': 'style',
'G': 'label',
'H': 'year',
'I': 'country',
'J': 'format',
'K': 'mediaCondition',
'L': 'sleeveCondition',
'M': 'price',
'N': 'have',
'O': 'want',
'P': 'numForSale',
'Q': 'lowPrice',
'R': 'medianPrice',
'S': 'highPrice',
'T': 'lastSold',
'U': 'tracklist',
'V': 'extraArtists',
'W': 'videos',
'X': 'priceSuggestions',
'Y': 'labelId',
'Z': 'artistIds',
'AA': '',
'AB': 'imageUrl',
'AC': 'comment',
'AD': 'imageUrls',
'AE': 'appleId',
'AF': 'catno',
'AG': 'trackArtists',
'AH': 'collectionFolder',
'AI': 'notes',
'AJ': 'companies',
'AK': 'identifiers',
'AL': 'estimatedWeight'
};
const FIELD_OPTIONS = [
{ value: '', label: 'Skip (empty)' },
{ value: 'timestamp', label: 'Timestamp' },
{ value: 'id', label: 'Release ID' },
{ value: 'artist', label: 'Artist' },
{ value: 'title', label: 'Title' },
{ value: 'genre', label: 'Genre' },
{ value: 'style', label: 'Style' },
{ value: 'label', label: 'Label' },
{ value: 'year', label: 'Year' },
{ value: 'country', label: 'Country' },
{ value: 'format', label: 'Format' },
{ value: 'mediaCondition', label: 'Media Condition' },
{ value: 'sleeveCondition', label: 'Sleeve Condition' },
{ value: 'price', label: 'Price' },
{ value: 'have', label: 'Have' },
{ value: 'want', label: 'Want' },
{ value: 'numForSale', label: 'Num For Sale' },
{ value: 'lowPrice', label: 'Low Price' },
{ value: 'medianPrice', label: 'Median Price' },
{ value: 'highPrice', label: 'High Price' },
{ value: 'lastSold', label: 'Last Sold' },
{ value: 'tracklist', label: 'Tracklist' },
{ value: 'extraArtists', label: 'Extra Artists' },
{ value: 'videos', label: 'Videos' },
{ value: 'priceSuggestions', label: 'Price Suggestions' },
{ value: 'labelId', label: 'Label ID' },
{ value: 'artistIds', label: 'Artist IDs' },
{ value: 'imageUrl', label: 'Image URL' },
{ value: 'comment', label: 'Comment' },
{ value: 'imageUrls', label: 'Image URLs' },
{ value: 'appleId', label: 'Apple ID' },
{ value: 'catno', label: 'Catalog Number' },
{ value: 'trackArtists', label: 'Track Artists' },
{ value: 'collectionFolder', label: 'Collection Folder' },
{ value: 'notes', label: 'Notes' },
{ value: 'companies', label: 'Companies' },
{ value: 'identifiers', label: 'Identifiers' },
{ value: 'estimatedWeight', label: 'Estimated Weight' },
{ value: 'actualWeight', label: 'Actual Weight (scale)' }
];
// ── DYMO M10 scale (via rfid-daemon :7790) ────────────────────────────────────
// While a session is active the daemon keeps the HID device open and the scale
// stays awake. Click the pill to start/stop a session. With session OFF, /weight
// still works one-shot (opens, reads, closes) — useful for quick checks without
// keeping the USB warm.
const SCALE_DAEMON_BASE = 'http://localhost:7790';
let lastScale = { online: false, grams: null, stable: false, sessionActive: false, ts: 0 };
async function pollScaleOnce() {
try {
const r = await fetch(`${SCALE_DAEMON_BASE}/weight`, { cache: 'no-store' });
if (!r.ok) { lastScale = { online: false, grams: null, stable: false, sessionActive: false, ts: Date.now() }; return; }
const j = await r.json();
lastScale = {
online: !!j.online,
grams: j.grams != null ? Number(j.grams) : null,
stable: !!j.stable,
sessionActive: !!j.sessionActive,
ts: Date.now()
};
} catch (e) {
lastScale = { online: false, grams: null, stable: false, sessionActive: false, ts: Date.now() };
}
renderScalePill();
}
function renderScalePill() {
const pill = document.getElementById('scale-pill');
if (!pill) return;
chrome.storage.local.get(['scaleShowOnDiscogs'], (s) => {
const show = s.scaleShowOnDiscogs !== false; // default ON
if (!show) { pill.style.display = 'none'; return; }
pill.style.display = 'inline-block';
pill.style.cursor = 'pointer';
pill.title = lastScale.sessionActive
? 'Scale session ON — click to stop (lets scale auto-power-off to save batteries)'
: 'Scale session OFF — click to start (keeps scale awake, reads are instant)';
if (lastScale.sessionActive) {
if (!lastScale.online || lastScale.grams == null) {
pill.style.background = '#fde2e1';
pill.style.color = '#c33';
pill.textContent = '⚖ ON · scale asleep?';
} else if (lastScale.stable) {
pill.style.background = '#e6f4ea';
pill.style.color = '#0a7';
pill.textContent = `⚖ ON · ${lastScale.grams}g (stable)`;
} else {
pill.style.background = '#fff3cd';
pill.style.color = '#856404';
pill.textContent = `⚖ ON · ${lastScale.grams}g …settling`;
}
} else {
pill.style.background = '#eee';
pill.style.color = '#666';
if (lastScale.online && lastScale.grams != null) {
pill.textContent = `⚖ OFF · last ${lastScale.grams}g`;
} else {
pill.textContent = '⚖ OFF · click to start session';
}
}
});
}
async function scaleSessionToggle() {
const desiredOn = !lastScale.sessionActive;
try {
const r = await fetch(`${SCALE_DAEMON_BASE}/scale/${desiredOn ? 'start' : 'stop'}`, { method: 'POST' });
const j = await r.json();
if (!r.ok || j.error) throw new Error(j.error || `HTTP ${r.status}`);
} catch (e) {
const pill = document.getElementById('scale-pill');
if (pill) {
const prev = pill.textContent;
pill.textContent = '✗ ' + (e.message || 'toggle failed');
pill.style.background = '#fde2e1';
pill.style.color = '#c33';
setTimeout(() => { pill.textContent = prev; pollScaleOnce(); }, 2000);
}
return;
}
// Fast follow-up poll so the UI updates within a frame.
pollScaleOnce();
}
// One-shot fresh read for the save/update flows — returns most-recent grams,
// or null if scale is offline / would-be-unstable when requireStable=true.
async function getCurrentWeight({ requireStable = true } = {}) {
await pollScaleOnce();
if (!lastScale.online || lastScale.grams == null) return null;
if (requireStable && !lastScale.stable) return null;
return lastScale.grams;
}
// Wire the pill click and kick off polling.
document.addEventListener('DOMContentLoaded', () => {
const pill = document.getElementById('scale-pill');
if (pill) pill.addEventListener('click', scaleSessionToggle);
});
setInterval(pollScaleOnce, 700);
setTimeout(pollScaleOnce, 50);
// Global variable to store detected sheet structure
let detectedSheetStructure = null;
function initializeMappingTab() {
const mappingGrid = document.querySelector('.mapping-grid');
if (!mappingGrid) return;
// Clear existing content
mappingGrid.innerHTML = '';
// Use detected structure if available, otherwise use default A-AL
const columns = detectedSheetStructure ?
detectedSheetStructure.columns :
getDefaultColumns();
columns.forEach(column => {
const item = document.createElement('div');
item.className = 'mapping-item';
const label = document.createElement('label');
label.textContent = column.header || getColumnLabel(column.letter);
const columnLetter = document.createElement('div');
columnLetter.className = 'column-letter';
columnLetter.textContent = column.letter;
const select = document.createElement('select');
select.id = `mapping-${column.letter}`;
// Add options
FIELD_OPTIONS.forEach(option => {
const optionElement = document.createElement('option');
optionElement.value = option.value;
optionElement.textContent = option.label;
select.appendChild(optionElement);
});
// Set default value
select.value = DEFAULT_COLUMN_MAPPINGS[column.letter] || '';
item.appendChild(label);
item.appendChild(columnLetter);
item.appendChild(select);
mappingGrid.appendChild(item);
});
// Load saved mappings
loadMappings();
}
function getDefaultColumns() {
const letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL'];
return letters.map(letter => ({ letter, header: '', index: letters.indexOf(letter) }));
}
async function detectSheetStructureUI() {
try {
showStatusMessage('mappingStatus', 'Detecting sheet structure...', true);
// Get Google Sheets settings
const settings = await new Promise(resolve => {
chrome.storage.local.get(['googleSettings'], result => {
resolve(result.googleSettings || {});
});
});
if (!settings.spreadsheetId) {
throw new Error('No spreadsheet ID configured. Please set up Google Sheets in Settings first.');
}
// Call the detection function from sheets.js (different name to avoid conflict)
const structure = await detectSheetStructure(settings.spreadsheetId, settings.sheetName || 'Sheet1');
detectedSheetStructure = structure;
// Update UI with detected info
document.getElementById('detected-sheet-name').textContent = structure.sheetName;
document.getElementById('detected-column-range').textContent = `A:${structure.endColumn} (${structure.columnCount} columns)`;
// Regenerate the mapping grid with detected columns
initializeMappingTab();
showStatusMessage('mappingStatus', `Detected ${structure.columnCount} columns from sheet "${structure.sheetName}"`, true);
} catch (error) {
console.error('Error detecting sheet structure:', error);
showStatusMessage('mappingStatus', `Error: ${error.message}`, false);
}
}
async function autoMapHeaders() {
if (!detectedSheetStructure) {
showStatusMessage('mappingStatus', 'Please detect sheet structure first', false);
return;
}
try {
showStatusMessage('mappingStatus', 'Auto-mapping headers...', true);
// Use the detection function from sheets.js
const autoMappings = detectFieldMappings(detectedSheetStructure.columns);
// Apply the auto-detected mappings to the UI
Object.keys(autoMappings).forEach(column => {
const select = document.getElementById(`mapping-${column}`);
if (select) {
select.value = autoMappings[column];
}
});
showStatusMessage('mappingStatus', 'Headers auto-mapped based on column names!', true);
} catch (error) {
console.error('Error auto-mapping headers:', error);
showStatusMessage('mappingStatus', `Error: ${error.message}`, false);
}
}
function toggleManualRange() {
const manualInput = document.getElementById('manual-range');
const toggleButton = document.getElementById('toggle-manual-range');
const detectedRange = document.getElementById('detected-column-range');
if (manualInput.style.display === 'none') {
manualInput.style.display = 'inline-block';
manualInput.focus();
toggleButton.textContent = 'Apply';
detectedRange.style.display = 'none';
} else {
const rangeValue = manualInput.value.trim();
if (rangeValue) {
applyManualRange(rangeValue);
}
manualInput.style.display = 'none';
toggleButton.textContent = 'Manual';
detectedRange.style.display = 'inline';
}
}
function applyManualRange(rangeString) {
try {
// Parse range like "A:AL" or "A:Z"
const match = rangeString.match(/^([A-Z]+):([A-Z]+)$/i);
if (!match) {
throw new Error('Invalid range format. Use format like "A:AL"');
}
const startCol = match[1].toUpperCase();
const endCol = match[2].toUpperCase();
// Generate columns for the range
const columns = [];
let currentCol = startCol;
while (true) {
columns.push({ letter: currentCol, header: '', index: columns.length });
if (currentCol === endCol) break;
currentCol = getNextColumnLetter(currentCol);
}
// Create a manual structure
detectedSheetStructure = {
columnCount: columns.length,
endColumn: endCol,
columns: columns,
sheetName: 'Manual Range'
};
// Update UI
document.getElementById('detected-sheet-name').textContent = 'Manual Range';
document.getElementById('detected-column-range').textContent = `${startCol}:${endCol} (${columns.length} columns)`;
// Regenerate mapping grid
initializeMappingTab();
showStatusMessage('mappingStatus', `Applied manual range: ${rangeString}`, true);
} catch (error) {
showStatusMessage('mappingStatus', `Error: ${error.message}`, false);
}
}
function getNextColumnLetter(current) {
let result = '';
let carry = 1;
for (let i = current.length - 1; i >= 0; i--) {
let charCode = current.charCodeAt(i) - 65 + carry;
if (charCode > 25) {
result = 'A' + result;
carry = 1;
} else {
result = String.fromCharCode(65 + charCode) + result;
carry = 0;
break;
}
}
if (carry) {
result = 'A' + result;
}
return result;
}
function getColumnLabel(column) {
const labels = {
'A': 'TIMESTAMP', 'B': 'ID', 'C': 'ARTIST', 'D': 'TITLE', 'E': 'GENRE', 'F': 'STYLE',
'G': 'LABEL', 'H': 'YEAR', 'I': 'COUNTRY', 'J': 'FORMAT', 'K': 'MEDIA COND',
'L': 'SLEEVE COND', 'M': 'PRICE', 'N': 'HAVE', 'O': 'WANT', 'P': 'NUM FOR SALE',
'Q': 'LOW PRICE', 'R': 'MEDIAN PRICE', 'S': 'HIGH PRICE', 'T': 'LAST SOLD',
'U': 'TRACKLIST', 'V': 'EXTRA ARTISTS', 'W': 'VIDEOS', 'X': 'PRICE SUGG',
'Y': 'LABEL ID', 'Z': 'ARTIST IDS', 'AA': 'EMPTY', 'AB': 'IMAGE',
'AC': 'COMMENT', 'AD': 'IMAGE URLS', 'AE': 'APPLE ID', 'AF': 'CATNO',
'AG': 'TRACK ARTISTS', 'AH': 'COLL FOLDER', 'AI': 'NOTES', 'AJ': 'COMPANIES',
'AK': 'IDENTIFIERS', 'AL': 'EST WT'
};
return labels[column] || column;
}
function saveMappings() {
const mappings = {};
const columns = detectedSheetStructure ?
detectedSheetStructure.columns.map(c => c.letter) :
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL'];
columns.forEach(column => {
const select = document.getElementById(`mapping-${column}`);
if (select) {
mappings[column] = select.value;
}
});
// Also save the detected structure for future use
const saveData = {
columnMappings: mappings,
detectedStructure: detectedSheetStructure
};
chrome.storage.local.set(saveData, () => {
showStatusMessage('mappingStatus', 'Mappings saved successfully!', true);
});
}
function loadMappings() {
chrome.storage.local.get(['columnMappings', 'detectedStructure'], (result) => {
const savedMappings = result.columnMappings || {};
// Restore detected structure if available
if (result.detectedStructure) {
detectedSheetStructure = result.detectedStructure;
document.getElementById('detected-sheet-name').textContent = detectedSheetStructure.sheetName;
document.getElementById('detected-column-range').textContent = `A:${detectedSheetStructure.endColumn} (${detectedSheetStructure.columnCount} columns)`;
}
Object.keys(savedMappings).forEach(column => {
const select = document.getElementById(`mapping-${column}`);
if (select) {
select.value = savedMappings[column];
}
});
if (Object.keys(savedMappings).length > 0) {
showStatusMessage('mappingStatus', 'Mappings loaded successfully!', true);
}
});
}
function resetMappings() {
const columns = detectedSheetStructure ?
detectedSheetStructure.columns.map(c => c.letter) :
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'AA', 'AB', 'AC', 'AD', 'AE', 'AF', 'AG', 'AH', 'AI', 'AJ', 'AK', 'AL'];
columns.forEach(column => {
const select = document.getElementById(`mapping-${column}`);
if (select) {
select.value = DEFAULT_COLUMN_MAPPINGS[column] || '';
}
});
showStatusMessage('mappingStatus', 'Mappings reset to defaults!', true);
}
// Export function to get current mappings for use in sheets.js
function getCurrentMappings() {
return new Promise((resolve) => {
chrome.storage.local.get(['columnMappings'], (result) => {
const savedMappings = result.columnMappings || {};
const finalMappings = { ...DEFAULT_COLUMN_MAPPINGS, ...savedMappings };
resolve(finalMappings);
});
});
}
// ============================================
// TEXT LABEL TAB FUNCTIONALITY
// ============================================
// Initialize text label tab
function initializeTextLabelTab() {
const textInput = document.getElementById('text-input-area');
const previewContent = document.getElementById('label-preview-content');
const previewBox = document.getElementById('label-preview-box');
const fontFamilySelect = document.getElementById('text-font-family');
const fontSizeInput = document.getElementById('text-font-size');
const boldButton = document.getElementById('text-bold');
const underlineButton = document.getElementById('text-underline');
const wrapCheckbox = document.getElementById('text-wrap');
const printButton = document.getElementById('print-text-label');
const clearButton = document.getElementById('clear-text-label');
const labelSizeElement = document.getElementById('labelSize');
const currentLabelSizeDisplay = document.getElementById('current-label-size');
if (!textInput || !previewContent) return; // Elements don't exist yet
// State management for text label
const labelState = {
text: '',
fontFamily: 'Arial',
fontSize: 12,
bold: false,
underline: false,
wrap: true,
horizontalAlign: 'left',
verticalAlign: 'top'
};
// Update label size display
function updateLabelSizeDisplay() {
if (labelSizeElement && currentLabelSizeDisplay && previewBox) {
const size = labelSizeElement.value;
const sizeText = size === 'large' ? 'Large (25mm x 54mm)' : 'Small (19mm x 51mm)';
currentLabelSizeDisplay.textContent = sizeText;
// Update preview box size
if (size === 'small') {
previewBox.classList.add('small');
} else {
previewBox.classList.remove('small');
}
}
}
// Update preview based on current state
function updatePreview() {
// Update text content
previewContent.textContent = labelState.text || '';
// Apply font family
previewContent.style.fontFamily = labelState.fontFamily;
// Apply font size
previewContent.style.fontSize = labelState.fontSize + 'pt';
// Apply bold
if (labelState.bold) {
previewContent.classList.add('bold');
} else {
previewContent.classList.remove('bold');
}
// Apply underline
if (labelState.underline) {
previewContent.classList.add('underline');
} else {
previewContent.classList.remove('underline');
}
// Apply wrap
if (labelState.wrap) {
previewContent.classList.remove('no-wrap');
} else {
previewContent.classList.add('no-wrap');
}
// Apply horizontal alignment
previewContent.classList.remove('align-left', 'align-center', 'align-right');
previewContent.classList.add('align-' + labelState.horizontalAlign);
// Apply vertical alignment
previewContent.classList.remove('valign-top', 'valign-middle', 'valign-bottom');
previewContent.classList.add('valign-' + labelState.verticalAlign);
}
// Listen for label size changes in main tab
if (labelSizeElement) {
labelSizeElement.addEventListener('change', updateLabelSizeDisplay);
updateLabelSizeDisplay(); // Initial update
}
// Text input change
if (textInput) {
textInput.addEventListener('input', () => {
labelState.text = textInput.value;
updatePreview();
});
}
// Font family change
if (fontFamilySelect) {
fontFamilySelect.addEventListener('change', () => {
labelState.fontFamily = fontFamilySelect.value;
updatePreview();
});
}
// Font size change
if (fontSizeInput) {
fontSizeInput.addEventListener('input', () => {
const size = parseInt(fontSizeInput.value) || 12;
labelState.fontSize = size;
updatePreview();
});
}
// Bold toggle
if (boldButton) {
boldButton.addEventListener('click', (e) => {
e.preventDefault();
labelState.bold = !labelState.bold;
boldButton.classList.toggle('active');
updatePreview();
});
}
// Underline toggle
if (underlineButton) {
underlineButton.addEventListener('click', (e) => {
e.preventDefault();
labelState.underline = !labelState.underline;
underlineButton.classList.toggle('active');
updatePreview();
});
}
// Word wrap toggle
if (wrapCheckbox) {
wrapCheckbox.addEventListener('change', () => {
labelState.wrap = wrapCheckbox.checked;
updatePreview();
});
}
// Horizontal alignment buttons
const alignLeftBtn = document.getElementById('align-left');
const alignCenterBtn = document.getElementById('align-center');
const alignRightBtn = document.getElementById('align-right');
if (alignLeftBtn) {
alignLeftBtn.addEventListener('click', (e) => {
e.preventDefault();
labelState.horizontalAlign = 'left';
alignLeftBtn.classList.add('active');
alignCenterBtn?.classList.remove('active');
alignRightBtn?.classList.remove('active');
updatePreview();
});
}
if (alignCenterBtn) {
alignCenterBtn.addEventListener('click', (e) => {
e.preventDefault();
labelState.horizontalAlign = 'center';
alignCenterBtn.classList.add('active');
alignLeftBtn?.classList.remove('active');
alignRightBtn?.classList.remove('active');
updatePreview();
});
}
if (alignRightBtn) {
alignRightBtn.addEventListener('click', (e) => {
e.preventDefault();
labelState.horizontalAlign = 'right';
alignRightBtn.classList.add('active');
alignLeftBtn?.classList.remove('active');
alignCenterBtn?.classList.remove('active');
updatePreview();
});
}
// Vertical alignment buttons
const alignTopBtn = document.getElementById('align-top');
const alignMiddleBtn = document.getElementById('align-middle');
const alignBottomBtn = document.getElementById('align-bottom');
if (alignTopBtn) {
alignTopBtn.addEventListener('click', (e) => {
e.preventDefault();
labelState.verticalAlign = 'top';
alignTopBtn.classList.add('active');
alignMiddleBtn?.classList.remove('active');
alignBottomBtn?.classList.remove('active');
updatePreview();
});
}
if (alignMiddleBtn) {
alignMiddleBtn.addEventListener('click', (e) => {
e.preventDefault();
labelState.verticalAlign = 'middle';
alignMiddleBtn.classList.add('active');
alignTopBtn?.classList.remove('active');
alignBottomBtn?.classList.remove('active');
updatePreview();
});
}
if (alignBottomBtn) {
alignBottomBtn.addEventListener('click', (e) => {
e.preventDefault();
labelState.verticalAlign = 'bottom';
alignBottomBtn.classList.add('active');
alignTopBtn?.classList.remove('active');
alignMiddleBtn?.classList.remove('active');
updatePreview();
});
}
// Clear button
if (clearButton) {
clearButton.addEventListener('click', () => {
// Reset state
labelState.text = '';
labelState.fontFamily = 'Arial';
labelState.fontSize = 12;
labelState.bold = false;
labelState.underline = false;
labelState.wrap = true;
labelState.horizontalAlign = 'left';
labelState.verticalAlign = 'top';
// Reset UI
textInput.value = '';
fontFamilySelect.value = 'Arial';
fontSizeInput.value = '12';
boldButton.classList.remove('active');
underlineButton.classList.remove('active');
wrapCheckbox.checked = true;
// Reset alignment buttons
const alignLeftBtn = document.getElementById('align-left');
const alignCenterBtn = document.getElementById('align-center');
const alignRightBtn = document.getElementById('align-right');
const alignTopBtn = document.getElementById('align-top');
const alignMiddleBtn = document.getElementById('align-middle');
const alignBottomBtn = document.getElementById('align-bottom');
alignLeftBtn?.classList.add('active');
alignCenterBtn?.classList.remove('active');
alignRightBtn?.classList.remove('active');
alignTopBtn?.classList.add('active');
alignMiddleBtn?.classList.remove('active');
alignBottomBtn?.classList.remove('active');
// Update preview
updatePreview();
});
}
// Print button
if (printButton) {
printButton.addEventListener('click', () => {
printTextLabel(labelState);
});
}
// Initial preview update
updatePreview();
}
// Print text label function
function printTextLabel(labelState) {
const labelSizeElement = document.getElementById('labelSize');
const statusElement = document.getElementById('text-label-status');
// Check if there's actual text content
if (!labelState.text || labelState.text.trim() === '') {
if (statusElement) {
statusElement.textContent = 'Please enter some text first';
statusElement.style.color = '#dc3545';
setTimeout(() => {
statusElement.textContent = '';
}, 3000);
}
return;
}
// Get label size
const labelSize = labelSizeElement ? labelSizeElement.value : 'large';
const dimensions = labelSize === 'large'
? { width: '54mm', height: '25mm' }
: { width: '51mm', height: '19mm' };
// Build text styles
const fontWeight = labelState.bold ? 'bold' : 'normal';
const textDecoration = labelState.underline ? 'underline' : 'none';
const whiteSpace = labelState.wrap ? 'pre-wrap' : 'pre';
// Map horizontal alignment to flexbox align-items
let alignItems = 'flex-start';
if (labelState.horizontalAlign === 'center') {
alignItems = 'center';
} else if (labelState.horizontalAlign === 'right') {
alignItems = 'flex-end';
}
// Map vertical alignment to flexbox justify-content
let justifyContent = 'flex-start';
if (labelState.verticalAlign === 'middle') {
justifyContent = 'center';
} else if (labelState.verticalAlign === 'bottom') {
justifyContent = 'flex-end';
}
// Create print window content
const printContent = `
<!DOCTYPE html>
<html>
<head>
<title>Print Text Label</title>
<style>
@page {
size: ${dimensions.width} ${dimensions.height};
margin: 0;
}
body {
margin: 0;
padding: 0;
width: ${dimensions.width};
height: ${dimensions.height};
display: flex;
align-items: center;
justify-content: center;
}
.label-content {
width: 100%;
height: 100%;
padding: 2mm;
box-sizing: border-box;
font-family: ${labelState.fontFamily};
font-size: ${labelState.fontSize}pt;
font-weight: ${fontWeight};
text-decoration: ${textDecoration};
white-space: ${whiteSpace};
overflow: hidden;
word-wrap: break-word;
display: flex;
flex-direction: column;
align-items: ${alignItems};
justify-content: ${justifyContent};
}
</style>
</head>
<body>
<div class="label-content">${escapeHtml(labelState.text)}</div>
<script>
window.onload = function() {
window.print();
setTimeout(function() {
window.close();
}, 100);
};
</script>
</body>
</html>
`;
// Open print window
const printWindow = window.open('', '_blank', 'width=600,height=400');
if (printWindow) {
printWindow.document.open();
printWindow.document.write(printContent);
printWindow.document.close();
// Show success message
if (statusElement) {
statusElement.textContent = 'Label sent to printer!';
statusElement.style.color = '#28a745';
setTimeout(() => {
statusElement.textContent = '';
}, 3000);
}
} else {
// Show error if popup was blocked
if (statusElement) {
statusElement.textContent = 'Print window blocked. Please allow popups.';
statusElement.style.color = '#dc3545';
setTimeout(() => {
statusElement.textContent = '';
}, 3000);
}
}
}
// Helper function to escape HTML
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Initialize QR Code tab
function initializeQRCodeTab() {
const qrcodeInput = document.getElementById('qrcode-integer-input');
const qrcodePreviewImg = document.getElementById('qrcode-preview-img');
const printButton = document.getElementById('print-qrcode-label');
const clearButton = document.getElementById('clear-qrcode');
const statusElement = document.getElementById('qrcode-status');
if (!qrcodeInput) return; // Elements don't exist yet
// Generate QR code URL with maximum size for preview
function generateQRUrl(integer, size) {
return `https://api.qrserver.com/v1/create-qr-code/?size=${size}x${size}&data=${encodeURIComponent(String(integer))}`;
}
// Update preview when input changes
function updatePreview() {
const value = qrcodeInput.value.trim();
if (value === '' || isNaN(parseInt(value))) {
qrcodePreviewImg.style.display = 'none';
return;
}
// Use 200x200 for preview (will scale to fit)
qrcodePreviewImg.src = generateQRUrl(parseInt(value), 200);
qrcodePreviewImg.style.display = 'block';
}
// Listen for input changes
qrcodeInput.addEventListener('input', updatePreview);
// Print button click handler
if (printButton) {
printButton.addEventListener('click', function() {
const value = qrcodeInput.value.trim();
if (value === '' || isNaN(parseInt(value))) {
if (statusElement) {
statusElement.textContent = 'Please enter a valid integer';
statusElement.style.color = '#dc3545';
setTimeout(() => { statusElement.textContent = ''; }, 3000);
}
return;
}
const integer = parseInt(value);
// Label dimensions: 54mm x 25mm (large label)
// QR code is square, so height is limiting factor (25mm)
// Use 300x300 for best quality, CSS will scale to fit height
const qrSize = 300; // Use 300x300 for best quality
const qrUrl = generateQRUrl(integer, qrSize);
// Create print content - maximize QR code on label
const printContent = `
<!DOCTYPE html>
<html>
<head>
<title>Print QR Code</title>
<style>
@page {
size: 54mm 25mm;
margin: 0;
}
body {
margin: 0;
padding: 0;
width: 54mm;
height: 25mm;
display: flex;
align-items: center;
justify-content: center;
background: white;
}
.qr-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.qr-container img {
max-width: 95%;
max-height: 95%;
object-fit: contain;
}
</style>
</head>
<body>
<div class="qr-container">
<img src="${qrUrl}" alt="${integer}">
</div>
<script>
window.onload = function() {
window.print();
setTimeout(function() {
window.close();
}, 500);
};
</script>
</body>
</html>
`;
// Open print window
const printWindow = window.open('', '_blank', 'width=600,height=400');
if (printWindow) {
printWindow.document.open();
printWindow.document.write(printContent);
printWindow.document.close();
if (statusElement) {
statusElement.textContent = 'Label sent to printer!';
statusElement.style.color = '#28a745';
setTimeout(() => { statusElement.textContent = ''; }, 3000);
}
} else {
if (statusElement) {
statusElement.textContent = 'Print window blocked. Please allow popups.';
statusElement.style.color = '#dc3545';
setTimeout(() => { statusElement.textContent = ''; }, 3000);
}
}
});
}
// Clear button click handler
if (clearButton) {
clearButton.addEventListener('click', function() {
qrcodeInput.value = '';
qrcodePreviewImg.style.display = 'none';
if (statusElement) {
statusElement.textContent = '';
}
});
}
}
// Initialize text label tab when DOM is ready
document.addEventListener('DOMContentLoaded', function () {
// Add a small delay to ensure all elements are loaded
setTimeout(() => {
initializeTextLabelTab();
initializeQRCodeTab();
}, 100);
});
// ================================================================
// RESEARCH TAB — LLM Research Export
// ================================================================
/**
* Build the comprehensive LLM-ready JSON from all available release data.
*/
// buildResearchJson moved to monster_capture.js (loaded before popup.js) so the
// passive background auto-capture path shares identical payload shaping.
function buildGeminiInputJson(rd) {
const have = parseInt(rd.have) || 0;
const want = parseInt(rd.want) || 0;
const priceSugg = {};
if (rd.priceSuggestions) {
Object.entries(rd.priceSuggestions).forEach(([cond, data]) => {
const v = (data && typeof data === 'object') ? data.value : data;
if (v != null) priceSugg[cond] = v;
});
}
const tracklist = (rd.tracklist || [])
.filter(t => t.type_ !== 'heading')
.map(t => {
const track = { position: t.position, title: t.title };
if (t.duration) track.duration = t.duration;
const artists = (t.artists || [])
.map(a => a.name.replace(/\s*\(\d+\)$/, '').trim())
.filter(Boolean);
if (artists.length) track.artists = artists;
const extra = (t.extraartists || [])
.map(a => ({ name: a.name.replace(/\s*\(\d+\)$/, '').trim(), role: a.role }))
.filter(a => a.name);
if (extra.length) track.extraartists = extra;
return track;
});
const cleanReview = r => ({
...(r.username && { username: r.username }),
...(r.date && { date: r.date }),
...(r.rating && { rating: r.rating }),
...(r.text && { text: r.text.trim() }),
...(r.helpfulCount != null && r.helpfulCount > 0 && { helpful: r.helpfulCount }),
...(r.replies?.length && { replies: r.replies.map(cleanReview) }),
});
const reviews = (rd.reviews || [])
.filter(r => r.text || r.rating)
.map(cleanReview);
const formats = (rd.formats || [])
.map(f => [f.name, ...(f.descriptions || [])].filter(Boolean).join(', '));
const companies = (rd.companies || []).map(c => ({
name: c.name,
role: c.entity_type_name || c.entity_type,
})).filter(c => c.name);
return {
release: {
artist: rd.artist,
title: rd.title,
label: rd.label,
year: rd.year,
country: rd.country,
format: formats.join(' / '),
genre: rd.genres || (rd.genre ? rd.genre.split(', ') : []),
style: rd.styles || (rd.style ? rd.style.split(', ') : []),
tracklist,
companies,
identifiers: rd.identifiers || [],
},
market: {
have,
want,
for_sale: rd.num_for_sale != null ? parseInt(rd.num_for_sale) : null,
median_price: rd.medianPrice || null,
price_suggestions: priceSugg,
},
community: {
reviews,
},
};
}
function buildGeminiPromptText(inputJson) {
return buildGeminiPromptTextWithTemplate(defaultGeminiPromptTemplate(), inputJson);
}
function defaultGeminiPromptTemplate() {
return [
'If the output is not valid JSON, regenerate it correctly.',
'',
'You are a record store assistant writing structured data for a vinyl inventory system.',
'',
'INPUT:',
'A JSON object containing Discogs-style release, market, and community data.',
'',
'TASK:',
'Analyze the input and return ONLY valid JSON. No markdown, no commentary, no extra text.',
'',
'OUTPUT SCHEMA:',
'{',
' "summary": "Short factual description (1-2 sentences)",',
'',
' "store_description": "Punchy, slightly opinionated record store blurb (max 80 words, human tone, not generic AI)",',
'',
' "features": {',
' "special_traits": [],',
' "pressing_notes": "",',
' "notable_physical": ""',
' },',
'',
' "sound_profile": {',
' "genres": [],',
' "descriptors": [],',
' "dj_use": []',
' },',
'',
' "highlight_tracks": [',
' {',
' "title": "",',
' "note": ""',
' }',
' ],',
'',
' "production": {',
' "mastering": null,',
' "pressing": null,',
' "distribution": null,',
' "notes": ""',
' },',
'',
' "market_insight": {',
' "rarity": "common | uncommon | scarce",',
' "collector_notes": "",',
' "price_commentary": ""',
' },',
'',
' "sales_angles": [],',
'',
' "embedding": {',
' "description": "One clean sentence summarizing the record for similarity search",',
' "vibe": "One clean sentence describing sound/vibe (no lists)",',
' "sales": "One clean sentence describing who this is for / why it sells (no lists)"',
' },',
' "eom": "__PLICE_EOM__"',
'}',
'',
'RULES:',
'- Use only information from the input JSON',
'- Do NOT hallucinate credits or facts not present',
'- Infer vibe and sound from genre/style/reviews',
'- Keep language concise and information-dense',
'- "store_description" should feel like a real record shop, not corporate or overly polished',
'- Each embedding field must be a single natural sentence, no lists',
'- Do NOT invent or substitute production credits',
'- If data is missing, return null instead of guessing',
'- Only use company names explicitly present in the input',
'- Map roles exactly (Mastered At → mastering, Pressed By → pressing, Distributed By → distribution)',
'- Use exact track titles from input tracklist',
'- Do NOT rename or generalize track names',
'- The release title is NOT a track name',
'- Only use track titles found in input.tracklist',
'- If no track is clearly highlighted in the input, return an empty array',
'- Do NOT infer or create a “title track”',
'- If a review references a side (e.g. "B-side") and the tracklist contains a small number of tracks, infer the corresponding track.',
'- Always include the field "eom" with the exact value "__PLICE_EOM__" at the top level of the output JSON',
'',
'INPUT JSON:',
'{{INPUT_JSON}}',
].join('\n');
}
function buildGeminiPromptTextWithTemplate(template, inputJson) {
const inputStr = JSON.stringify(inputJson, null, 2);
if (!template || typeof template !== 'string') return defaultGeminiPromptTemplate().replace('{{INPUT_JSON}}', inputStr);
if (template.includes('{{INPUT_JSON}}')) return template.replace('{{INPUT_JSON}}', inputStr);
return template.trimEnd() + '\n\n' + inputStr;
}
async function startGeminiJob({ openInBackground, silent } = {}) {
if (!state.releaseData?.id) return { ok: false, error: 'No release data.' };
const editor = document.getElementById('gemini-input-editor');
let inputJson;
if (editor && editor.value.trim()) {
try { inputJson = JSON.parse(editor.value); }
catch { return { ok: false, error: 'Gemini Input JSON is not valid JSON.' }; }
} else {
inputJson = buildGeminiInputJson(state.releaseData);
}
const cfg = await new Promise(resolve =>
chrome.storage.local.get(['geminiPromptTemplate', 'aiProvider'], r => resolve(r || {}))
);
const tpl = cfg.geminiPromptTemplate || defaultGeminiPromptTemplate();
const provider = (cfg.aiProvider === 'deepseek') ? 'deepseek' : 'gemini';
const promptText = buildGeminiPromptTextWithTemplate(tpl, inputJson);
const jobId = `ai-${state.releaseData.id}-${Date.now()}`;
const job = {
job_id: jobId,
status: 'pending',
created_at: new Date().toISOString(),
model: provider === 'deepseek' ? 'deepseek-web' : 'gemini-web',
provider,
release_id: state.releaseData.id,
discogs_url: `https://www.discogs.com/release/${state.releaseData.id}`,
input_json: inputJson,
prompt: promptText,
};
await new Promise(resolve => chrome.storage.local.set({ geminiJob: job }, resolve));
const url = provider === 'deepseek' ? 'https://chat.deepseek.com/' : 'https://gemini.google.com/app';
chrome.tabs.create({ url, active: openInBackground !== true });
if (!silent) {
setResearchStatus(`${provider === 'deepseek' ? 'DeepSeek' : 'Gemini'} tab opened — running prompt…`, 'ok');
setTimeout(() => setResearchStatus(''), 4000);
}
return { ok: true, job_id: jobId };
}
function maybeAutoRunGemini() {
const rd = state.releaseData;
if (!rd?.id) return;
if (state.geminiAutoTriggered) return;
state.geminiAutoTriggered = true;
(async () => {
const cfg = await new Promise(resolve =>
chrome.storage.local.get(['geminiAutoRunOnPopup', 'geminiAutoLast', 'geminiJob'], r => resolve(r || {}))
);
if (cfg.geminiAutoRunOnPopup !== true) return;
const activeJob = cfg.geminiJob;
if (activeJob && ['pending', 'running', 'posting'].includes(activeJob.status)) return;
const last = cfg.geminiAutoLast || {};
const lastAt = last.at ? Date.parse(last.at) : 0;
if (last.release_id === rd.id && lastAt && (Date.now() - lastAt) < 6 * 60 * 60 * 1000) return;
await new Promise(resolve =>
chrome.storage.local.set({ geminiAutoLast: { release_id: rd.id, at: new Date().toISOString() } }, resolve)
);
await startGeminiJob({ openInBackground: true, silent: true });
})();
}
/**
* Populate the Research tab UI from state.releaseData.
* Safe to call even if no data yet — it gracefully hides sections.
*/
function updateResearchTab() {
const rd = state.releaseData;
const hasData = rd && rd.id;
// Elements
const descEl = document.getElementById('research-desc');
const card = document.getElementById('research-release-card');
const marketEl = document.getElementById('research-market');
const trackWrap = document.getElementById('research-tracklist-wrap');
const actionsEl = document.getElementById('research-actions');
const previewWrap = document.getElementById('research-preview-wrap');
if (!descEl) return; // tab not in DOM yet
if (!hasData) {
descEl.textContent = 'Navigate to a Discogs release page and open the popup to load release data.';
[card, marketEl, trackWrap, actionsEl, previewWrap].forEach(el => {
if (el) el.style.display = 'none';
});
return;
}
descEl.textContent = 'All available data captured — ready to export.';
// --- Release card ---
card.style.display = 'flex';
const img = document.getElementById('research-img');
const placeholder = document.getElementById('research-cover-placeholder');
const coverUrl = rd.imageUrl || ((rd.images || [])[0]?.uri) || '';
if (coverUrl) {
img.src = coverUrl;
img.style.display = 'block';
if (placeholder) placeholder.style.display = 'none';
} else {
img.style.display = 'none';
if (placeholder) placeholder.style.display = 'flex';
}
document.getElementById('research-artist').textContent = rd.artist || '';
document.getElementById('research-title').textContent = rd.title || '';
document.getElementById('research-label').textContent = rd.label || '';
document.getElementById('research-year').textContent = rd.year || '';
document.getElementById('research-country').textContent = rd.country || '';
const catno = (rd.labels || [])[0]?.catno || '';
document.getElementById('research-catno').textContent = catno;
const catSep = document.getElementById('research-catno-sep');
if (catSep) catSep.style.display = catno ? '' : 'none';
const formats = (rd.formats || [])
.map(f => [f.name, ...(f.descriptions || [])].filter(Boolean).join(', '));
document.getElementById('research-format').textContent = formats.join(' / ');
// Genre + style pills
const genres = rd.genres || (rd.genre ? rd.genre.split(', ') : []);
const styles = rd.styles || (rd.style ? rd.style.split(', ') : []);
const tagsEl = document.getElementById('research-tags');
tagsEl.innerHTML = [
...genres.map(g => `<span class="research-tag research-tag-genre">${g}</span>`),
...styles.map(s => `<span class="research-tag research-tag-style">${s}</span>`),
].join('');
// --- Market stats ---
const have = parseInt(rd.have) || 0;
const want = parseInt(rd.want) || 0;
const ratio = have > 0 ? (want / have).toFixed(2) : '—';
document.getElementById('rs-have').textContent = have || '—';
document.getElementById('rs-want').textContent = want || '—';
document.getElementById('rs-ratio').textContent = ratio;
document.getElementById('rs-forsale').textContent = rd.num_for_sale != null ? rd.num_for_sale : '—';
document.getElementById('rs-low').textContent = rd.lowPrice || '—';
document.getElementById('rs-med').textContent = rd.medianPrice || '—';
document.getElementById('rs-high').textContent = rd.highPrice || '—';
document.getElementById('rs-lastsold').textContent = rd.lastSold || '—';
const reviews = (rd.reviews || []).filter(r => r.text || r.rating);
const replyCount = reviews.reduce((n, r) => n + (r.replies?.length || 0), 0);
const reviewLabel = reviews.length
? (replyCount ? `${reviews.length}+${replyCount}` : `${reviews.length}`)
: '—';
document.getElementById('rs-reviews').textContent = reviewLabel;
const tracks = (rd.tracklist || []).filter(t => t.type_ !== 'heading');
document.getElementById('rs-tracks').textContent = tracks.length || '—';
// Ratio signal hint
const sigEl = document.getElementById('rs-ratio-signal');
if (sigEl && have > 0) {
const r = want / have;
sigEl.textContent = r >= 1 ? 'hot' : r >= 0.5 ? 'sought' : '';
}
marketEl.style.display = 'grid';
// --- Tracklist ---
const trackEl = document.getElementById('research-tracklist');
if (tracks.length) {
trackEl.innerHTML = tracks.map(t => {
const extras = (t.extraartists || [])
.map(a => a.name.replace(/\s*\(\d+\)$/, ''))
.join(', ');
return `<div class="research-track">
<span class="research-track-pos">${t.position || ''}</span>
<span class="research-track-title">${t.title || ''}</span>
${extras ? `<span class="research-track-extra">${extras}</span>` : ''}
${t.duration ? `<span class="research-track-dur">${t.duration}</span>` : ''}
</div>`;
}).join('');
trackWrap.style.display = 'block';
} else {
trackWrap.style.display = 'none';
}
// --- Actions ---
actionsEl.style.display = 'flex';
previewWrap.style.display = 'block';
// Show Drive button only if service account is configured
chrome.storage.local.get(['googleSettings'], result => {
const driveBtn = document.getElementById('research-drive');
if (!driveBtn) return;
const hasSA = !!(result.googleSettings && result.googleSettings.serviceAccountJson);
driveBtn.style.display = hasSA ? '' : 'none';
});
// Clear any stale status/preview
setResearchStatus('');
const preview = document.getElementById('research-preview');
if (preview) preview.textContent = '';
const geminiInputEditor = document.getElementById('gemini-input-editor');
if (geminiInputEditor) geminiInputEditor.value = JSON.stringify(buildGeminiInputJson(rd), null, 2);
const geminiPromptPreview = document.getElementById('gemini-prompt-preview');
if (geminiPromptPreview) geminiPromptPreview.textContent = '';
const toggle = document.getElementById('research-toggle-preview');
if (toggle) {
toggle.innerHTML = '<span id="research-toggle-icon">▶</span> Preview JSON';
if (preview) preview.style.display = 'none';
}
const giToggle = document.getElementById('gemini-toggle-input');
if (giToggle) giToggle.innerHTML = '<span id="gemini-input-toggle-icon">▶</span> Preview/Edit Gemini Input';
if (geminiInputEditor) geminiInputEditor.style.display = 'none';
const gpToggle = document.getElementById('gemini-toggle-prompt');
if (gpToggle) gpToggle.innerHTML = '<span id="gemini-prompt-toggle-icon">▶</span> Preview Gemini Prompt';
if (geminiPromptPreview) geminiPromptPreview.style.display = 'none';
maybeAutoRunGemini();
}
/**
* Set a status message on the research tab.
* @param {string} msg
* @param {'ok'|'err'|'inf'|''} type
*/
function setResearchStatus(msg, type) {
const el = document.getElementById('research-status');
if (!el) return;
el.textContent = msg;
el.className = 'research-status' + (type ? ` ${type}` : '');
}
/**
* Upload a JSON file to Google Drive using the configured service account.
*/
async function uploadToDrive(jsonString, filename) {
const settings = await new Promise(resolve =>
chrome.storage.local.get(['googleSettings'], r => resolve(r.googleSettings))
);
if (!settings || !settings.serviceAccountJson) {
throw new Error('Google service account not configured — add it in Settings > Connect.');
}
let sa;
try { sa = JSON.parse(settings.serviceAccountJson); }
catch (e) { throw new Error('Service account JSON is invalid.'); }
// JWT with drive.file scope (createJWT is from sheets.js, now accepts optional scope)
const jwt = await createJWT(
sa.client_email,
sa.private_key,
'https://www.googleapis.com/auth/drive.file'
);
const token = await getAccessToken(jwt);
const boundary = 'PliceCogsBoundary' + Date.now();
const meta = JSON.stringify({ name: filename, mimeType: 'application/json' });
const body = [
`--${boundary}`,
'Content-Type: application/json; charset=UTF-8',
'',
meta,
`--${boundary}`,
'Content-Type: application/json',
'',
jsonString,
`--${boundary}--`,
].join('\r\n');
const resp = await fetch(
'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': `multipart/related; boundary=${boundary}`,
},
body,
}
);
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.error?.message || `Drive upload failed (${resp.status})`);
}
return await resp.json(); // returns { id, name, ... }
}
// ----------------------------------------------------------------
// Research tab event wiring
// ----------------------------------------------------------------
document.addEventListener('DOMContentLoaded', function () {
// PROMPT button in Discogs tab → switch to Research tab
const promptBtn = document.getElementById('promptResearch');
if (promptBtn) {
promptBtn.addEventListener('click', () => {
const researchTabBtn = document.querySelector('.tab-button[data-tab="research"]');
if (researchTabBtn) researchTabBtn.click();
});
}
// Download JSON
const dlBtn = document.getElementById('research-download');
if (dlBtn) {
dlBtn.addEventListener('click', () => {
if (!state.releaseData?.id) return;
const json = JSON.stringify(buildResearchJson(state.releaseData), null, 2);
const filename = `discogs-${state.releaseData.id}-research.json`;
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
chrome.downloads.download({ url, filename, saveAs: false }, () => {
URL.revokeObjectURL(url);
setResearchStatus(`Saved: ${filename}`, 'ok');
setTimeout(() => setResearchStatus(''), 3000);
});
});
}
// Copy JSON to clipboard
const copyBtn = document.getElementById('research-copy');
if (copyBtn) {
copyBtn.addEventListener('click', async () => {
if (!state.releaseData?.id) return;
const json = JSON.stringify(buildResearchJson(state.releaseData), null, 2);
try {
await navigator.clipboard.writeText(json);
setResearchStatus('Copied to clipboard — paste straight into your LLM.', 'ok');
setTimeout(() => setResearchStatus(''), 3000);
} catch (e) {
setResearchStatus('Clipboard write failed.', 'err');
}
});
}
const geminiBtn = document.getElementById('research-gemini');
if (geminiBtn) {
geminiBtn.addEventListener('click', async () => {
if (!state.releaseData?.id) return;
geminiBtn.disabled = true;
const orig = geminiBtn.innerHTML;
geminiBtn.innerHTML = '<span class="research-btn-icon">⏳</span> Gemini…';
setResearchStatus('Opening Gemini…', 'inf');
try {
const r = await startGeminiJob({ openInBackground: false, silent: true });
if (!r.ok) throw new Error(r.error || 'Failed to start Gemini.');
} catch (e) {
setResearchStatus(e.message || 'Failed to start Gemini.', 'err');
} finally {
geminiBtn.disabled = false;
geminiBtn.innerHTML = orig;
}
});
}
// Upload to Google Drive
const driveBtn = document.getElementById('research-drive');
if (driveBtn) {
driveBtn.addEventListener('click', async () => {
if (!state.releaseData?.id) return;
driveBtn.disabled = true;
const orig = driveBtn.innerHTML;
driveBtn.innerHTML = '<span class="research-btn-icon">⏳</span> Uploading…';
setResearchStatus('Uploading to Google Drive…', 'inf');
try {
const json = JSON.stringify(buildResearchJson(state.releaseData), null, 2);
const filename = `discogs-${state.releaseData.id}-research.json`;
const file = await uploadToDrive(json, filename);
setResearchStatus(`Saved to Drive: ${file.name}`, 'ok');
setTimeout(() => setResearchStatus(''), 4000);
} catch (e) {
setResearchStatus(e.message, 'err');
} finally {
driveBtn.disabled = false;
driveBtn.innerHTML = orig;
}
});
}
// JSON preview toggle
const toggleBtn = document.getElementById('research-toggle-preview');
if (toggleBtn) {
toggleBtn.addEventListener('click', () => {
const preview = document.getElementById('research-preview');
const icon = document.getElementById('research-toggle-icon');
if (!preview) return;
if (preview.style.display === 'none') {
// Build fresh and show
if (state.releaseData?.id) {
preview.textContent = JSON.stringify(buildResearchJson(state.releaseData), null, 2);
}
preview.style.display = 'block';
if (icon) icon.textContent = '▼';
toggleBtn.querySelector('span + text') // just update the label
toggleBtn.innerHTML = `<span id="research-toggle-icon">▼</span> Hide JSON`;
} else {
preview.style.display = 'none';
toggleBtn.innerHTML = `<span id="research-toggle-icon">▶</span> Preview JSON`;
}
});
}
const geminiInputToggle = document.getElementById('gemini-toggle-input');
if (geminiInputToggle) {
geminiInputToggle.addEventListener('click', () => {
const editor = document.getElementById('gemini-input-editor');
const icon = document.getElementById('gemini-input-toggle-icon');
if (!editor) return;
if (editor.style.display === 'none') {
if (state.releaseData?.id && !editor.value.trim()) {
editor.value = JSON.stringify(buildGeminiInputJson(state.releaseData), null, 2);
}
editor.style.display = 'block';
if (icon) icon.textContent = '▼';
geminiInputToggle.innerHTML = `<span id="gemini-input-toggle-icon">▼</span> Hide Gemini Input`;
} else {
editor.style.display = 'none';
geminiInputToggle.innerHTML = `<span id="gemini-input-toggle-icon">▶</span> Preview/Edit Gemini Input`;
}
});
}
const geminiPromptToggle = document.getElementById('gemini-toggle-prompt');
if (geminiPromptToggle) {
geminiPromptToggle.addEventListener('click', async () => {
const preview = document.getElementById('gemini-prompt-preview');
const icon = document.getElementById('gemini-prompt-toggle-icon');
if (!preview) return;
if (preview.style.display === 'none') {
if (state.releaseData?.id) {
const promptCfg = await new Promise(resolve => chrome.storage.local.get(['geminiPromptTemplate'], r => resolve(r.geminiPromptTemplate || null)));
const tpl = promptCfg || defaultGeminiPromptTemplate();
let inputJson;
const editor = document.getElementById('gemini-input-editor');
if (editor && editor.value.trim()) {
try { inputJson = JSON.parse(editor.value); }
catch { inputJson = buildGeminiInputJson(state.releaseData); }
} else {
inputJson = buildGeminiInputJson(state.releaseData);
}
preview.textContent = buildGeminiPromptTextWithTemplate(tpl, inputJson);
}
preview.style.display = 'block';
if (icon) icon.textContent = '▼';
geminiPromptToggle.innerHTML = `<span id="gemini-prompt-toggle-icon">▼</span> Hide Gemini Prompt`;
} else {
preview.style.display = 'none';
geminiPromptToggle.innerHTML = `<span id="gemini-prompt-toggle-icon">▶</span> Preview Gemini Prompt`;
}
});
}
});
// Beatport token status UI
function updateBeatportStatusUI(tokenJson) {
const dot = document.getElementById('beatport-status-dot');
const label = document.getElementById('beatport-status-label');
const info = document.getElementById('beatport-token-info');
if (!dot) return;
if (tokenJson) {
dot.style.background = '#4CAF50';
dot.title = 'Token saved';
try {
const parsed = JSON.parse(tokenJson);
const email = parsed.user && parsed.user.email ? parsed.user.email : '';
const expires = parsed.token && parsed.token.accessTokenExpires
? new Date(parsed.token.accessTokenExpires).toLocaleString()
: '';
label.textContent = email || 'Token saved';
info.textContent = expires ? `Expires: ${expires}` : 'Token saved';
} catch(e) {
label.textContent = 'Token saved';
info.textContent = '';
}
} else {
dot.style.background = '#ccc';
dot.title = 'No token saved';
label.textContent = 'No token';
info.textContent = '';
}
}
document.addEventListener('DOMContentLoaded', function () {
// Load saved Beatport token state on popup open
chrome.storage.local.get(['beatportToken'], (result) => {
updateBeatportStatusUI(result.beatportToken || null);
});
document.getElementById('settings-toggle-beatport').addEventListener('click', () => {
const content = document.getElementById('settings-content-beatport');
content.style.display = content.style.display === 'none' ? 'block' : 'none';
});
document.getElementById('beatport-fetch-token').addEventListener('click', () => {
const statusEl = document.getElementById('beatport-fetch-status');
const btn = document.getElementById('beatport-fetch-token');
btn.disabled = true;
btn.textContent = 'Fetching...';
statusEl.textContent = '';
chrome.runtime.sendMessage({ action: 'getBeatportToken' }, (response) => {
btn.disabled = false;
btn.textContent = 'Fetch Token';
if (response && response.success && response.tokenJson) {
chrome.storage.local.set({
beatportToken: response.tokenJson,
beatportAuth: { data: response.tokenJson, timestamp: Date.now() }
}, () => {
updateBeatportStatusUI(response.tokenJson);
statusEl.style.color = 'green';
statusEl.textContent = 'Token saved.';
});
} else {
const msg = (response && response.message) ? response.message : 'Failed to fetch token.';
statusEl.style.color = '#c0392b';
statusEl.textContent = msg;
updateBeatportStatusUI(null);
}
});
});
document.getElementById('beatport-clear-token').addEventListener('click', () => {
chrome.storage.local.remove(['beatportToken', 'beatportAuth'], () => {
updateBeatportStatusUI(null);
const statusEl = document.getElementById('beatport-fetch-status');
statusEl.style.color = '#666';
statusEl.textContent = 'Token cleared.';
});
});
// Copy JSON button in Beatport settings — copies raw tokenJson to clipboard
document.getElementById('beatport-copy-json').addEventListener('click', () => {
const statusEl = document.getElementById('beatport-fetch-status');
chrome.storage.local.get(['beatportToken'], (result) => {
const tokenJson = result.beatportToken;
if (!tokenJson) {
statusEl.style.color = '#c0392b';
statusEl.textContent = 'No token saved yet.';
return;
}
navigator.clipboard.writeText(tokenJson).then(() => {
statusEl.style.color = 'green';
statusEl.textContent = 'Copied to clipboard!';
setTimeout(() => { statusEl.textContent = ''; }, 2000);
}).catch(() => {
statusEl.style.color = '#c0392b';
statusEl.textContent = 'Clipboard write failed.';
});
});
});
});
// ── RFID tab ─────────────────────────────────────────────────────────────────
async function rfidReadCurrentTag() {
const skuEl = document.getElementById('rfid-tag-sku');
const ridEl = document.getElementById('rfid-tag-releaseid');
const rssiEl = document.getElementById('rfid-tag-rssi');
const antEl = document.getElementById('rfid-tag-antenna');
const scanEl = document.getElementById('rfid-tag-scanning');
if (!skuEl) return;
scanEl.style.display = '';
skuEl.style.color = '#555';
skuEl.textContent = '—';
ridEl.textContent = '—';
if (rssiEl) rssiEl.textContent = '—';
if (antEl) antEl.textContent = '—';
try {
const resp = await fetch('http://127.0.0.1:7790/read-tag', { signal: AbortSignal.timeout(15000) });
const data = await resp.json();
if (data.ok) {
skuEl.style.color = '#1976d2';
skuEl.textContent = data.sku || '—';
ridEl.textContent = data.releaseId != null ? data.releaseId : '—';
if (rssiEl) rssiEl.textContent = data.rssi != null ? `${data.rssi} dBm` : '—';
if (antEl) antEl.textContent = data.antenna != null ? `${data.antenna}` : '—';
} else {
skuEl.style.color = '#888';
skuEl.textContent = 'no tag';
ridEl.textContent = '—';
}
} catch {
skuEl.style.color = '#aaa';
skuEl.textContent = 'daemon offline';
ridEl.textContent = '—';
} finally {
scanEl.style.display = 'none';
}
}
async function rfidCheckConnection() {
const dot = document.getElementById('rfid-status-dot');
const label = document.getElementById('rfid-status-label');
const port = document.getElementById('rfid-status-port');
const stopBtn = document.getElementById('rfid-stop-btn');
const maskBtn = document.getElementById('rfid-clear-mask-btn');
const startCmd = document.getElementById('rfid-start-cmd');
if (!dot) return;
label.textContent = 'Checking…';
dot.style.background = '#ffc107';
try {
const resp = await fetch('http://127.0.0.1:7790/status', { signal: AbortSignal.timeout(2000) });
const data = await resp.json();
if (data.ok) {
dot.style.background = '#4CAF50';
label.textContent = 'H102 connected';
port.textContent = data.port || '';
if (stopBtn) { stopBtn.style.display = 'inline-block'; }
if (maskBtn) { maskBtn.style.display = 'inline-block'; }
if (startCmd) { startCmd.style.display = 'none'; }
} else {
dot.style.background = '#f44336';
label.textContent = 'Reader not found';
port.textContent = data.error || '';
if (stopBtn) { stopBtn.style.display = 'none'; }
if (maskBtn) { maskBtn.style.display = 'none'; }
if (startCmd) { startCmd.style.display = 'none'; }
}
} catch (e) {
dot.style.background = '#ccc';
label.textContent = 'Daemon not running';
port.textContent = '';
if (stopBtn) { stopBtn.style.display = 'none'; }
if (maskBtn) { maskBtn.style.display = 'none'; }
if (startCmd) { startCmd.style.display = 'flex'; }
}
}
function rfidUpdateDisplayedData() {
const skuEl = document.getElementById('rfid-sku-display');
const ridEl = document.getElementById('rfid-releaseid-display');
if (!skuEl) return;
const rawSku = (state.releaseData?.collectionSku) || state.sku || '—';
const sku = rawSku !== '—' ? rawSku.replace(/-A$/i, '') : rawSku;
skuEl.textContent = sku;
ridEl.textContent = state.releaseData?.id || '—';
}
document.addEventListener('DOMContentLoaded', function () {
// Show current data when RFID tab is activated
document.querySelectorAll('.tab-button').forEach(btn => {
btn.addEventListener('click', () => {
if (btn.dataset.tab === 'rfid') {
rfidUpdateDisplayedData();
rfidCheckConnection();
rfidReadCurrentTag();
}
});
});
const checkBtn = document.getElementById('rfid-check-btn');
if (checkBtn) checkBtn.addEventListener('click', rfidCheckConnection);
// Stop daemon
const stopBtn = document.getElementById('rfid-stop-btn');
if (stopBtn) {
stopBtn.addEventListener('click', async () => {
stopBtn.disabled = true;
try {
await fetch('http://127.0.0.1:7790/shutdown', { method: 'POST', signal: AbortSignal.timeout(3000) });
} catch (_) {}
setTimeout(() => { stopBtn.disabled = false; rfidCheckConnection(); }, 600);
});
}
// Copy start command to clipboard
const copyCmdBtn = document.getElementById('rfid-copy-cmd-btn');
if (copyCmdBtn) {
copyCmdBtn.addEventListener('click', () => {
const cmd = document.getElementById('rfid-start-cmd-text')?.textContent || 'node rfid-daemon/index.js -q';
navigator.clipboard.writeText(cmd).then(() => {
const orig = copyCmdBtn.textContent;
copyCmdBtn.textContent = 'Copied!';
setTimeout(() => { copyCmdBtn.textContent = orig; }, 1500);
});
});
}
// Clear mask — both the status-area button and the write-area button
async function doClearMask() {
const statusEl = document.getElementById('rfid-write-status');
try {
const resp = await fetch('http://127.0.0.1:7790/clear-mask', { method: 'POST', signal: AbortSignal.timeout(5000) });
const result = await resp.json();
if (statusEl) {
statusEl.style.color = result.ok ? '#28a745' : '#c0392b';
statusEl.textContent = result.ok ? 'Mask cleared — reader can see all tags again.' : `Clear failed: ${result.error}`;
}
} catch (e) {
if (statusEl) { statusEl.style.color = '#c0392b'; statusEl.textContent = 'Daemon not running.'; }
}
}
const clearMaskBtn2 = document.getElementById('rfid-clear-mask-btn2');
if (clearMaskBtn2) clearMaskBtn2.addEventListener('click', doClearMask);
const recoverBtn = document.getElementById('rfid-recover-btn');
if (recoverBtn) {
recoverBtn.addEventListener('click', async () => {
const statusEl = document.getElementById('rfid-write-status');
recoverBtn.disabled = true;
if (statusEl) { statusEl.style.color = '#555'; statusEl.textContent = 'Recovering reader…'; }
try {
const resp = await fetch('http://127.0.0.1:7790/recover', { method: 'POST', signal: AbortSignal.timeout(15000) });
const result = await resp.json();
if (statusEl) {
statusEl.style.color = result.ok ? '#28a745' : '#c0392b';
statusEl.textContent = result.ok ? 'Reader recovered — try again.' : `Recover failed: ${result.error}`;
}
} catch (e) {
if (statusEl) { statusEl.style.color = '#c0392b'; statusEl.textContent = 'Recover failed — press trigger to wake reader, then try again.'; }
} finally {
recoverBtn.disabled = false;
}
});
}
const recoverModeBtn = document.getElementById('rfid-recover-mode-btn');
if (recoverModeBtn) {
recoverModeBtn.addEventListener('click', async () => {
const statusEl = document.getElementById('rfid-write-status');
recoverModeBtn.disabled = true;
if (statusEl) { statusEl.style.color = '#555'; statusEl.textContent = 'Restoring RFID mode — scanning baud rates, please wait…'; }
try {
const resp = await fetch('http://127.0.0.1:7790/recover-mode', { method: 'POST', signal: AbortSignal.timeout(35000) });
const result = await resp.json();
if (statusEl) {
statusEl.style.color = result.ok ? '#28a745' : '#c0392b';
statusEl.textContent = result.ok ? result.message : `Restore failed: ${result.error}`;
}
} catch (e) {
if (statusEl) { statusEl.style.color = '#c0392b'; statusEl.textContent = 'Restore failed — daemon not running or device not found.'; }
} finally {
recoverModeBtn.disabled = false;
}
});
}
const unlockBtn = document.getElementById('rfid-unlock-btn');
if (unlockBtn) {
unlockBtn.addEventListener('click', async () => {
const statusEl = document.getElementById('rfid-write-status');
unlockBtn.disabled = true;
statusEl.style.color = '#555';
statusEl.textContent = 'Unlocking EPC bank…';
try {
const resp = await fetch('http://127.0.0.1:7790/unlock-tag', { method: 'POST' });
const result = await resp.json();
if (result.ok) {
statusEl.style.color = '#28a745';
statusEl.textContent = 'Unlocked — tag is now rewritable. Try writing again.';
} else {
statusEl.style.color = '#c0392b';
statusEl.textContent = `Unlock failed: ${result.error}`;
}
} catch (e) {
statusEl.style.color = '#c0392b';
statusEl.textContent = 'Daemon not running.';
}
unlockBtn.disabled = false;
});
}
const rescanBtn = document.getElementById('rfid-rescan-btn');
if (rescanBtn) {
rescanBtn.addEventListener('click', () => rfidReadCurrentTag());
}
const writeBtn = document.getElementById('rfid-write-btn');
if (writeBtn) {
writeBtn.addEventListener('click', async () => {
const statusEl = document.getElementById('rfid-write-status');
const rawSku = (state.releaseData?.collectionSku) || state.sku;
const sku = rawSku ? rawSku.replace(/-[AR]$/i, '') : rawSku;
if (!sku) {
statusEl.style.color = '#c0392b';
statusEl.textContent = 'No SKU available — open a Discogs release page first.';
return;
}
writeBtn.disabled = true;
statusEl.style.color = '#555';
statusEl.textContent = 'Writing…';
const cfg = await new Promise(resolve =>
chrome.storage.local.get(['rfidAppendR'], resolve)
);
// Always include release_id — see rationale at rfidQuickBtn handler.
const releaseId = state.releaseData?.id || null;
const result = await writeRfidTag(sku, releaseId);
writeBtn.disabled = false;
if (!result) {
statusEl.style.color = '#c0392b';
statusEl.textContent = 'Daemon not running — start node index.js first.';
} else if (result.ok) {
statusEl.style.color = '#28a745';
statusEl.textContent = `Written: ${sku}${releaseId ? ' + release ' + releaseId : ''}`;
if (cfg.rfidAppendR && state.discogsTabId) {
try {
await chrome.tabs.sendMessage(state.discogsTabId, { action: 'updateDiscogsSku', sku: sku + '-R', boxIndex: state.collectionBoxIndex });
} catch (e) { console.warn('[RFID] -R SKU update failed:', e.message); }
}
} else {
statusEl.style.color = '#c0392b';
statusEl.textContent = `Failed: ${result.error}`;
}
});
}
// Device Info button
const deviceInfoBtn = document.getElementById('rfid-device-info-btn');
if (deviceInfoBtn) {
deviceInfoBtn.addEventListener('click', async () => {
const display = document.getElementById('rfid-device-info-display');
deviceInfoBtn.disabled = true;
display.textContent = 'Fetching…';
try {
const resp = await fetch('http://127.0.0.1:7790/device-info', { signal: AbortSignal.timeout(5000) });
const data = await resp.json();
if (data.ok) {
display.textContent = [
`HW: ${data.hwVer || '—'}`,
`Firm: ${data.firmVer || '—'}`,
`SN: ${data.sn || '—'}`,
`Mod: ${data.moduleName || '—'} v${data.moduleVer || '—'}`,
`Raw: ${data.raw}`,
].join('\n');
} else {
display.textContent = `Error: ${data.error}`;
}
} catch (e) {
display.textContent = `Daemon offline: ${e.message}`;
} finally {
deviceInfoBtn.disabled = false;
}
});
}
// Battery button
const batteryBtn = document.getElementById('rfid-battery-btn');
if (batteryBtn) {
batteryBtn.addEventListener('click', async () => {
const display = document.getElementById('rfid-device-info-display');
batteryBtn.disabled = true;
try {
const resp = await fetch('http://127.0.0.1:7790/battery', { signal: AbortSignal.timeout(5000) });
const data = await resp.json();
display.textContent = data.ok ? `Battery: ${data.battery}%` : `Error: ${data.error}`;
} catch (e) {
display.textContent = `Daemon offline: ${e.message}`;
} finally {
batteryBtn.disabled = false;
}
});
}
// Get Config button — fetches and populates fields
const getConfigBtn = document.getElementById('rfid-get-config-btn');
if (getConfigBtn) {
getConfigBtn.addEventListener('click', async () => {
const display = document.getElementById('rfid-config-display');
const statusEl = document.getElementById('rfid-config-status');
getConfigBtn.disabled = true;
display.textContent = 'Fetching…';
try {
const resp = await fetch('http://127.0.0.1:7790/get-config', { signal: AbortSignal.timeout(5000) });
const d = await resp.json();
if (d.ok) {
display.textContent = [
`WorkMode: ${d.workModeName} Interface: ${d.interfaceName} Baud: ${d.baudRateName}`,
`Protocol: ${d.rfidProName} Freq: ${d.rfidFreqName || '—'}`,
`Ant: 0x${(d.ant||0).toString(16).padStart(2,'0')} Q: ${d.qValue} Session: ${d.session}`,
`FilterTime: ${d.filterTime}s Buzzer: ${d.buzzerTime}×10ms Poll: ${d.pollingInterval}×10ms`,
`RF Power: ${(d.rfidPower / 10).toFixed(1)} dBm`,
].join('\n');
// Populate input fields with current values
const set = (id, val) => { const el = document.getElementById(id); if (el && val != null) el.value = val; };
set('rfid-cfg-qvalue', d.qValue);
set('rfid-cfg-session', d.session);
set('rfid-cfg-filtertime',d.filterTime);
set('rfid-cfg-buzzertime',d.buzzerTime);
set('rfid-cfg-rfpower', (d.rfidPower / 10).toFixed(1));
set('rfid-cfg-polling', d.pollingInterval);
statusEl.textContent = '';
} else {
display.textContent = `Error: ${d.error}`;
}
} catch (e) {
display.textContent = `Daemon offline: ${e.message}`;
} finally {
getConfigBtn.disabled = false;
}
});
}
// Set Config button — sends changed fields
const setConfigBtn = document.getElementById('rfid-set-config-btn');
if (setConfigBtn) {
setConfigBtn.addEventListener('click', async () => {
const statusEl = document.getElementById('rfid-config-status');
setConfigBtn.disabled = true;
statusEl.style.color = '#555';
statusEl.textContent = 'Applying…';
const body = {};
const grab = (id, key) => {
const el = document.getElementById(id);
if (el && el.value !== '') body[key] = parseInt(el.value, 10);
};
grab('rfid-cfg-qvalue', 'qValue');
grab('rfid-cfg-session', 'session');
grab('rfid-cfg-filtertime','filterTime');
grab('rfid-cfg-buzzertime','buzzerTime');
{ const el = document.getElementById('rfid-cfg-rfpower'); if (el && el.value !== '') body.rfidPower = Math.round(parseFloat(el.value) * 10); }
grab('rfid-cfg-polling', 'pollingInterval');
if (Object.keys(body).length === 0) {
statusEl.style.color = '#c0392b';
statusEl.textContent = 'No fields to set.';
setConfigBtn.disabled = false;
return;
}
try {
const resp = await fetch('http://127.0.0.1:7790/set-config', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body), signal: AbortSignal.timeout(5000)
});
const data = await resp.json();
statusEl.style.color = data.ok ? '#28a745' : '#c0392b';
statusEl.textContent = data.ok ? `Applied: ${data.changed.join(', ')}` : `Failed: ${data.error}`;
} catch (e) {
statusEl.style.color = '#c0392b';
statusEl.textContent = `Daemon offline: ${e.message}`;
} finally {
setConfigBtn.disabled = false;
}
});
}
// Inventory Scan button
const scanBtn = document.getElementById('rfid-scan-btn');
if (scanBtn) {
scanBtn.addEventListener('click', async () => {
const resultEl = document.getElementById('rfid-scan-result');
const type = document.getElementById('rfid-inv-type')?.value || 'time';
const param = document.getElementById('rfid-inv-param')?.value || '1';
scanBtn.disabled = true;
resultEl.textContent = 'Scanning…';
try {
const url = `http://127.0.0.1:7790/inventory?type=${type}&param=${param}`;
const resp = await fetch(url, { signal: AbortSignal.timeout(15000) });
const data = await resp.json();
if (data.ok && data.status === 0x00) {
const parts = [`${data.statusText}`];
if (data.rssi != null) parts.push(`RSSI: ${data.rssi} dBm`);
if (data.antenna != null) parts.push(`Ant: ${data.antenna}`);
if (data.epc) parts.push(`EPC: ${data.epc}`);
resultEl.textContent = parts.join(' | ');
} else {
resultEl.textContent = data.statusText || `status 0x${data.status?.toString(16)}`;
}
} catch (e) {
resultEl.textContent = `Daemon offline: ${e.message}`;
} finally {
scanBtn.disabled = false;
}
});
}
});