Torn Junk Seller

Automates pricing with support for fixed prices, API, and RRP.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Advertisement:

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

Advertisement:

// ==UserScript==
// @name         Torn Junk Seller
// @namespace    torn.tools
// @version      2.2.2
// @description  Automates pricing with support for fixed prices, API, and RRP.
// @match        https://www.torn.com/*
// @grant        GM_xmlhttpRequest
// @connect      greasyfork.org
// @run-at       document-idle
// @license      CC BY-ND 4.0
// ==/UserScript==

(function () {
    'use strict';

    const CURRENT_VERSION     = '2.2.2';
    const STORAGE_KEY_MARKET  = 'tm_saved_items';
    const STORAGE_KEY_BAZAAR  = 'tm_saved_items_bazaar';
    const CONFIG_KEY          = 'tm_config';
    const KEEP_QTY_KEY_MARKET = 'tm_keep_qty_market';
    const KEEP_QTY_KEY_BAZAAR = 'tm_keep_qty_bazaar';
    const ITEM_MARGIN_KEY     = 'tm_item_margin';
    const PRICE_CACHE_KEY     = 'tm_price_cache';
    const LOCKED_PRICES_KEY   = 'tm_locked_prices';
    const FIXED_PRICE_KEY     = 'tm_fixed_prices';

    const BULK_THRESHOLD = 200;
    const BULK_DISCOUNT  = 0.10;
    const RRP_FALLBACK_THRESHOLD = 0.20;

    function getInfoEl(row) {
        const itemRow = row.querySelector('[class*="itemRow"]');
        if (!itemRow) return null;
        const children = [...itemRow.children];
        return children.find(el => el.tagName !== 'BUTTON') ?? null;
    }

    function getPriceWrapper(row) {
        const priceInput = row.querySelector('input[aria-label*=" price"]');
        return priceInput?.closest('[class*="priceInputWrapper"]') ?? null;
    }

    function getNameEl(row) {
        return row.querySelector('[class*="name"]') ?? row.querySelector('[class*="title"]');
    }

    /* ===============================
       PAGE DETECTION
    =============================== */

    function isAddListingPage() {
        const hash = window.location.hash;
        return (hash.includes('/addListing') || hash.includes('/add')) && !hash.includes('/viewListing') && !hash.includes('/confirm');
    }

    function isConfirmPage() {
        return window.location.hash.includes('/addListing/confirm');
    }

    function isViewListingPage() {
        return window.location.hash.includes('/viewListing');
    }

    function isBazaarPage() {
        return window.location.pathname.includes('bazaar.php') && window.location.hash.includes('/add');
    }

    function isManageBazaarPage() {
        return window.location.pathname.includes('bazaar.php') && window.location.hash.includes('/manage');
    }

    function isActivePage() {
        return isAddListingPage() || isBazaarPage() || isManageBazaarPage() || isViewListingPage() || isConfirmPage();
    }

    /* ===============================
       CONFIG & STORAGE
    =============================== */

    let _configCache = null;
    function getConfig() {
        if (_configCache) return _configCache;
        const defaults = { margin: 5, rounding: 50, apiKey: '', cacheTtlMin: 5, bulkDetection: true, bulkThreshold: 200 };
        try {
            const cfg = localStorage.getItem(CONFIG_KEY);
            _configCache = cfg ? Object.assign({}, defaults, JSON.parse(cfg)) : defaults;
        } catch { _configCache = defaults; }
        return _configCache;
    }

    function saveConfig(cfg) {
        _configCache = null;
        _playerIdCache = null;
        localStorage.setItem(CONFIG_KEY, JSON.stringify(cfg));
    }

    function getMargin()          { return getConfig().margin ?? 5; }
    function getRounding()        { return getConfig().rounding ?? 50; }
    function getApiKey()          { return getConfig().apiKey ?? ''; }
    function getCacheTtlMs()      { return (getConfig().cacheTtlMin ?? 5) * 60 * 1000; }
    function isBulkDetectionOn()  { return getConfig().bulkDetection !== false; }
    function getBulkThreshold()   { return getConfig().bulkThreshold ?? BULK_THRESHOLD; }

    let _fixedPriceCache = null;
    function getFixedPriceMap() {
        if (_fixedPriceCache) return _fixedPriceCache;
        try { _fixedPriceCache = JSON.parse(localStorage.getItem(FIXED_PRICE_KEY)) || {}; }
        catch { _fixedPriceCache = {}; }
        return _fixedPriceCache;
    }

    function getFixedPrice(id) { return getFixedPriceMap()[id] ?? null; }

    function setFixedPrice(id, value) {
        const map = getFixedPriceMap();
        if (value === null || value === '' || isNaN(value)) delete map[id];
        else map[id] = Number(value);
        _fixedPriceCache = map;
        localStorage.setItem(FIXED_PRICE_KEY, JSON.stringify(map));
    }

    /* ===============================
       PRICE CACHE
    =============================== */

    let _priceCache = null;
    function loadPriceCache() {
        if (_priceCache) return _priceCache;
        try { _priceCache = JSON.parse(localStorage.getItem(PRICE_CACHE_KEY)) || {}; }
        catch { _priceCache = {}; }
        return _priceCache;
    }

    function savePriceCache(cache) {
        _priceCache = cache;
        localStorage.setItem(PRICE_CACHE_KEY, JSON.stringify(cache));
    }

    function getCacheEntry(itemId) {
        const cache = loadPriceCache();
        const entry = cache[itemId];
        if (!entry) return null;
        if (Date.now() - entry.ts > getCacheTtlMs()) return null;
        return entry;
    }

    function setCacheEntry(itemId, entry) {
        const cache = loadPriceCache();
        cache[itemId] = { ...entry, ts: Date.now() };
        const ttl = getCacheTtlMs();
        const now = Date.now();
        for (const id of Object.keys(cache)) {
            if (now - cache[id].ts > ttl) delete cache[id];
        }
        savePriceCache(cache);
    }

    function clearCacheEntry(itemId) {
        const cache = loadPriceCache();
        delete cache[itemId];
        savePriceCache(cache);
    }

    /* ===============================
       LOCKED PRICES
    =============================== */

    let _lockedPricesCache = null;
    function getLockedPrices() {
        if (_lockedPricesCache) return _lockedPricesCache;
        try { _lockedPricesCache = JSON.parse(localStorage.getItem(LOCKED_PRICES_KEY)) || []; }
        catch { _lockedPricesCache = []; }
        return _lockedPricesCache;
    }

    function isPriceLocked(itemId) { return getLockedPrices().includes(itemId); }

    function togglePriceLock(itemId) {
        const locked = getLockedPrices();
        const idx = locked.indexOf(itemId);
        if (idx === -1) locked.push(itemId);
        else locked.splice(idx, 1);
        _lockedPricesCache = locked;
        localStorage.setItem(LOCKED_PRICES_KEY, JSON.stringify(locked));
        return idx === -1;
    }

    /* ===============================
       PLAYER ID
    =============================== */

    let _playerIdCache = null;
    async function getPlayerId() {
        if (_playerIdCache) return _playerIdCache;
        const key = getApiKey();
        if (!key) return null;
        try {
            const resp = await fetch(`https://api.torn.com/v2/user?key=${key}&fields=basic`);
            if (!resp.ok) return null;
            const data = await resp.json();
            if (data.error || !data.id) return null;
            _playerIdCache = data.id;
            return _playerIdCache;
        } catch { return null; }
    }

    /* ===============================
       TORN API
    =============================== */

    async function fetchMarketData(itemId) {
        const cached = getCacheEntry(itemId);
        if (cached) return cached;

        const key = getApiKey();
        if (!key) return null;

        try {
            const url = `https://api.torn.com/v2/market/${itemId}/itemmarket?key=${key}&limit=10`;
            const resp = await fetch(url);
            if (!resp.ok) return null;
            const data = await resp.json();
            if (data.error) return null;

            const allListings = data?.itemmarket?.listings;
            if (!allListings || allListings.length === 0) return null;

            const playerId = await getPlayerId();
            const listings = playerId
                ? allListings.filter(l => l.seller?.id !== playerId)
                : allListings;

            if (listings.length === 0) return null;

            const bulkThreshold = getBulkThreshold();
            const lowest = listings[0].price;
            let bulk = null;
            for (const l of listings) {
                if (l.amount >= bulkThreshold) {
                    if (!bulk || l.price < bulk.price) bulk = { price: l.price, amount: l.amount };
                }
            }

            const entry = { lowest, bulk };
            setCacheEntry(itemId, entry);
            return entry;
        } catch {
            return null;
        }
    }

    /* ===============================
       UPDATE CHECKER (Bypass CSP)
    =============================== */

    function checkVersion() {
        if (!window.location.href.includes('page.php?sid=ItemMarket') || !window.location.hash.includes('/addListing')) {
            return;
        }

        if (typeof GM_xmlhttpRequest === 'undefined') {
            console.error('[Junk Seller] GM_xmlhttpRequest nie jest dostepny.');
            return;
        }

        GM_xmlhttpRequest({
            method: 'GET',
            url: 'https://greasyfork.org/en/scripts/570131-torn-junk-seller.json',
            onload: function (response) {
                if (response.status !== 200) return;
                try {
                    const data = JSON.parse(response.responseText);
                    if (data && data.version && data.version !== CURRENT_VERSION) {
                        createUpdateWidget(data.version);
                    }
                } catch (e) {
                    console.error('[Junk Seller] Blad parsowania JSON z aktualizacja:', e);
                }
            },
            onerror: function (e) {
                console.error('[Junk Seller] Blad polaczenia z GreasyFork:', e);
            }
        });
    }

    function createUpdateWidget(latestVersion) {
        if (document.getElementById('tm_update_notification')) return;
        if (isConfirmPage()) return;

        const btn = document.createElement('a');
        btn.id = 'tm_update_notification';
        btn.href = 'https://greasyfork.org/en/scripts/570131-torn-junk-seller';
        btn.target = '_blank';
        btn.title = `Wersja w repozytorium (${latestVersion}) rozni sie od Twojej aktualnej (${CURRENT_VERSION})! Kliknij, aby zaktualizowac.`;
        btn.classList.add('tm-injected');
        btn.style.cssText = `
            width: 24px; position:fixed;right:0;bottom:calc(20% - 22px);z-index:9999;cursor:pointer;
            background:linear-gradient(90deg, #4CAF50, #2E7D32);
            padding:3px 8px;border-radius:4px 0 0 4px;display:flex;align-items:center;justify-content:center;
            box-shadow: -2px 2px 6px rgba(0,0,0,0.4);text-decoration:none;
        `;
        btn.innerHTML = `<span style="color:white;font-family:Arial,sans-serif;font-size:10px;font-weight:bold;letter-spacing:0.5px;text-align: right;">update</span>`;

        document.body.appendChild(btn);
    }

    /* ===============================
       PRICE LOGIC
    =============================== */

    function parseMoney(text) {
        if (!text) return 0;
        return Number(text.replace('$', '').replace(/,/g, '').trim());
    }

    function roundToNearest(value) {
        const r = getRounding();
        if (!r || r <= 0) return Math.round(value);
        return Math.round(value / r) * r;
    }

    function getItemMarginPct(itemId) {
        return itemId !== undefined ? (getItemMargin(itemId) ?? getMargin()) : getMargin();
    }

    function computePrice(rrp, apiData, itemId) {
        const fixedPrice = itemId ? getFixedPrice(itemId) : null;
        if (fixedPrice !== null && fixedPrice > 0) {
            return {
                price: fixedPrice,
                tooltip: `Fixed Price: $${fmt(fixedPrice)} (Manual Override)`,
            };
        }

        const marginPct = getItemMarginPct(itemId);
        const ts = apiData ? fmtTime(getCacheEntry(itemId)?.ts ?? Date.now()) : null;
        const bulkEnabled = isBulkDetectionOn();
        const bulkThreshold = getBulkThreshold();

        if (!apiData) {
            const price = roundToNearest(rrp * (1 + marginPct / 100));
            const marginAmt = price - rrp;
            return {
                price,
                tooltip: `RRP: $${fmt(rrp)} + $${fmt(marginAmt)} (+${marginPct}%) = $${fmt(price)}`,
            };
        }

        const { lowest, bulk } = apiData;
        let basePrice, usedRrpFallback = false;

        if (bulkEnabled && bulk) {
            basePrice = bulk.price * (1 - BULK_DISCOUNT);
        } else {
            basePrice = lowest;
        }

        if (!basePrice || basePrice < rrp * RRP_FALLBACK_THRESHOLD) {
            basePrice = rrp;
            usedRrpFallback = true;
        }

        const price = roundToNearest(basePrice * (1 + marginPct / 100));
        const marginAmt = price - Math.round(basePrice);

        const source = usedRrpFallback ? 'RRP' : 'API';
        const baseLabel = usedRrpFallback ? fmt(rrp) : fmt(lowest);
        let line = `${source} (${ts}): $${baseLabel} + $${fmt(marginAmt)} (+${marginPct}%) = $${fmt(price)}`;

        if (bulkEnabled && bulk) {
            line += `\n(Bulk seller: ${fmt(bulk.amount)}x $${fmt(bulk.price)} - -${BULK_DISCOUNT * 100}% - base $${fmt(Math.round(basePrice))})`;
        } else if (!bulkEnabled && bulk) {
            line += `\n(Bulk seller detected but ignored - bulk detection disabled)`;
        } else if (usedRrpFallback) {
            line += `\n(API $${fmt(lowest)} < RRP $${fmt(rrp)} = using RRP)`;
        } else if (bulkEnabled && !bulk) {
            line += `\n(No bulk seller found - threshold: ${fmt(bulkThreshold)}+ items)`;
        }

        return { price, tooltip: line };
    }

    function fmt(n) {
        return Number(n).toLocaleString('en-US');
    }

    function fmtTime(ts) {
        return new Date(ts).toTimeString().slice(0, 5);
    }

    /* ===============================
       ITEM MARKET - RRP + apply
    =============================== */

    function getRRP_Market(row) {
        const priceContainer = row.querySelector('[aria-label*="Recommended Retail Price"]');
        if (!priceContainer) return 0;
        const visiblePrice = priceContainer.querySelector('span');
        if (!visiblePrice) return 0;
        return parseMoney(visiblePrice.textContent);
    }

    function applyPriceToRow_Market(row, price, tooltip, priceOnly = false) {
        const expandBtn = row.querySelector('button[aria-expanded="false"] .group-arrow');
        const groupExpandBtn = expandBtn?.closest('button');

        if (groupExpandBtn) {
            groupExpandBtn.click();
            setTimeout(() => applyPriceToRow_Market(row, price, tooltip, priceOnly), 300);
            return;
        }

        const selectCheckbox = row.querySelector('input[id^="itemRow-selectCheckbox"]');
        if (!priceOnly && selectCheckbox && !selectCheckbox.checked) selectCheckbox.click();

        const inputs = row.querySelectorAll('input[data-testid="legacy-money-input"]:not([type="hidden"])');
        if (!inputs.length) return;

        const itemId = getItemId(row);

        inputs.forEach(input => {
            const hidden = input.parentElement.querySelector(
                'input[data-testid="legacy-money-input"][type="hidden"]'
            );

            let value;
            const dataMoney = input.dataset.money;

            if (dataMoney === 'Infinity') {
                value = price;
                if (tooltip) {
                    const wrap = input.closest('[class*="priceInput"]') || input.parentElement;
                    wrap.title = tooltip;
                }
            } else {
                if (priceOnly) return;
                if (selectCheckbox) return;
                const maxQty = Number(dataMoney);
                const keepQty = itemId ? getKeepQty(itemId) : null;
                value = (keepQty !== null && keepQty >= 0) ? Math.max(0, maxQty - keepQty) : maxQty;
            }

            setReactInputValue(input, value);
            if (hidden) hidden.value = value;
        });
    }

    async function setValue_Market(row, priceOnly = false) {
        const itemId = getItemId(row);
        const rrp = getRRP_Market(row);
        const apiData = itemId ? await fetchMarketData(itemId) : null;
        const { price, tooltip } = computePrice(rrp, apiData, itemId);
        applyPriceToRow_Market(row, price, tooltip, priceOnly);
    }

    /* ===============================
       VIEW LISTING - row updates
    =============================== */

    function refreshNameTitle(row) {
        const itemId = getItemId(row);
        if (!itemId) return;
        const cached = getCacheEntry(itemId);
        const nameEl = getNameEl(row);
        if (!nameEl) return;
        if (cached) {
            nameEl.title = `Market lowest: $${fmt(cached.lowest)}${cached.bulk ? ` | Bulk ${fmt(cached.bulk.amount)}x @ $${fmt(cached.bulk.price)}` : ''}`;
        } else {
            nameEl.removeAttribute('title');
        }
    }

    function addButton_ViewListing(row) {
        const info = getInfoEl(row);
        if (!info || info.dataset.tmViewAdded) return;
        info.dataset.tmViewAdded = '1';

        ensureStyles();
        const itemId = getItemId(row);
        refreshNameTitle(row);

        const fixedInput = document.createElement('input');
        fixedInput.type = 'text';
        fixedInput.placeholder = 'Fix $';
        fixedInput.title = 'Fixed price (overrides API/RRP)';
        fixedInput.classList.add('tm-keep-input', 'tm-injected');
        applyInputStyle(fixedInput);
        fixedInput.style.marginLeft = '6px';
        if (itemId) {
            const savedFixed = getFixedPrice(itemId);
            if (savedFixed !== null) fixedInput.value = savedFixed;
        }
        fixedInput.addEventListener('click', e => e.stopPropagation());
        fixedInput.addEventListener('change', () => {
            if (!itemId) return;
            setFixedPrice(itemId, fixedInput.value.trim() || null);
            setValue_Market(row, true);
        });

        const marginInput = document.createElement('input');
        marginInput.type = 'text';
        marginInput.placeholder = '%';
        marginInput.title = 'Custom margin % for this item';
        marginInput.classList.add('tm-keep-input', 'tm-injected');
        applyInputStyle(marginInput);
        marginInput.style.marginLeft = '6px';
        if (itemId) {
            const savedMargin = getItemMargin(itemId);
            if (savedMargin !== null) marginInput.value = savedMargin;
        }
        marginInput.addEventListener('click', e => e.stopPropagation());
        marginInput.addEventListener('change', () => {
            if (!itemId) return;
            setItemMargin(itemId, marginInput.value.trim() || null);
            clearCacheEntry(itemId);
            setValue_Market(row, true).then(() => refreshNameTitle(row));
        });

        const netLabel = document.createElement('span');
        netLabel.classList.add('tm-row-net', 'tm-injected');
        netLabel.style.cssText = 'margin-left:6px;font-size:10px;color:#8BC34A;line-height:1.2;text-align:right;vertical-align:middle;white-space:nowrap;width:55px;display:flex;align-items:center;justify-content:flex-end;';

        function refreshNetLabel() {
            const priceHidden = row.querySelector('input[data-money="Infinity"][type="hidden"]');
            const qtyHidden   = row.querySelector('input[type="hidden"]:not([data-money="Infinity"])');
            if (!priceHidden || !qtyHidden) { netLabel.textContent = ''; return; }
            const price   = Number(priceHidden.value);
            const qty     = Number(qtyHidden.value);
            if (!price || !qty) { netLabel.textContent = ''; return; }
            const isAnon  = !!row.querySelector('[class*="checkboxWrapper"][class*="active"]');
            const feeRate = isAnon ? 0.15 : 0.05;
            const net     = Math.round(price * qty * (1 - feeRate));
            netLabel.innerHTML = `$${fmt(net)}`;
            netLabel.title = fmt(Math.round(price * qty))+"$ - "+(fmt(Math.round((price * qty)*(feeRate))))+"$";
        }

        refreshNetLabel();

        const btnA = document.createElement('button');
        btnA.textContent = 'A';
        btnA.title = 'Refresh price (API)';
        btnA.style.cssText = 'padding:0 4px;cursor:pointer;color:#2196F3;margin-left:3px;';
        btnA.classList.add('tm-injected');
        btnA.addEventListener('click', e => {
            e.stopPropagation();
            if (itemId) clearCacheEntry(itemId);
            setValue_Market(row, true).then(() => { refreshNameTitle(row); refreshNetLabel(); updateViewListingSummary(); });
        });

        const btnL = document.createElement('button');
        btnL.classList.add('tm-injected');
        btnL.style.cssText = 'padding:0 2px;cursor:pointer;margin-left:3px;background:none;border:none;line-height:1;vertical-align:middle;';

        const svgLocked = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#4CAF50" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`;
        const svgUnlocked = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#FFA726" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></svg>`;

        function updateLockBtn() {
            const locked = itemId && isPriceLocked(itemId);
            btnL.innerHTML = locked ? svgLocked : svgUnlocked;
            btnL.title = locked ? 'Unlock price' : 'Lock price';
            marginInput.disabled = !!locked;
            fixedInput.disabled = !!locked;
            marginInput.style.opacity = locked ? '0.4' : '1';
            fixedInput.style.opacity = locked ? '0.4' : '1';
        }

        updateLockBtn();
        btnL.addEventListener('click', e => {
            e.stopPropagation();
            if (!itemId) return;
            togglePriceLock(itemId);
            updateLockBtn();
        });

        info.appendChild(fixedInput);
        info.appendChild(marginInput);
        info.appendChild(netLabel);
        info.appendChild(btnA);
        info.appendChild(btnL);
    }

    /* ===============================
       BAZAAR - ADD
    =============================== */

    function getRRP_Bazaar(row) {
        const infoWrap = row.querySelector('.info-wrap[title="Market value"]');
        if (!infoWrap) return 0;
        const text = infoWrap.textContent.split('|')[0].trim();
        return parseMoney(text);
    }

    function setValue_Bazaar(row) {
        const itemId = getItemId_Bazaar(row);
        const rrp = getRRP_Bazaar(row);

        const priceInput = row.querySelector('.price input.input-money:not([type="hidden"])');
        const priceHidden = row.querySelector('.price input.input-money[type="hidden"]');

        if (priceInput) {
            const { price } = computePrice(rrp, null, itemId);
            setReactInputValue(priceInput, price);
            if (priceHidden) priceHidden.value = price;
        }

        const checkbox = row.querySelector('input[type="checkbox"][name="amount"]');
        const amountInput = row.querySelector('input[type="text"][name="amount"]');

        if (checkbox) {
            if (!checkbox.checked) checkbox.click();
        } else if (amountInput) {
            const maxQty = Number(row.querySelector('.item-amount.qty')?.textContent?.trim() ?? 0);
            const keepQty = itemId ? getKeepQty(itemId) : null;
            const qty = (keepQty !== null && keepQty >= 0) ? Math.max(0, maxQty - keepQty) : maxQty;
            setReactInputValue(amountInput, qty);
        }
    }

    /* ===============================
       BAZAAR - MANAGE
    =============================== */

    function getRRP_ManageBazaar(row) {
        const rrpEl = row.querySelector('[class*="rrp"]');
        if (!rrpEl) return 0;
        return parseMoney(rrpEl.textContent);
    }

    function getItemId_ManageBazaar(row) {
        const img = row.querySelector('img[src*="/images/items/"]');
        if (!img) return null;
        const match = (img.src || img.getAttribute('srcset') || '').match(/items\/(\d+)\//);
        return match ? match[1] : null;
    }

    async function setValue_ManageBazaar(row, priceOnly = false) {
        const itemId = getItemId_ManageBazaar(row);
        const rrp = getRRP_ManageBazaar(row);
        const apiData = itemId ? await fetchMarketData(itemId) : null;
        const { price, tooltip } = computePrice(rrp, apiData, itemId);
        applyPriceToRow_ManageBazaar(row, price, tooltip);
    }

    function applyPriceToRow_ManageBazaar(row, price, tooltip) {
        const priceInput = row.querySelector('[class*="price"] input[data-testid="legacy-money-input"]:not([type="hidden"])');
        const priceHidden = row.querySelector('[class*="price"] input[data-testid="legacy-money-input"][type="hidden"]');
        if (!priceInput) return;

        if (tooltip) {
            const wrap = priceInput.closest('[class*="price"]') || priceInput.parentElement;
            wrap.title = tooltip;
        }

        setReactInputValue(priceInput, price);
        if (priceHidden) priceHidden.value = price;
    }

    function addControls_ManageBazaar(row) {
        const itemEl = row.querySelector('[class*="item"]');
        if (!itemEl || itemEl.dataset.tmManageAdded) return;
        itemEl.dataset.tmManageAdded = '1';

        ensureStyles();

        const itemId = getItemId_ManageBazaar(row);

        const wrap = document.createElement('div');
        wrap.classList.add('tm-injected');
        wrap.style.cssText = 'display:flex;align-items:center;gap:3px;margin-left:4px;';

        const fixedInput = document.createElement('input');
        fixedInput.type = 'text';
        fixedInput.placeholder = 'Fix $';
        fixedInput.title = 'Fixed price (overrides API/RRP)';
        fixedInput.classList.add('tm-keep-input', 'tm-injected');
        applyInputStyle(fixedInput);
        if (itemId) {
            const saved = getFixedPrice(itemId);
            if (saved !== null) fixedInput.value = saved;
        }
        fixedInput.addEventListener('click', e => e.stopPropagation());
        fixedInput.addEventListener('change', () => {
            if (!itemId) return;
            setFixedPrice(itemId, fixedInput.value.trim() || null);
            setValue_ManageBazaar(row);
        });

        const marginInput = document.createElement('input');
        marginInput.type = 'text';
        marginInput.placeholder = '%';
        marginInput.title = 'Custom margin % for this item';
        marginInput.classList.add('tm-keep-input', 'tm-injected');
        applyInputStyle(marginInput);
        if (itemId) {
            const saved = getItemMargin(itemId);
            if (saved !== null) marginInput.value = saved;
        }
        marginInput.addEventListener('click', e => e.stopPropagation());
        marginInput.addEventListener('change', () => {
            if (!itemId) return;
            setItemMargin(itemId, marginInput.value.trim() || null);
            clearCacheEntry(itemId);
            setValue_ManageBazaar(row).then(() => refreshNameTitle_ManageBazaar(row));
        });

        const btnA = document.createElement('button');
        btnA.textContent = 'A';
        btnA.title = 'Auto-price (API/RRP)';
        btnA.style.cssText = 'padding:0 4px;cursor:pointer;color:#2196F3;';
        btnA.classList.add('tm-injected');
        btnA.addEventListener('click', e => {
            e.stopPropagation();
            if (itemId) clearCacheEntry(itemId);
            setValue_ManageBazaar(row).then(() => refreshNameTitle_ManageBazaar(row));
        });

        const btnL = document.createElement('button');
        btnL.classList.add('tm-injected');
        btnL.style.cssText = 'padding:0 2px;cursor:pointer;background:none;border:none;line-height:1;vertical-align:middle;';

        const svgLocked = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#4CAF50" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`;
        const svgUnlocked = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#FFA726" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/></svg>`;

        function updateLockBtn() {
            const locked = itemId && isPriceLocked(itemId);
            btnL.innerHTML = locked ? svgLocked : svgUnlocked;
            btnL.title = locked ? 'Unlock price' : 'Lock price';
            marginInput.disabled = !!locked;
            fixedInput.disabled = !!locked;
            marginInput.style.opacity = locked ? '0.4' : '1';
            fixedInput.style.opacity = locked ? '0.4' : '1';
        }

        updateLockBtn();
        btnL.addEventListener('click', e => {
            e.stopPropagation();
            if (!itemId) return;
            togglePriceLock(itemId);
            updateLockBtn();
        });

        wrap.appendChild(fixedInput);
        wrap.appendChild(marginInput);
        wrap.appendChild(btnA);
        wrap.appendChild(btnL);

        // Insert wrap into the price cell so controls sit next to the input
        const priceCell = row.querySelector('[class*="price"]');
        if (priceCell) {
            priceCell.style.display = 'flex';
            priceCell.style.alignItems = 'center';
            priceCell.appendChild(wrap);
        } else {
            itemEl.appendChild(wrap);
        }

        // Auto-apply if item is saved
        if (itemId && isItemSaved(itemId) && !isPriceLocked(itemId) && !row.dataset.tmAutoApplied) {
            row.dataset.tmAutoApplied = '1';
            setValue_ManageBazaar(row);
        }
    }

    function refreshNameTitle_ManageBazaar(row) {
        const itemId = getItemId_ManageBazaar(row);
        if (!itemId) return;
        const cached = getCacheEntry(itemId);
        const descEl = row.querySelector('[class*="desc"]');
        if (!descEl) return;
        if (cached) {
            descEl.title = `Market lowest: $${fmt(cached.lowest)}${cached.bulk ? ` | Bulk ${fmt(cached.bulk.amount)}x @ $${fmt(cached.bulk.price)}` : ''}`;
        } else {
            descEl.removeAttribute('title');
        }
    }

    function scanRows_ManageBazaar() {
        document.querySelectorAll('[data-testid="sortable-item"]').forEach(row => {
            addControls_ManageBazaar(row);
        });
    }

    function createRefreshAllWidget_ManageBazaar() {
        if (document.getElementById('tm_refresh_all_btn')) return;

        const btn = document.createElement('div');
        btn.id = 'tm_refresh_all_btn';
        btn.title = 'Refresh all bazaar prices (API)';
        btn.classList.add('tm-injected');
        btn.style.cssText = `
            position:fixed;right:0;bottom:calc(20% + 45px);z-index:9999;cursor:pointer;
            background:linear-gradient(90deg, rgb(2,36,74), rgb(3,111,201));
            padding:8px;border-radius:6px;
        `;
        btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="white" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
          <polyline points="23 4 23 10 17 10"/>
          <polyline points="1 20 1 14 7 14"/>
          <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
        </svg>`;

        btn.addEventListener('click', async () => {
            btn.style.opacity = '0.5';
            btn.style.pointerEvents = 'none';

            const rows = [...document.querySelectorAll('[data-testid="sortable-item"]')];
            for (const row of rows) {
                const itemId = getItemId_ManageBazaar(row);
                if (itemId && isPriceLocked(itemId)) continue;
                if (itemId) clearCacheEntry(itemId);
                await setValue_ManageBazaar(row);
                refreshNameTitle_ManageBazaar(row);
            }

            btn.style.opacity = '1';
            btn.style.pointerEvents = '';
        });

        document.body.appendChild(btn);
    }

    /* ===============================
       setReactInputValue
    =============================== */

    function setReactInputValue(input, value) {
        const nativeSetter = Object.getOwnPropertyDescriptor(
            window.HTMLInputElement.prototype, 'value'
        ).set;
        nativeSetter.call(input, value);
        input.dispatchEvent(new Event('input', { bubbles: true }));
        input.dispatchEvent(new Event('change', { bubbles: true }));
    }

    /* ===============================
       LOCAL STORAGE HELPERS
    =============================== */

    function getStorageKey() { return (isBazaarPage() || isManageBazaarPage()) ? STORAGE_KEY_BAZAAR : STORAGE_KEY_MARKET; }

    let _savedItemsCache = {};
    function getSavedItems(key) {
        const k = key ?? getStorageKey();
        if (_savedItemsCache[k]) return _savedItemsCache[k];
        try { _savedItemsCache[k] = JSON.parse(localStorage.getItem(k)) || []; }
        catch { _savedItemsCache[k] = []; }
        return _savedItemsCache[k];
    }
    function saveItems(items, key) {
        const k = key ?? getStorageKey();
        _savedItemsCache[k] = items;
        localStorage.setItem(k, JSON.stringify(items));
    }
    function addItem(id) {
        const items = getSavedItems();
        if (!items.includes(id)) { items.push(id); saveItems(items); }
    }
    function removeItem(id) { saveItems(getSavedItems().filter(i => i !== id)); }
    function isItemSaved(id) { return getSavedItems().includes(id); }

    function getKeepQtyKey() { return (isBazaarPage() || isManageBazaarPage()) ? KEEP_QTY_KEY_BAZAAR : KEEP_QTY_KEY_MARKET; }
    let _keepQtyCache = {};
    function getKeepQtyMap() {
        const k = getKeepQtyKey();
        if (_keepQtyCache[k]) return _keepQtyCache[k];
        try { _keepQtyCache[k] = JSON.parse(localStorage.getItem(k)) || {}; }
        catch { _keepQtyCache[k] = {}; }
        return _keepQtyCache[k];
    }
    function getKeepQty(id) { return getKeepQtyMap()[id] ?? null; }
    function setKeepQty(id, value) {
        const map = getKeepQtyMap();
        if (value === null || value === '' || isNaN(value)) delete map[id];
        else map[id] = Number(value);
        _keepQtyCache[getKeepQtyKey()] = map;
        localStorage.setItem(getKeepQtyKey(), JSON.stringify(map));
    }
    function removeKeepQty(id) { setKeepQty(id, null); }

    let _itemMarginCache = null;
    function getItemMarginMap() {
        if (_itemMarginCache) return _itemMarginCache;
        try { _itemMarginCache = JSON.parse(localStorage.getItem(ITEM_MARGIN_KEY)) || {}; }
        catch { _itemMarginCache = {}; }
        return _itemMarginCache;
    }
    function getItemMargin(id) { return getItemMarginMap()[id] ?? null; }
    function setItemMargin(id, value) {
        const map = getItemMarginMap();
        if (value === null || value === '' || isNaN(value)) delete map[id];
        else map[id] = Number(value);
        _itemMarginCache = map;
        localStorage.setItem(ITEM_MARGIN_KEY, JSON.stringify(map));
    }
    function removeItemMargin(id) { setItemMargin(id, null); }

    function bustAllCaches() {
        _configCache = null; _fixedPriceCache = null; _priceCache = null;
        _lockedPricesCache = null; _savedItemsCache = {}; _keepQtyCache = {};
        _itemMarginCache = null; _playerIdCache = null;
    }

    /* ===============================
       IMPORT / EXPORT
    =============================== */

    function exportAll() {
        return JSON.stringify({
            market:      getSavedItems(STORAGE_KEY_MARKET),
            bazaar:      getSavedItems(STORAGE_KEY_BAZAAR),
            keepMarket:  JSON.parse(localStorage.getItem(KEEP_QTY_KEY_MARKET) || '{}'),
            keepBazaar:  JSON.parse(localStorage.getItem(KEEP_QTY_KEY_BAZAAR) || '{}'),
            itemMargin:  JSON.parse(localStorage.getItem(ITEM_MARGIN_KEY) || '{}'),
            fixedPrices: JSON.parse(localStorage.getItem(FIXED_PRICE_KEY) || '{}'),
        }, null, 2);
    }

    function importAll(json) {
        try {
            const d = JSON.parse(json);
            if (d.market)      saveItems(d.market, STORAGE_KEY_MARKET);
            if (d.bazaar)      saveItems(d.bazaar, STORAGE_KEY_BAZAAR);
            if (d.keepMarket)  localStorage.setItem(KEEP_QTY_KEY_MARKET, JSON.stringify(d.keepMarket));
            if (d.keepBazaar)  localStorage.setItem(KEEP_QTY_KEY_BAZAAR, JSON.stringify(d.keepBazaar));
            if (d.itemMargin)  localStorage.setItem(ITEM_MARGIN_KEY, JSON.stringify(d.itemMargin));
            if (d.fixedPrices) localStorage.setItem(FIXED_PRICE_KEY, JSON.stringify(d.fixedPrices));
            alert('Import OK');
            bustAllCaches();
        } catch {
            alert('Invalid JSON');
        }
    }

    /* ===============================
       ITEM HELPERS
    =============================== */

    function getItemId(row) {
        const img = row.querySelector('img.torn-item');
        if (!img) return null;
        const match = img.src.match(/items\/(\d+)\//);
        return match ? match[1] : null;
    }

    function getItemId_Bazaar(row) {
        const img = row.querySelector("img[src*='/images/items/']");
        if (!img) return null;
        const match = img.src.match(/items\/(\d+)\//);
        return match ? match[1] : null;
    }

    /* ===============================
       STYLES
    =============================== */

    function applyInputStyle(el) {
        Object.assign(el.style, {
            width: '42px', padding: '5px 3px',
            background: 'linear-gradient(0deg, rgb(17,17,17) 0%, rgb(0,0,0) 100%)',
            color: 'rgb(255,255,255)', textAlign: 'center',
            border: '0.8px solid rgb(68,68,68)', borderRadius: '5px',
            fontSize: '12px', fontFamily: 'Arial, Helvetica, sans-serif',
            lineHeight: '14px', verticalAlign: 'middle',
            appearance: 'none', MozAppearance: 'textfield', boxSizing: 'border-box',
        });
    }

    function ensureStyles() {
        if (document.getElementById('tm-keep-input-style')) return;
        const s = document.createElement('style');
        s.id = 'tm-keep-input-style';
        s.textContent = [
            'input.tm-keep-input::-webkit-outer-spin-button,',
            'input.tm-keep-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }',
            'input.tm-keep-input::placeholder { text-align: center; font-size: 10px; }',
            '.d .items-wrap .category-wrap ul.items-cont > li { position: relative; }',
            /* Manage Bazaar - widen price column in header and rows */
            '[data-testid="cells-header"] [class*="price"] { width: 256px !important; }',
            '[data-testid="sortable-item"] [class*="price"] { width: 260px !important; }',
            /* Manage Bazaar - fix price input padding */
            '.d .input-money-group input.input-money:not([type="file"]):not([type="submit"]):not([type="button"]) { padding: 5px 6px !important; }',
        ].join('\n');
        document.head.appendChild(s);
    }

    /* ===============================
       CONTROLS BUILDER
    =============================== */

    function buildControls(itemId, onAuto, onSave, onDelete, bazaar) {
        ensureStyles();

        const col = document.createElement('div');
        col.classList.add('tm-injected');
        col.style.cssText = 'display:flex;align-items:center;gap:4px;margin-left:6px;';

        if (bazaar) {
            Object.assign(col.style, {
                position: 'absolute', left: 'calc(100% - 6px)',
                background: '#333', height: '33px',
                border: '1px solid black', borderBottom: 'none',
                top: '-1px', paddingLeft: '3px',
            });
        }

        const fixedInput = document.createElement('input');
        fixedInput.type = 'text';
        fixedInput.placeholder = 'Fix $';
        fixedInput.title = 'Fixed price (overrides API/RRP)';
        fixedInput.classList.add('tm-keep-input', 'tm-injected');
        applyInputStyle(fixedInput);
        fixedInput.style.display = 'none';

        const keepInput = document.createElement('input');
        keepInput.type = 'text';
        keepInput.placeholder = 'Keep';
        keepInput.title = 'Amount to keep after sale';
        keepInput.classList.add('tm-keep-input', 'tm-injected');
        applyInputStyle(keepInput);
        keepInput.style.display = 'none';

        const marginInput = document.createElement('input');
        marginInput.type = 'text';
        marginInput.placeholder = '%';
        marginInput.title = 'Custom margin % for this item';
        marginInput.classList.add('tm-keep-input', 'tm-injected');
        applyInputStyle(marginInput);
        marginInput.style.display = 'none';

        const btnA = document.createElement('button');
        btnA.textContent = 'A';
        btnA.title = 'Auto-price';
        btnA.style.cssText = 'padding:0 4px;cursor:pointer;color:#2196F3;';
        btnA.classList.add('tm-injected');
        btnA.addEventListener('click', e => { e.stopPropagation(); onAuto(); });

        const btnToggle = document.createElement('button');
        btnToggle.style.cssText = 'padding:0 4px;cursor:pointer;margin-right:5px;';
        btnToggle.classList.add('tm-injected');

        if (itemId) {
            const savedKeep = getKeepQty(itemId);
            if (savedKeep !== null) keepInput.value = savedKeep;
            const savedMargin = getItemMargin(itemId);
            if (savedMargin !== null) marginInput.value = savedMargin;
            const savedFixed = getFixedPrice(itemId);
            if (savedFixed !== null) fixedInput.value = savedFixed;
        }

        fixedInput.addEventListener('click', e => e.stopPropagation());
        fixedInput.addEventListener('change', () => {
            if (!itemId) return;
            setFixedPrice(itemId, fixedInput.value.trim() || null);
            onAuto();
        });

        keepInput.addEventListener('click', e => e.stopPropagation());
        keepInput.addEventListener('change', () => {
            if (!itemId) return;
            setKeepQty(itemId, keepInput.value.trim() || null);
            onAuto();
        });

        marginInput.addEventListener('click', e => e.stopPropagation());
        marginInput.addEventListener('change', () => {
            if (!itemId) return;
            setItemMargin(itemId, marginInput.value.trim() || null);
            onAuto();
        });

        function updateToggle() {
            if (!itemId) return;
            if (isItemSaved(itemId)) {
                btnToggle.textContent = 'D'; btnToggle.title = 'Remove';
                btnToggle.style.color = '#F44336';
                fixedInput.style.display = '';
                keepInput.style.display = '';
                marginInput.style.display = '';
            } else {
                btnToggle.textContent = 'S'; btnToggle.title = 'Save';
                btnToggle.style.color = '#4CAF50';
                fixedInput.style.display = 'none';
                keepInput.style.display = 'none';
                marginInput.style.display = 'none';
            }
        }

        btnToggle.addEventListener('click', e => {
            e.stopPropagation();
            if (!itemId) return;
            if (isItemSaved(itemId)) {
                removeItem(itemId); removeKeepQty(itemId); removeItemMargin(itemId);
                setFixedPrice(itemId, null);
                onDelete();
            } else {
                onSave(); addItem(itemId);
            }
            updateToggle();
        });

        updateToggle();

        col.appendChild(fixedInput);
        col.appendChild(keepInput);
        col.appendChild(marginInput);
        col.appendChild(btnA);
        col.appendChild(btnToggle);

        return col;
    }

    /* ===============================
       CONFIRM PAGE - net price summary
    =============================== */

    function getConfirmFeeRate() {
        const activeAnon = document.querySelector('[class*="anonymousPreview"][class*="active"]');
        const summaryText = document.querySelector('[class*="text"]')?.textContent ?? '';
        const hasAnonFee = activeAnon !== null || summaryText.includes('anonymity fee');
        return hasAnonFee ? 0.15 : 0.05;
    }

    function parseQty(titleEl) {
        const titleWrapper = titleEl?.closest('[class*="title"]');
        if (!titleWrapper) return 1;
        const spans = titleWrapper.querySelectorAll('span');
        for (const s of spans) {
            const m = s.textContent.trim().match(/^x(\d+)$/);
            if (m) return parseInt(m[1], 10);
        }
        return 1;
    }

    function injectConfirmNetPrices() {
        if (document.querySelector('.tm-confirm-injected')) return;

        const feeRate = getConfirmFeeRate();

        const rows = document.querySelectorAll('[class*="itemRowWrapper"]');
        let grandTotal = 0;

        rows.forEach(row => {
            if (row.querySelector('.tm-confirm-injected')) return;

            const priceEl   = row.querySelector('[class*="price___"]');
            const nameEl    = row.querySelector('[class*="name"]');
            const previewEl = row.querySelector('[class*="pricePreview"]');
            if (!priceEl) return;

            if (previewEl) {
                previewEl.style.flexDirection = 'column';
                previewEl.style.placeSelf     = 'flex-start';
                previewEl.style.alignItems    = 'flex-end';
            }

            const unitPrice = parseMoney(priceEl.textContent);
            const qty = parseQty(nameEl);
            const gross = unitPrice * qty;
            const net   = Math.round(gross * (1 - feeRate));
            grandTotal += net;

            const netEl = document.createElement('div');
            netEl.className = 'tm-confirm-injected tm-injected';
            netEl.textContent = `$${fmt(net)}`;
            netEl.title = `${qty} x $${fmt(unitPrice)} - ${feeRate * 100}% fee = $${fmt(net)}`;
            netEl.style.cssText = `
                font-size: 11px;
                color: #8BC34A;
                margin-top: 1px;
                text-align: right;
            `;

            priceEl.parentElement.appendChild(netEl);
        });

        injectGrandTotal(grandTotal, feeRate);
    }

    function injectGrandTotal(grandTotal, feeRate) {
        const textBlock = document.querySelector('[class*="text___"]');
        if (!textBlock || textBlock.querySelector('.tm-grand-total')) return;

        const totalEl = document.createElement('p');
        totalEl.className = 'tm-grand-total tm-injected';
        totalEl.style.cssText = `
            margin-top: 6px;
            font-size: 12px;
            color: #8BC34A;
            border-top: 1px solid #333;
            padding-top: 6px;
        `;
        totalEl.innerHTML = `Net to receive: <b style="color:#8BC34A;">$${fmt(grandTotal)}</b> <span style="color:#777;font-size:11px;">(after ${feeRate * 100}% fee)</span>`;
        textBlock.appendChild(totalEl);
    }

    function scanRows_Confirm() {
        injectConfirmNetPrices();
    }

    /* ===============================
       INIT & SCAN
    =============================== */

    function addButton_Market(row) {
        const info = getInfoEl(row);
        if (!info || info.dataset.tmButtonAdded) return;
        info.dataset.tmButtonAdded = '1';

        const priceWrapper = getPriceWrapper(row);
        if (priceWrapper) {
            priceWrapper.dataset.tmOrigWidth = priceWrapper.style.width || '';
            priceWrapper.style.width = '';
        }

        const itemId = getItemId(row);
        const col = buildControls(
            itemId,
            () => setValue_Market(row),
            () => setValue_Market(row),
            () => {},
            false
        );
        info.appendChild(col);
    }

    function addButton_Bazaar(row) {
        const actionsWrap = row.querySelector('.actions-main-wrap');
        if (!actionsWrap || actionsWrap.dataset.tmButtonAdded) return;
        actionsWrap.dataset.tmButtonAdded = '1';

        const itemId = getItemId_Bazaar(row);
        const col = buildControls(
            itemId,
            () => setValue_Bazaar(row),
            () => setValue_Bazaar(row),
            () => {},
            true
        );
        actionsWrap.appendChild(col);
    }

    function autoApplyForSaved_Market(row) {
        const itemId = getItemId(row);
        if (!itemId || !isItemSaved(itemId) || row.dataset.tmAutoApplied) return;
        row.dataset.tmAutoApplied = '1';
        setValue_Market(row);
    }

    function autoApplyForSaved_Bazaar(row) {
        const itemId = getItemId_Bazaar(row);
        if (!itemId || !isItemSaved(itemId) || row.dataset.tmAutoApplied) return;
        row.dataset.tmAutoApplied = '1';
        setValue_Bazaar(row);
    }

    function scanRows() {
        if (isManageBazaarPage())         scanRows_ManageBazaar();
        else if (isBazaarPage())          scanRows_Bazaar();
        else if (isViewListingPage())     scanRows_ViewListing();
        else if (isConfirmPage())         scanRows_Confirm();
        else                              scanRows_Market();
    }

    function cleanup() {
        document.querySelectorAll('.tm-injected').forEach(el => el.remove());
        document.querySelectorAll('[data-tm-button-added]').forEach(el => { delete el.dataset.tmButtonAdded; });
        document.querySelectorAll('[data-tm-view-added]').forEach(el => { delete el.dataset.tmViewAdded; });
        document.querySelectorAll('[data-tm-manage-added]').forEach(el => { delete el.dataset.tmManageAdded; });
        document.querySelectorAll('[class*="priceInputWrapper"][data-tm-orig-width]').forEach(wrapper => {
            wrapper.style.width = wrapper.dataset.tmOrigWidth;
            delete wrapper.dataset.tmOrigWidth;
        });
        document.querySelectorAll('[data-tm-auto-applied]').forEach(el => { delete el.dataset.tmAutoApplied; });
        document.getElementById('tm-keep-input-style')?.remove();
        document.getElementById('tm_settings')?.remove();
        document.getElementById('tm_settings_panel')?.remove();
        document.getElementById('tm_refresh_all_btn')?.remove();
        document.getElementById('tm_update_notification')?.remove();
    }

    /* ===============================
       REFRESH ALL WIDGET (viewListing)
    =============================== */

    function createRefreshAllWidget() {
        if (document.getElementById('tm_refresh_all_btn')) return;

        const btn = document.createElement('div');
        btn.id = 'tm_refresh_all_btn';
        btn.title = 'Refresh all listing prices (API)';
        btn.style.cssText = `
            position:fixed;right:0;bottom:calc(20% + 45px);z-index:9999;cursor:pointer;
            background:linear-gradient(90deg, rgb(2,36,74), rgb(3,111,201));
            padding:8px;border-radius:6px;
        `;
        btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="white" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
          <polyline points="23 4 23 10 17 10"/>
          <polyline points="1 20 1 14 7 14"/>
          <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
        </svg>`;

        btn.addEventListener('click', async () => {
            btn.style.opacity = '0.5';
            btn.style.pointerEvents = 'none';

            const rows = getViewListingRows();
            rows.forEach(row => {
                const itemId = getItemId(row);
                if (itemId && !isPriceLocked(itemId)) clearCacheEntry(itemId);
            });
            for (const row of rows) {
                const itemId = getItemId(row);
                if (itemId && isPriceLocked(itemId)) continue;
                await setValue_Market(row, true);
                refreshNameTitle(row);
            }

            btn.style.opacity = '1';
            btn.style.pointerEvents = '';
            updateViewListingSummary();
        });

        document.body.appendChild(btn);
    }

    function getViewListingRows() {
        const rows = [];
        document.querySelectorAll('button[aria-label^="View"], button[aria-label^="Expand"]')
            .forEach(button => {
                const row = button.closest('div');
                if (!row) return;
                const wrapper = row.parentElement;
                if (wrapper) rows.push(wrapper);
            });
        return rows;
    }

    function createWidget() {
        const btn = document.createElement('div');
        btn.id = 'tm_settings';
        btn.title = 'Junk Seller Settings';
        btn.style.cssText = `
            position:fixed;right:0;bottom:20%;z-index:9999;cursor:pointer;
            background:linear-gradient(90deg,#4a0202,#c90319);
            padding:8px;border-radius:6px;
        `;
        btn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<g fill="white"><g transform="scale(1.5)" style="transform-origin:12px center;">
<path d="M1443,19.674a8.407,8.407,0,0,0-.659-1.593,2.576,2.576,0,0,1-2.582-.84,2.492,2.492,0,0,1-.65-2.582,8.4,8.4,0,0,0-1.592-.659,2.828,2.828,0,0,1-5.034,0,8.4,8.4,0,0,0-1.592.659,2.492,2.492,0,0,1-.65,2.582,2.578,2.578,0,0,1-2.582.841,8.371,8.371,0,0,0-.659,1.592A2.722,2.722,0,0,1,1428.539,22,3.015,3.015,0,0,1,1427,24.517a8.4,8.4,0,0,0,.659,1.592,2.595,2.595,0,0,1,3.232,3.232,8.338,8.338,0,0,0,1.592.659,2.828,2.828,0,0,1,5.034,0,8.4,8.4,0,0,0,1.592-.659,2.492,2.492,0,0,1-.65-2.582,2.576,2.576,0,0,1,2.582-.84,8.407,8.407,0,0,0,.659-1.593A2.722,2.722,0,0,1,1441.461,22,2.722,2.722,0,0,1,1443,19.674Zm-8,5.805A3.479,3.479,0,1,1,1438.479,22,3.48,3.48,0,0,1,1435,25.479Z" transform="translate(-1423 -10)"/>
</g></g></svg>`;

        const panel = document.createElement('div');
        panel.id = 'tm_settings_panel';
        const cfg = getConfig();
        panel.style.cssText = `
            position:fixed;right:50px;bottom:20%;background:#222;color:white;
            padding:0;display:none;z-index:9999;font-size:12px;border-radius:6px;
            min-width:240px;font-family:Arial,sans-serif;border:1px solid #444;
        `;

        panel.innerHTML = `
<div style="display:flex;border-bottom:1px solid #444;">
  <button class="tm-tab active" data-tab="pricing" style="${tabBtnStyle(true)}">Pricing</button>
  <button class="tm-tab" data-tab="io" style="${tabBtnStyle(false)}">Import/Export</button>
  <button class="tm-tab" data-tab="api" style="${tabBtnStyle(false)}">API</button>
</div>

<div class="tm-tab-content" data-content="pricing" style="padding:10px;display:block;">
  <div style="margin-bottom:8px;">
    <label>Margin %</label><br>
    <input id="tm_margin" type="text" value="${cfg.margin}"
      style="width:80px;padding:3px;margin-top:3px;background:#333;color:white;border:1px solid #555;border-radius:3px;">
  </div>
  <div style="margin-bottom:8px;">
    <label>Rounding</label><br>
    <input id="tm_rounding" type="text" value="${cfg.rounding}"
      style="width:80px;padding:3px;margin-top:3px;background:#333;color:white;border:1px solid #555;border-radius:3px;">
  </div>
  <div style="margin-bottom:8px;">
    <label>API cache (minutes)</label><br>
    <input id="tm_cache_ttl" type="text" value="${cfg.cacheTtlMin}"
      style="width:80px;padding:3px;margin-top:3px;background:#333;color:white;border:1px solid #555;border-radius:3px;">
  </div>
  <div style="margin-bottom:8px;display:flex;align-items:center;gap:6px;">
    <input id="tm_bulk_detection" type="checkbox" ${cfg.bulkDetection !== false ? 'checked' : ''}
      style="cursor:pointer;width:14px;height:14px;accent-color:#2196F3;">
    <label for="tm_bulk_detection" style="cursor:pointer;">
      Bulk seller detection
      <span style="display:block;font-size:10px;color:#aaa;margin-top:1px;">
        Apply -10% discount when a seller has many items
      </span>
    </label>
  </div>
  <div style="margin-bottom:12px;">
    <label>Bulk seller threshold (items)</label><br>
    <input id="tm_bulk_threshold" type="text" value="${cfg.bulkThreshold ?? 200}"
      style="width:80px;padding:3px;margin-top:3px;background:#333;color:white;border:1px solid #555;border-radius:3px;">
    <span style="display:block;font-size:10px;color:#aaa;margin-top:3px;">
      Min. quantity listed to count as bulk seller
    </span>
  </div>
  <button id="tm_save_cfg" style="${saveBtnStyle()}">Save</button>
</div>

<div class="tm-tab-content" data-content="io" style="padding:10px;display:none;">
  <div style="margin-bottom:6px;color:#aaa;font-size:11px;">
    Exports and imports item lists and per-item Keep &amp; Margin settings.
  </div>
  <textarea id="tm_io_textarea"
    style="width:100%;height:80px;resize:vertical;box-sizing:border-box;
           background:#111;color:white;border:1px solid #555;border-radius:3px;
           font-size:11px;padding:4px;"></textarea>
  <div style="display:flex;gap:6px;margin-top:6px;">
    <button id="tm_export_all" style="${saveBtnStyle()}">Export</button>
    <button id="tm_import_all" style="${saveBtnStyle()}">Import</button>
  </div>
</div>

<div class="tm-tab-content" data-content="api" style="padding:10px;display:none;">
  <div style="margin-bottom:6px;font-size:11px;color:#aaa;line-height:1.5;">
    Requires a <b style="color:white;">Public Only</b> key.<br>
    Get it from: <a href="https://www.torn.com/preferences.php#tab=api"
      target="_blank" style="color:#2196F3;">Settings &rarr; API Keys</a><br>
    Without a key, prices fall back to RRP from the table.
  </div>
  <div style="display:flex;align-items:center;gap:4px;margin-bottom:12px;">
    <input id="tm_api_key" type="password" value="${cfg.apiKey ?? ''}"
      placeholder="Public Only key"
      style="flex:1;padding:3px;background:#333;color:white;border:1px solid #555;border-radius:3px;">
    <button id="tm_toggle_api_key" title="Show/hide key"
      style="padding:2px 6px;background:#333;color:white;border:1px solid #555;border-radius:3px;cursor:pointer;">&#128065;</button>
  </div>
  <button id="tm_save_api" style="${saveBtnStyle()}">Save</button>
</div>
`;

        panel.querySelectorAll('.tm-tab').forEach(tab => {
            tab.addEventListener('click', () => {
                panel.querySelectorAll('.tm-tab').forEach(t => {
                    t.style.background = '#333'; t.style.borderBottom = '1px solid #444';
                });
                panel.querySelectorAll('.tm-tab-content').forEach(c => c.style.display = 'none');
                tab.style.background = '#222'; tab.style.borderBottom = '1px solid #222';
                panel.querySelector(`.tm-tab-content[data-content="${tab.dataset.tab}"]`).style.display = 'block';
            });
        });

        btn.onclick = () => {
            panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
        };

        document.body.appendChild(btn);
        document.body.appendChild(panel);

        panel.querySelector('#tm_save_cfg').onclick = () => {
            const c = getConfig();
            c.margin         = parseFloat(panel.querySelector('#tm_margin').value) || 0;
            c.rounding       = parseInt(panel.querySelector('#tm_rounding').value) || 1;
            c.cacheTtlMin    = parseInt(panel.querySelector('#tm_cache_ttl').value) || 5;
            c.bulkDetection  = panel.querySelector('#tm_bulk_detection').checked;
            c.bulkThreshold  = parseInt(panel.querySelector('#tm_bulk_threshold').value) || 200;
            saveConfig(c);
            alert('Saved');
        };

        panel.querySelector('#tm_save_api').onclick = () => {
            const c = getConfig();
            c.apiKey = panel.querySelector('#tm_api_key').value.trim();
            saveConfig(c);
            alert('Saved');
        };

        panel.querySelector('#tm_toggle_api_key').onclick = () => {
            const inp = panel.querySelector('#tm_api_key');
            inp.type = inp.type === 'password' ? 'text' : 'password';
        };

        panel.querySelector('#tm_export_all').onclick = () => {
            panel.querySelector('#tm_io_textarea').value = exportAll();
        };
        panel.querySelector('#tm_import_all').onclick = () => {
            importAll(panel.querySelector('#tm_io_textarea').value);
        };
    }

    function tabBtnStyle(active) {
        return `flex:1;padding:6px 4px;cursor:pointer;font-size:11px;border:none;border-radius:0;` +
               `background:${active ? '#222' : '#333'};color:white;` +
               `border-bottom:${active ? '1px solid #222' : '1px solid #444'};`;
    }

    function saveBtnStyle() {
        return 'padding:3px 8px;cursor:pointer;color:white;background:#444;border:1px solid #666;border-radius:3px;';
    }

    /* ===============================
       SCAN
    =============================== */

    function scanRows_Market() {
        document.querySelectorAll('button[aria-label^="View"], button[aria-label^="Expand"]')
            .forEach(button => {
                const row = button.closest('div');
                if (!row) return;
                const wrapper = row.parentElement;
                if (!wrapper) return;
                addButton_Market(wrapper);
                autoApplyForSaved_Market(wrapper);
            });
    }

    function scanRows_ViewListing() {
        document.querySelectorAll('button[aria-label^="View"], button[aria-label^="Expand"]')
            .forEach(button => {
                const row = button.closest('div');
                if (!row) return;
                const wrapper = row.parentElement;
                if (!wrapper) return;
                addButton_ViewListing(wrapper);
            });
    }

    function updateViewListingSummary() {
        const selectAllEl = document.querySelector('[class*="selectAll"]');
        if (!selectAllEl) return;

        let totalNet   = 0;
        let totalGross = 0;
        let itemCount  = 0;

        document.querySelectorAll('[class*="itemRowWrapper"]').forEach(row => {
            const priceHidden = row.querySelector('input[data-money="Infinity"][type="hidden"]');
            const qtyHidden   = row.querySelector('input[type="hidden"]:not([data-money="Infinity"])');
            if (!priceHidden || !qtyHidden) return;

            const price = Number(priceHidden.value);
            const qty   = Number(qtyHidden.value);
            if (!price || !qty) return;

            const isAnon  = !!row.querySelector('[class*="checkboxWrapper"][class*="active"]');
            const feeRate = isAnon ? 0.15 : 0.05;
            const gross   = price * qty;
            const net     = Math.round(gross * (1 - feeRate));

            totalGross += gross;
            totalNet   += net;
            itemCount  += qty;
        });

        let summaryEl = document.getElementById('tm_view_summary');
        if (!summaryEl) {
            summaryEl = document.createElement('div');
            summaryEl.id = 'tm_view_summary';
            summaryEl.classList.add('tm-injected');
            summaryEl.style.cssText = `
                padding: 6px 10px;
                font-size: 12px;
                font-family: Arial, Helvetica, sans-serif;
                color: #aaa;
                border-top: 1px solid #333;
                margin-top: 4px;
                line-height: 1.7;
            `;
            selectAllEl.insertAdjacentElement('afterend', summaryEl);
        }

        const fees = totalGross - totalNet;
        const tooltip = `Listed: ${fmt(itemCount)} | Gross: $${fmt(totalGross)} | Fees: -$${fmt(fees)} | Net: $${fmt(totalNet)}`;
        summaryEl.title = tooltip;
        summaryEl.innerHTML = `
            <span style="color:#777;">Listed:</span>
            <b style="color:white;">${fmt(itemCount)}</b>
            <span style="color:#777;margin-left:8px;">Gross:</span>
            <b style="color:white;">$${fmt(totalGross)}</b>
            <span style="color:#777;margin-left:8px;">Fees:</span>
            <b style="color:#F44336;">-$${fmt(fees)}</b>
            <span style="color:#777;margin-left:8px;">Net:</span>
            <b style="color:#8BC34A;">$${fmt(totalNet)}</b>
        `;
    }

    function scanRows_Bazaar() {
        document.querySelectorAll('li[data-group="child"]').forEach(row => {
            addButton_Bazaar(row);
            autoApplyForSaved_Bazaar(row);
        });
    }

    let initialized = false;
    let mutationObserver = null;
    let _scanDebounceTimer = null;

    function initScript() {
        if (!isActivePage() || initialized) return;
        initialized = true;

        mutationObserver = new MutationObserver(() => {
            clearTimeout(_scanDebounceTimer);
            _scanDebounceTimer = setTimeout(() => {
                scanRows();
                if (isViewListingPage()) {
                    createRefreshAllWidget();
                    if (!document.getElementById('tm_view_summary')) updateViewListingSummary();
                }
                if (isManageBazaarPage()) {
                    createRefreshAllWidget_ManageBazaar();
                }
            }, 80);
        });
        mutationObserver.observe(document.body, { childList: true, subtree: true });

        scanRows();
        if (!isConfirmPage()) createWidget();
        if (isViewListingPage()) {
            createRefreshAllWidget();
            updateViewListingSummary();
        }
        if (isManageBazaarPage()) {
            createRefreshAllWidget_ManageBazaar();
        }

        checkVersion();
    }

    function deactivate() {
        clearTimeout(_scanDebounceTimer);
        mutationObserver?.disconnect();
        mutationObserver = null;
        cleanup();
        initialized = false;
    }

    initScript();
    window.addEventListener('hashchange', () => {
        deactivate();
        if (isActivePage()) initScript();
    });

})();