← back to Govarbitrage
extension/popup.js
199 lines
/**
* GovArbitrage Capture — popup controller.
*
* Responsibilities:
* - load/save the API base URL to chrome.storage.local
* - on "Capture", inject content.js's extractor into the active tab and show
* the returned object
* - on "Send", POST the captured object to `${apiBase}/api/import/extension`
*
* All async paths are guarded so a failure surfaces as readable status text
* rather than an unhandled rejection.
*/
const DEFAULT_API_BASE = 'http://localhost:3000';
const STORAGE_KEY = 'govarbitrage_api_base';
const IMPORT_PATH = '/api/import/extension';
const els = {
apiBase: document.getElementById('apiBase'),
captureBtn: document.getElementById('captureBtn'),
sendBtn: document.getElementById('sendBtn'),
preview: document.getElementById('preview'),
summary: document.getElementById('summary'),
status: document.getElementById('status'),
};
/** Last successfully captured listing object (or null). */
let captured = null;
// ------------------------------------------------------------------ status
function setStatus(msg, kind = 'info') {
els.status.textContent = msg || '';
els.status.className = kind;
}
// ------------------------------------------------------------- storage load
function loadApiBase() {
try {
chrome.storage.local.get([STORAGE_KEY], (res) => {
const val = (res && res[STORAGE_KEY]) || DEFAULT_API_BASE;
els.apiBase.value = val;
});
} catch (_e) {
els.apiBase.value = DEFAULT_API_BASE;
}
}
function saveApiBase() {
const val = normalizeBase(els.apiBase.value);
try {
chrome.storage.local.set({ [STORAGE_KEY]: val });
} catch (_e) {
/* non-fatal */
}
}
function normalizeBase(v) {
let base = (v || '').trim() || DEFAULT_API_BASE;
// strip trailing slashes
base = base.replace(/\/+$/, '');
return base;
}
// ------------------------------------------------------------- active tab
function getActiveTab() {
return new Promise((resolve, reject) => {
try {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
if (chrome.runtime.lastError) return reject(new Error(chrome.runtime.lastError.message));
const tab = tabs && tabs[0];
if (!tab) return reject(new Error('No active tab found.'));
resolve(tab);
});
} catch (e) {
reject(e);
}
});
}
// ------------------------------------------------------------------ capture
async function onCapture() {
setStatus('Capturing…', 'info');
els.sendBtn.disabled = true;
captured = null;
let tab;
try {
tab = await getActiveTab();
} catch (e) {
setStatus('Could not read the active tab: ' + e.message, 'err');
return;
}
if (!tab.id || !/^https?:/i.test(tab.url || '')) {
setStatus('This page cannot be captured (only http/https pages).', 'err');
return;
}
try {
const results = await chrome.scripting.executeScript({
target: { tabId: tab.id },
// The extractor is defined in content.js; injecting the file makes
// extractGovArbitrageListing available in the page, then we call it.
files: ['content.js'],
});
// File injection returns undefined results, so run the call in a second
// pass now that the function is defined in the page.
const callResults = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () =>
typeof extractGovArbitrageListing === 'function'
? extractGovArbitrageListing()
: null,
});
const data = callResults && callResults[0] && callResults[0].result;
if (!data) {
setStatus('Extraction returned no data on this page.', 'err');
els.preview.textContent = '';
els.summary.textContent = '';
return;
}
captured = data;
renderPreview(data);
els.sendBtn.disabled = false;
setStatus('Captured. Review, then send.', 'ok');
} catch (e) {
setStatus('Capture failed: ' + (e && e.message ? e.message : String(e)), 'err');
els.preview.textContent = '';
els.summary.textContent = '';
}
}
function renderPreview(data) {
try {
els.preview.textContent = JSON.stringify(data, null, 2);
} catch (_e) {
els.preview.textContent = String(data);
}
const imgCount = Array.isArray(data.imageUrls) ? data.imageUrls.length : 0;
els.summary.textContent = `${data.source} · #${data.sourceAuctionId || '—'} · ${imgCount} img`;
}
// --------------------------------------------------------------------- send
async function onSend() {
if (!captured) {
setStatus('Nothing captured yet.', 'err');
return;
}
const base = normalizeBase(els.apiBase.value);
saveApiBase();
const endpoint = base + IMPORT_PATH;
setStatus('Sending to ' + endpoint + ' …', 'info');
els.sendBtn.disabled = true;
try {
const resp = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(captured),
});
let payload = null;
const raw = await resp.text();
try {
payload = raw ? JSON.parse(raw) : null;
} catch (_e) {
payload = { raw };
}
if (resp.ok) {
const id = payload && (payload.id || (payload.listing && payload.listing.id));
setStatus('Sent ✓' + (id ? ' — listing ' + id : ' (HTTP ' + resp.status + ')'), 'ok');
} else {
const detail =
(payload && (payload.error || payload.message)) || 'HTTP ' + resp.status;
setStatus('Server rejected it: ' + detail, 'err');
els.sendBtn.disabled = false;
}
} catch (e) {
setStatus(
'Network error — is the app running at ' + base + '? (' + (e && e.message ? e.message : e) + ')',
'err'
);
els.sendBtn.disabled = false;
}
}
// -------------------------------------------------------------------- wiring
els.captureBtn.addEventListener('click', onCapture);
els.sendBtn.addEventListener('click', onSend);
els.apiBase.addEventListener('change', saveApiBase);
els.apiBase.addEventListener('blur', saveApiBase);
loadApiBase();