Torn Trading Assistant

Track purchases, sales, trades, museum sets & profits for Torn City. Syncs with Google Sheets.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Torn Trading Assistant
// @namespace    https://oatshead.dev/torn-trading-assistant
// @version      1.3.5
// @description  Track purchases, sales, trades, museum sets & profits for Torn City. Syncs with Google Sheets.
// @author       Oatshead
// @match        https://www.torn.com/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @connect      trading-assistant.oatshead.workers.dev
// @connect      api.torn.com
// @connect      script.google.com
// @connect      script.googleusercontent.com
// @icon         https://www.torn.com/images/items/186/large.png
// @run-at       document-idle
// ==/UserScript==

/*
╔══════════════════════════════════════════════════════════════════╗
║               TORN TRADING ASSISTANT — ToS                       ║
╠══════════════════════════════════════════════════════════════════╣
║ Data Storage     │ Only locally (GM storage in browser/app)      ║
║ Data Sharing     │ Only to YOUR Google Sheet (via YOUR web app)  ║
║ Purpose of Use   │ Personal profit/loss tracking & museum sets   ║
║ Key Storage      │ Stored locally / Not shared                   ║
║ Key Access Level │ Limited Access (user → log, money, travel)    ║
╠══════════════════════════════════════════════════════════════════╣
║ This script ONLY uses the Torn API and monitors pages you are   ║
║ currently viewing. It makes NO automated non-API requests to    ║
║ Torn. All data is stored locally in your browser/app.           ║
║ API calls are triggered by YOU navigating pages.                ║
╚══════════════════════════════════════════════════════════════════╝
*/

(function () {
    "use strict";

    // ========================================================================
    // CONFIGURATION & CONSTANTS
    // ========================================================================

    const WORKER_URL = "https://trading-assistant.oatshead.workers.dev";
    const SHARED_SECRET = "k7Hs9Qp2mX4wR8vL1nB6jT3yC0fA5eD";
    const VERSION = "1.0.0";

    // Log type IDs we care about
    const LOG_TYPES = {
        ITEM_MARKET_BUY: 1112,
        ITEM_MARKET_SELL: 1113,
        BAZAAR_BUY: 1225,
        BAZAAR_SELL: 1226,
        ABROAD_BUY: 4201,
        TRADE_ITEMS_OUT: 4445,
        TRADE_ITEMS_IN: 4446,
        TRADE_COMPLETED: 4430,
        TRADE_ACCEPTED: 4431,
        TRADE_MONEY_OUT: 4440,
        TRADE_MONEY_IN: 4441,
        MUSEUM_EXCHANGE: 7000,
        POINTS_BUY: 5010,
        POINTS_SELL: 5011,
        SHOP_BUY: 4200,
        CITY_ITEM_FIND: 7011,
    };

    // All known plushies and flowers
    const MUSEUM_ITEM_IDS = new Set([
        186, 187, 215, 258, 618, 261, 266, 268, 269, 273, 274, 384, 281, 260, 617,
        263, 264, 267, 271, 272, 277, 276, 385, 282,
    ]);

    // Complete item name→id map (loaded from storage or built from API)
    let ITEM_MAP = {};

    // ========================================================================
    // STORAGE HELPERS
    // ========================================================================

    function getSetting(key, defaultVal) {
        const v = GM_getValue("tta_" + key);
        return v !== undefined ? v : defaultVal;
    }

    function setSetting(key, val) {
        GM_setValue("tta_" + key, val);
    }

    function getConfig() {
        return {
            licenseKey: getSetting("licenseKey", ""),
            licenseValid: getSetting("licenseValid", false),
            tornApiKey: getSetting("tornApiKey", ""),
            sheetWebAppUrl: getSetting("sheetWebAppUrl", ""),
            sheetSecret: getSetting("sheetSecret", ""),
            whitelist: JSON.parse(getSetting("whitelist", "[]")),
            lastLogCheck: getSetting("lastLogCheck", 0),
            processedLogs: JSON.parse(getSetting("processedLogs", "{}")),
            panelX: getSetting("panelX", 10),
            panelY: getSetting("panelY", 10),
            panelMinimized: getSetting("panelMinimized", true),
            enabled: getSetting("enabled", true),
            pollInterval: getSetting("pollInterval", 30), // seconds
        };
    }

    function saveConfig(cfg) {
        setSetting("licenseKey", cfg.licenseKey);
        setSetting("licenseValid", cfg.licenseValid);
        setSetting("tornApiKey", cfg.tornApiKey);
        setSetting("sheetWebAppUrl", cfg.sheetWebAppUrl);
        setSetting("sheetSecret", cfg.sheetSecret);
        setSetting("whitelist", JSON.stringify(cfg.whitelist));
        setSetting("lastLogCheck", cfg.lastLogCheck);
        setSetting("processedLogs", JSON.stringify(cfg.processedLogs));
        setSetting("panelX", cfg.panelX);
        setSetting("panelY", cfg.panelY);
        setSetting("panelMinimized", cfg.panelMinimized);
        setSetting("enabled", cfg.enabled);
        setSetting("pollInterval", cfg.pollInterval);
    }

    function markLogProcessed(logId) {
        const cfg = getConfig();
        cfg.processedLogs[logId] = Date.now();
        // Keep only last 5000 entries to prevent storage bloat
        const keys = Object.keys(cfg.processedLogs);
        if (keys.length > 5000) {
            const sorted = keys.sort(
                (a, b) => cfg.processedLogs[a] - cfg.processedLogs[b],
            );
            for (let i = 0; i < keys.length - 5000; i++) {
                delete cfg.processedLogs[sorted[i]];
            }
        }
        setSetting("processedLogs", JSON.stringify(cfg.processedLogs));
    }

    function isLogProcessed(logId) {
        const cfg = getConfig();
        return !!cfg.processedLogs[logId];
    }

    // ========================================================================
    // GM_xmlhttpRequest WRAPPER
    // ========================================================================

    function fetchGM(url, opts = {}) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: opts.method || "GET",
                url: url,
                headers: opts.headers || { "Content-Type": "application/json" },
                data: opts.body || undefined,
                timeout: opts.timeout || 15000,
                onload: function (resp) {
                    try {
                        resolve({
                            status: resp.status,
                            data: JSON.parse(resp.responseText),
                        });
                    } catch (e) {
                        resolve({ status: resp.status, data: resp.responseText });
                    }
                },
                onerror: function (err) {
                    reject(err);
                },
                ontimeout: function () {
                    reject(new Error("Request timed out"));
                },
            });
        });
    }

    // ========================================================================
    // DEBUG — Run from Status tab to diagnose sync issues
    // ========================================================================

    async function debugLogFetch() {
        const cfg = getConfig();
        if (!cfg.tornApiKey) {
            alert("No API key set!");
            return;
        }

        const results = [];

        // ── Test 1: Raw log fetch (no filtering) ──
        results.push("=== TEST 1: Raw log (v1, no filter) ===");
        try {
            const url1 = `https://api.torn.com/user/?key=${cfg.tornApiKey}&selections=log`;
            const resp1 = await fetchGM(url1);
            if (resp1.data && resp1.data.log) {
                const entries = Object.entries(resp1.data.log);
                results.push(`Returned ${entries.length} log entries`);

                // Show all log types found
                const typeMap = {};
                entries.forEach(([id, entry]) => {
                    const t = entry.log;
                    if (!typeMap[t])
                        typeMap[t] = { title: entry.title, count: 0, sample: null };
                    typeMap[t].count++;
                    if (!typeMap[t].sample)
                        typeMap[t].sample = { id, data: entry.data, params: entry.params };
                });

                for (const [type, info] of Object.entries(typeMap)) {
                    results.push(`  Type ${type} (${info.title}): ${info.count}x`);
                    results.push(`    Sample data: ${JSON.stringify(info.sample.data)}`);
                }

                // Check for item market buy specifically
                const marketBuys = entries.filter(([id, e]) => e.log === 1112);
                results.push(`\nItem Market Buy (1112) entries: ${marketBuys.length}`);
                if (marketBuys.length > 0) {
                    results.push(`  Full entry: ${JSON.stringify(marketBuys[0][1])}`);
                }

                // Check for ALL purchase/sale related types
                const relevantTypes = [
                    1112, 1113, 1110, 1111, 1225, 1226, 4200, 4201, 4445, 4446, 4430,
                    7000, 5010, 5011,
                ];
                const relevant = entries.filter(([id, e]) =>
                    relevantTypes.includes(e.log),
                );
                results.push(`\nAll relevant entries: ${relevant.length}`);
                relevant.forEach(([id, entry]) => {
                    results.push(
                        `  [${id}] Type ${entry.log} (${entry.title}): ${JSON.stringify(entry.data)}`,
                    );
                });
            } else if (resp1.data && resp1.data.error) {
                results.push(`API Error: ${JSON.stringify(resp1.data.error)}`);
            } else {
                results.push(
                    `Unexpected response: ${JSON.stringify(resp1.data).substring(0, 500)}`,
                );
            }
        } catch (e) {
            results.push(`Fetch error: ${e.message}`);
        }

        // ── Test 2: Try v2 log endpoint ──
        results.push("\n=== TEST 2: v2 log endpoint ===");
        try {
            const url2 = `https://api.torn.com/v2/user/log?key=${cfg.tornApiKey}`;
            const resp2 = await fetchGM(url2);
            if (resp2.data) {
                results.push(`v2 response keys: ${Object.keys(resp2.data).join(", ")}`);
                results.push(
                    `v2 preview: ${JSON.stringify(resp2.data).substring(0, 1000)}`,
                );
            }
        } catch (e) {
            results.push(`v2 error: ${e.message}`);
        }

        // ── Test 3: Try v2 log with category filter ──
        results.push("\n=== TEST 3: v2 log with cat filter ===");
        try {
            const url3 = `https://api.torn.com/v2/user/log?key=${cfg.tornApiKey}&cat=trades`;
            const resp3 = await fetchGM(url3);
            if (resp3.data) {
                results.push(
                    `v2 trades: ${JSON.stringify(resp3.data).substring(0, 1000)}`,
                );
            }
        } catch (e) {
            results.push(`v2 trades error: ${e.message}`);
        }

        // ── Test 4: Check whitelist status ──
        results.push("\n=== TEST 4: Whitelist check ===");
        results.push(`Whitelist IDs: ${JSON.stringify(cfg.whitelist)}`);
        results.push(
            `Sheep Plushie (186) whitelisted: ${cfg.whitelist.includes(186)}`,
        );
        results.push(
            `Sheep Plushie (186) in museum set: ${MUSEUM_ITEM_IDS.has(186)}`,
        );

        // ── Test 5: Check processed logs ──
        results.push("\n=== TEST 5: Processed log IDs ===");
        const processedCount = Object.keys(cfg.processedLogs).length;
        results.push(`${processedCount} log IDs already processed`);

        // ── Test 6: Check sheet connection ──
        results.push("\n=== TEST 6: Sheet connection ===");
        results.push(`Sheet URL set: ${!!cfg.sheetWebAppUrl}`);
        results.push(`Sheet secret set: ${!!cfg.sheetSecret}`);

        // ── Show results ──
        showDebugDialog(results.join("\n"));
        console.log("[TTA DEBUG]\n" + results.join("\n"));
    }

    function showDebugDialog(text) {
        const existing = document.getElementById("tta-debug-dialog");
        if (existing) existing.remove();

        const dialog = document.createElement("div");
        dialog.id = "tta-debug-dialog";
        dialog.style.cssText = `
      position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
      z-index:9999999;background:#1a1a2e;border:2px solid #ff9800;
      border-radius:12px;padding:20px;color:#e0e0e0;
      max-height:80vh;overflow-y:auto;width:600px;
      box-shadow:0 8px 32px rgba(0,0,0,0.6);font-family:monospace;font-size:11px;
    `;
        dialog.innerHTML = `
      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
        <h3 style="margin:0;color:#ff9800;">🔍 Debug Log</h3>
        <div>
          <button class="tta-btn tta-btn-primary tta-btn-sm" id="tta-debug-copy">📋 Copy</button>
          <button class="tta-btn tta-btn-danger tta-btn-sm" id="tta-debug-close">✕ Close</button>
        </div>
      </div>
      <pre style="white-space:pre-wrap;word-break:break-all;background:#0f0f1a;
        padding:12px;border-radius:6px;max-height:60vh;overflow-y:auto;
        border:1px solid #333;">${text}</pre>
    `;

        document.body.appendChild(dialog);

        document
            .getElementById("tta-debug-close")
            .addEventListener("click", () => dialog.remove());
        document.getElementById("tta-debug-copy").addEventListener("click", () => {
            navigator.clipboard.writeText(text).then(() => {
                document.getElementById("tta-debug-copy").textContent = "✅ Copied!";
            });
        });
    }

    // ========================================================================
    // LICENSE VERIFICATION
    // ========================================================================

    async function verifyLicense(key) {
        try {
            const resp = await fetchGM(WORKER_URL + "/api/verify", {
                method: "POST",
                body: JSON.stringify({ licenseKey: key, sharedSecret: SHARED_SECRET }),
            });
            return resp.data && resp.data.valid === true;
        } catch (e) {
            console.error("[TTA] License verification failed:", e);
            return false;
        }
    }

    // ========================================================================
    // TORN API CALLS (all compliant — user-triggered, uses API only)
    // ========================================================================

    async function fetchTornApi(endpoint, params = "") {
        const cfg = getConfig();
        if (!cfg.tornApiKey) return null;
        const url = `https://api.torn.com/${endpoint}?key=${cfg.tornApiKey}${params}`;
        try {
            const resp = await fetchGM(url);
            if (resp.data && resp.data.error) {
                console.warn("[TTA] Torn API error:", resp.data.error);
                return null;
            }
            return resp.data;
        } catch (e) {
            console.error("[TTA] Torn API fetch failed:", e);
            return null;
        }
    }

    // ========================================================================
    // TORN API CALLS (Split to handle 10-ID limit)
    // ========================================================================

    async function fetchUserLog() {
        // We have 12 types, but API only allows 10 per call.
        // Split into two batches.
        const batch1 = [
            LOG_TYPES.ITEM_MARKET_BUY,
            LOG_TYPES.ITEM_MARKET_SELL,
            LOG_TYPES.BAZAAR_BUY,
            LOG_TYPES.BAZAAR_SELL,
            LOG_TYPES.ABROAD_BUY,
            LOG_TYPES.SHOP_BUY,
            LOG_TYPES.TRADE_ITEMS_OUT,
            LOG_TYPES.TRADE_ITEMS_IN,
            LOG_TYPES.TRADE_COMPLETED,
            LOG_TYPES.TRADE_ACCEPTED, // Added just in case
        ];

        const batch2 = [
            LOG_TYPES.MUSEUM_EXCHANGE,
            LOG_TYPES.POINTS_BUY,
            LOG_TYPES.POINTS_SELL,
            // Add any future types here
        ];

        try {
            // Run both requests in parallel
            const [resp1, resp2] = await Promise.all([
                fetchTornApi("user/", "&selections=log&log=" + batch1.join(",")),
                fetchTornApi("user/", "&selections=log&log=" + batch2.join(",")),
            ]);

            // Combine the 'log' objects from both responses
            const combinedLogs = {};

            if (resp1 && resp1.log) {
                Object.assign(combinedLogs, resp1.log);
            }
            if (resp2 && resp2.log) {
                Object.assign(combinedLogs, resp2.log);
            }

            // Return strictly the combined structure expected by pollLogs
            return { log: combinedLogs };
        } catch (e) {
            console.error("[TTA] Error fetching batched logs:", e);
            return null;
        }
    }

    // ========================================================================
    // ITEM MAP BUILDER
    // ========================================================================

    async function buildItemMap() {
        const cached = getSetting("itemMap", null);
        const cacheTime = getSetting("itemMapTime", 0);

        // Cache for 24 hours
        if (cached && Date.now() - cacheTime < 86400000) {
            ITEM_MAP = JSON.parse(cached);
            return;
        }

        const data = await fetchTornApi("torn/", "&selections=items");
        if (!data || !data.items) return;

        ITEM_MAP = {};
        for (const [id, item] of Object.entries(data.items)) {
            ITEM_MAP[id] = {
                name: item.name,
                type: item.type,
                tradeable: item.tradeable,
            };
        }
        setSetting("itemMap", JSON.stringify(ITEM_MAP));
        setSetting("itemMapTime", Date.now());
    }

    function getItemName(id) {
        return ITEM_MAP[id] ? ITEM_MAP[id].name : "Unknown Item #" + id;
    }
    function getItemType(id) {
        return ITEM_MAP[id] ? ITEM_MAP[id].type : "Unknown";
    }

    function reverseItemLookup(nameStr) {
        if (!nameStr) return 0;
        const lower = nameStr.toLowerCase().trim();
        for (const [id, info] of Object.entries(ITEM_MAP)) {
            if (info.name && info.name.toLowerCase() === lower) {
                return Number(id);
            }
        }
        return 0;
    }

    // ========================================================================
    // LOG PROCESSOR — Convert API logs to transactions
    // ========================================================================

    function processLogEntry(logId, entry, whitelist) {
        const whitelistSet = new Set(whitelist.map((id) => Number(id)));
        const logType = entry.log;
        const data = entry.data || {};
        const ts = entry.timestamp;

        // ── Extract item ID from various API data formats ──
        // Item Market & Bazaar use: data.items = [{ id, uid, qty }]
        // Shop Buy & Abroad Buy use: data.item = 269, data.quantity = 29
        // Trades use: data.item or data.items
        let itemId = 0;
        let qty = 1;

        if (data.items && Array.isArray(data.items) && data.items.length > 0) {
            // Item Market Buy/Sell, Bazaar Buy/Sell format
            itemId = data.items[0].id || 0;
            qty = data.items[0].qty || data.quantity || data.amount || 1;
        } else {
            // Shop Buy, Abroad Buy, Trade format
            let rawItem =
                data.item || data.itemid || data.item_id || data.itemID || 0;
            if (typeof rawItem === "number" && rawItem > 0) {
                itemId = rawItem;
            } else if (typeof rawItem === "string") {
                const parsed = Number(rawItem);
                if (!isNaN(parsed) && parsed > 0) {
                    itemId = parsed;
                } else {
                    itemId = reverseItemLookup(rawItem);
                }
            }
            qty = data.quantity || data.amount || 1;
        }

        const costEach = data.cost_each || data.cost || data.price || 0;
        const costTotal = data.cost_total || data.total || costEach * qty;

        // Skip if not whitelisted (unless it's a museum/points/trade event)
        const isMuseumItem = MUSEUM_ITEM_IDS.has(Number(itemId));
        const isWhitelisted = whitelistSet.has(Number(itemId));
        const isPointsOrMuseum = [
            LOG_TYPES.MUSEUM_EXCHANGE,
            LOG_TYPES.POINTS_BUY,
            LOG_TYPES.POINTS_SELL,
        ].includes(logType);
        const isTrade = [
            LOG_TYPES.TRADE_COMPLETED,
            LOG_TYPES.TRADE_ACCEPTED,
            LOG_TYPES.TRADE_ITEMS_IN,
            LOG_TYPES.TRADE_ITEMS_OUT,
        ].includes(logType);

        if (!isWhitelisted && !isMuseumItem && !isPointsOrMuseum && !isTrade) {
            console.log(
                `[TTA] Skipped log ${logId} type=${logType}: itemId=${itemId}, ` +
                `whitelisted=${isWhitelisted}, museumItem=${isMuseumItem}, ` +
                `pointsOrMuseum=${isPointsOrMuseum}, trade=${isTrade}`,
            );
            return null;
        }

        const itemName = getItemName(itemId);
        const itemType = getItemType(itemId);

        let transaction = null;

        switch (logType) {
            // ── Purchases ──
            case LOG_TYPES.ITEM_MARKET_BUY:
                if (!isWhitelisted && !isMuseumItem) return null;
                // Handle multi-item market buys (each items[] entry)
                // For now we handle the first item; multi-item support can be added later
                transaction = {
                    logId,
                    type: "purchase",
                    timestamp: ts,
                    source: "Item Market",
                    sourceDetail: data.seller ? String(data.seller) : "",
                    itemId,
                    itemName,
                    itemType,
                    quantity: qty,
                    unitPrice: costEach,
                    totalPrice: costTotal,
                };
                break;

            case LOG_TYPES.BAZAAR_BUY:
                if (!isWhitelisted && !isMuseumItem) return null;
                transaction = {
                    logId,
                    type: "purchase",
                    timestamp: ts,
                    source: "Bazaar",
                    sourceDetail: data.seller ? String(data.seller) : "",
                    itemId,
                    itemName,
                    itemType,
                    quantity: qty,
                    unitPrice: costEach,
                    totalPrice: costTotal,
                };
                break;

            case LOG_TYPES.ABROAD_BUY:
                if (!isWhitelisted && !isMuseumItem) return null;
                const areas = {
                    1: "Torn",
                    2: "Mexico",
                    3: "Cayman Islands",
                    4: "Canada",
                    5: "Hawaii",
                    6: "United Kingdom",
                    7: "Argentina",
                    8: "Switzerland",
                    9: "Japan",
                    10: "China",
                    11: "United Arab Emirates",
                    12: "South Africa",
                };
                transaction = {
                    logId,
                    type: "abroad_buy",
                    timestamp: ts,
                    source: "Abroad",
                    sourceDetail: areas[data.area] || "Unknown",
                    itemId,
                    itemName,
                    itemType,
                    quantity: qty,
                    unitPrice: costEach,
                    totalPrice: costTotal,
                };
                break;

            case LOG_TYPES.SHOP_BUY:
                if (!isWhitelisted && !isMuseumItem) return null;
                transaction = {
                    logId,
                    type: "purchase",
                    timestamp: ts,
                    source: "City Shop",
                    sourceDetail: "",
                    itemId,
                    itemName,
                    itemType,
                    quantity: qty,
                    unitPrice: costEach,
                    totalPrice: costTotal,
                };
                break;

            // ── Sales ──
            case LOG_TYPES.ITEM_MARKET_SELL:
                if (!isWhitelisted && !isMuseumItem) return null;
                transaction = {
                    logId,
                    type: "sale",
                    timestamp: ts,
                    source: "Item Market",
                    sourceDetail: data.buyer ? String(data.buyer) : "",
                    itemId,
                    itemName,
                    itemType,
                    quantity: qty,
                    unitPrice: costEach,
                    totalPrice: costTotal,
                    fees: data.fee || 0,
                    netAmount: costTotal - (data.fee || 0),
                };
                break;

            case LOG_TYPES.BAZAAR_SELL:
                if (!isWhitelisted && !isMuseumItem) return null;
                transaction = {
                    logId,
                    type: "sale",
                    timestamp: ts,
                    source: "Bazaar",
                    sourceDetail: data.buyer ? String(data.buyer) : "",
                    itemId,
                    itemName,
                    itemType,
                    quantity: qty,
                    unitPrice: costEach,
                    totalPrice: costTotal,
                    fees: data.fee || 0,
                    netAmount: costTotal - (data.fee || 0),
                };
                break;

            // ── Trade items ──
            case LOG_TYPES.TRADE_ITEMS_IN:
                if (!isWhitelisted && !isMuseumItem) return null;
                transaction = {
                    logId,
                    type: "trade_in",
                    timestamp: ts,
                    source: "Trade",
                    sourceDetail: data.sender ? String(data.sender) : "",
                    counterparty: data.sender ? String(data.sender) : "",
                    itemId,
                    itemName,
                    itemType,
                    quantity: qty,
                    unitPrice: 0,
                    totalPrice: 0, // Trade value unknown
                };
                break;

            case LOG_TYPES.TRADE_ITEMS_OUT:
                if (!isWhitelisted && !isMuseumItem) return null;
                transaction = {
                    logId,
                    type: "trade_out",
                    timestamp: ts,
                    source: "Trade",
                    sourceDetail: data.receiver ? String(data.receiver) : "",
                    counterparty: data.receiver ? String(data.receiver) : "",
                    itemId,
                    itemName,
                    itemType,
                    quantity: qty,
                    unitPrice: 0,
                    totalPrice: 0,
                };
                break;

            // ── Museum exchange ──
            case LOG_TYPES.MUSEUM_EXCHANGE:
                transaction = {
                    logId,
                    type: "museum_exchange",
                    timestamp: ts,
                    setType: data.set || "Unknown",
                    quantity: data.quantity || 1,
                    pointsReceived: data.points_received || (data.quantity || 1) * 10,
                };
                break;

            // ── Points ──
            case LOG_TYPES.POINTS_BUY:
                transaction = {
                    logId,
                    type: "points_buy",
                    timestamp: ts,
                    action: "Points Bought",
                    details: "Bought " + (data.quantity || 0) + " points",
                    quantity: data.quantity || 0,
                    unitPrice: data.cost_each || data.cost || data.price || 0,
                    totalValue:
                        data.cost_total ||
                        data.total ||
                        (data.cost_each || data.cost || 0) * (data.quantity || 0),
                };
                break;

            case LOG_TYPES.POINTS_SELL:
                transaction = {
                    logId,
                    type: "points_sell",
                    timestamp: ts,
                    action: "Points Sold",
                    details: "Sold " + (data.quantity || 0) + " points",
                    quantity: data.quantity || 0,
                    unitPrice: data.cost_each || data.cost || data.price || 0,
                    totalValue:
                        data.cost_total ||
                        data.total ||
                        (data.cost_each || data.cost || 0) * (data.quantity || 0),
                };
                break;
        }

        return transaction;
    }

    // ========================================================================
    // GOOGLE SHEETS SYNC
    // ========================================================================

    async function syncToSheet(transactions) {
        const cfg = getConfig();
        if (!cfg.sheetWebAppUrl || transactions.length === 0) return;

        // Separate by type
        const purchases = [];
        const sales = [];
        const museums = [];
        const points = [];

        for (const tx of transactions) {
            if (tx.type === "museum_exchange") {
                museums.push(tx);
            } else if (tx.type === "points_buy" || tx.type === "points_sell") {
                points.push(tx);
            } else if (["purchase", "abroad_buy", "trade_in"].includes(tx.type)) {
                purchases.push(tx);
            } else if (["sale", "trade_out"].includes(tx.type)) {
                sales.push(tx);
            }
        }

        // Send purchases/sales in one batch
        const allTxns = [...purchases, ...sales];
        if (allTxns.length > 0) {
            try {
                await fetchGM(cfg.sheetWebAppUrl, {
                    method: "POST",
                    body: JSON.stringify({
                        secret: cfg.sheetSecret,
                        action: "logTransactions",
                        transactions: allTxns,
                    }),
                });
            } catch (e) {
                console.error("[TTA] Sheet sync failed:", e);
            }
        }

        // Send museum exchanges
        for (const m of museums) {
            try {
                await fetchGM(cfg.sheetWebAppUrl, {
                    method: "POST",
                    body: JSON.stringify({
                        secret: cfg.sheetSecret,
                        action: "logMuseumExchange",
                        setType: m.setType,
                        quantity: m.quantity,
                        pointsReceived: m.pointsReceived,
                        timestamp: m.timestamp,
                    }),
                });
            } catch (e) {
                console.error("[TTA] Museum sync failed:", e);
            }
        }

        // Send points transactions
        for (const p of points) {
            try {
                await fetchGM(cfg.sheetWebAppUrl, {
                    method: "POST",
                    body: JSON.stringify({
                        secret: cfg.sheetSecret,
                        action: "logPointsTransaction",
                        data: p,
                    }),
                });
            } catch (e) {
                console.error("[TTA] Points sync failed:", e);
            }
        }
    }

    // ========================================================================
    // MAIN LOG POLLER
    // ========================================================================

    async function pollLogs() {
        const cfg = getConfig();
        if (!cfg.enabled || !cfg.licenseValid || !cfg.tornApiKey) return;

        try {
            const logData = await fetchUserLog();
            if (!logData || !logData.log) return;

            const newTransactions = [];

            for (const [logId, entry] of Object.entries(logData.log)) {
                if (isLogProcessed(logId)) continue;

                const tx = processLogEntry(logId, entry, cfg.whitelist);
                if (tx) {
                    newTransactions.push(tx);
                    markLogProcessed(logId);
                } else {
                    // Mark as processed even if not whitelisted (so we don't recheck)
                    markLogProcessed(logId);
                }
            }

            if (newTransactions.length > 0) {
                console.log(`[TTA] Found ${newTransactions.length} new transactions`);
                await syncToSheet(newTransactions);
                updateStatusBadge(newTransactions.length);
            }
        } catch (e) {
            console.error("[TTA] Poll error:", e);
        }
    }

    // ========================================================================
    // UI — STYLES
    // ========================================================================

    GM_addStyle(`
    #tta-panel {
      position: fixed;
      z-index: 999999;
      background: #1a1a2e;
      border: 1px solid #16213e;
      border-radius: 8px;
      color: #e0e0e0;
      font-family: Arial, sans-serif;
      font-size: 12px;
      box-shadow: 0 4px 16px rgba(0,0,0,0.4);
      min-width: 44px;
      max-width: 380px;
      user-select: none;
      transition: width 0.2s, height 0.2s;
    }
    #tta-panel.minimized {
      width: 44px !important;
      height: 44px !important;
      border-radius: 50%;
      overflow: hidden;
      cursor: pointer;
    }
    #tta-panel.minimized #tta-body { display: none; }
    #tta-panel.minimized #tta-header-text { display: none; }
    #tta-panel.minimized #tta-header-buttons { display: none; }

    #tta-header {
      background: #16213e;
      padding: 8px 12px;
      border-radius: 8px 8px 0 0;
      display: flex;
      align-items: center;
      justify-content: space-between;
      cursor: move;
    }
    #tta-panel.minimized #tta-header {
      border-radius: 50%;
      justify-content: center;
      padding: 10px;
    }

    #tta-logo {
      font-size: 18px;
      line-height: 1;
    }
    #tta-header-text {
      font-weight: bold;
      font-size: 13px;
      margin-left: 8px;
      flex: 1;
    }
    #tta-header-buttons button {
      background: none;
      border: none;
      color: #aaa;
      cursor: pointer;
      font-size: 14px;
      padding: 2px 6px;
    }
    #tta-header-buttons button:hover { color: #fff; }

    #tta-body {
      padding: 10px 12px;
      max-height: 500px;
      overflow-y: auto;
    }

    #tta-badge {
      position: absolute;
      top: -4px;
      right: -4px;
      background: #e74c3c;
      color: #fff;
      font-size: 10px;
      font-weight: bold;
      border-radius: 50%;
      width: 18px;
      height: 18px;
      display: flex;
      align-items: center;
      justify-content: center;
      display: none;
    }

    .tta-section {
      margin-bottom: 10px;
    }
    .tta-section-title {
      font-weight: bold;
      font-size: 11px;
      color: #888;
      text-transform: uppercase;
      margin-bottom: 4px;
      border-bottom: 1px solid #333;
      padding-bottom: 2px;
    }

    .tta-input {
      width: 100%;
      padding: 6px 8px;
      background: #0f0f1a;
      border: 1px solid #333;
      border-radius: 4px;
      color: #e0e0e0;
      font-size: 12px;
      margin: 2px 0;
      box-sizing: border-box;
    }
    .tta-input:focus { border-color: #4285F4; outline: none; }

    .tta-btn {
      padding: 6px 12px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 11px;
      font-weight: bold;
      margin: 2px;
      transition: background 0.15s;
    }
    .tta-btn-primary { background: #4285F4; color: #fff; }
    .tta-btn-primary:hover { background: #3367d6; }
    .tta-btn-success { background: #0F9D58; color: #fff; }
    .tta-btn-success:hover { background: #0b7a45; }
    .tta-btn-danger { background: #DB4437; color: #fff; }
    .tta-btn-danger:hover { background: #b8382d; }
    .tta-btn-sm { padding: 3px 8px; font-size: 10px; }

    .tta-status {
      padding: 4px 8px;
      border-radius: 4px;
      font-size: 11px;
      margin: 4px 0;
    }
    .tta-status-ok { background: #0f3d0f; color: #4caf50; }
    .tta-status-warn { background: #3d2f0f; color: #ff9800; }
    .tta-status-err { background: #3d0f0f; color: #f44336; }

    .tta-whitelist-item {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 3px 0;
      border-bottom: 1px solid #222;
    }
    .tta-whitelist-item span { flex: 1; }

    .tta-tabs {
      display: flex;
      border-bottom: 1px solid #333;
      margin-bottom: 8px;
    }
    .tta-tab {
      padding: 6px 12px;
      cursor: pointer;
      color: #888;
      font-size: 11px;
      border-bottom: 2px solid transparent;
      transition: all 0.15s;
    }
    .tta-tab.active {
      color: #4285F4;
      border-bottom-color: #4285F4;
    }
    .tta-tab:hover { color: #ccc; }

    .tta-log-entry {
      padding: 3px 0;
      font-size: 11px;
      border-bottom: 1px solid #1a1a2e;
    }
    .tta-log-buy { color: #4caf50; }
    .tta-log-sell { color: #f44336; }
    .tta-log-trade { color: #ff9800; }
    .tta-log-museum { color: #9c27b0; }
  `);

    // ========================================================================
    // UI — PANEL BUILDER
    // ========================================================================

    function createPanel() {
        const cfg = getConfig();

        const panel = document.createElement("div");
        panel.id = "tta-panel";
        if (cfg.panelMinimized) panel.classList.add("minimized");
        panel.style.left = cfg.panelX + "px";
        panel.style.top = cfg.panelY + "px";

        panel.innerHTML = `
      <div id="tta-badge">0</div>
      <div id="tta-header">
        <span id="tta-logo">📊</span>
        <span id="tta-header-text">Trading Assistant</span>
        <span id="tta-header-buttons">
          <button id="tta-btn-min" title="Minimize">_</button>
        </span>
      </div>
      <div id="tta-body">
        <div class="tta-tabs">
          <div class="tta-tab active" data-tab="status">Status</div>
          <div class="tta-tab" data-tab="whitelist">Whitelist</div>
          <div class="tta-tab" data-tab="settings">Settings</div>
          <div class="tta-tab" data-tab="log">Log</div>
        </div>
        <div id="tta-tab-status" class="tta-tab-content">
          <div id="tta-status-content"></div>
        </div>
        <div id="tta-tab-whitelist" class="tta-tab-content" style="display:none">
          <div id="tta-whitelist-content"></div>
        </div>
        <div id="tta-tab-settings" class="tta-tab-content" style="display:none">
          <div id="tta-settings-content"></div>
        </div>
        <div id="tta-tab-log" class="tta-tab-content" style="display:none">
          <div id="tta-log-content"></div>
        </div>
      </div>
    `;

        document.body.appendChild(panel);

        // ── Tab switching ──
        panel.querySelectorAll(".tta-tab").forEach((tab) => {
            tab.addEventListener("click", () => {
                panel
                    .querySelectorAll(".tta-tab")
                    .forEach((t) => t.classList.remove("active"));
                panel
                    .querySelectorAll(".tta-tab-content")
                    .forEach((c) => (c.style.display = "none"));
                tab.classList.add("active");
                document.getElementById("tta-tab-" + tab.dataset.tab).style.display =
                    "block";
            });
        });

        // ── Minimize toggle ──
        document.getElementById("tta-btn-min").addEventListener("click", (e) => {
            e.stopPropagation();
            panel.classList.add("minimized");
            const c = getConfig();
            c.panelMinimized = true;
            saveConfig(c);
        });

        panel.addEventListener("click", () => {
            if (panel.classList.contains("minimized")) {
                panel.classList.remove("minimized");
                const c = getConfig();
                c.panelMinimized = false;
                saveConfig(c);
            }
        });

        // ── Dragging ──
        makeDraggable(panel);

        // ── Render tabs ──
        renderStatusTab();
        renderWhitelistTab();
        renderSettingsTab();
        renderLogTab();
    }

    // ========================================================================
    // UI — DRAG HANDLER
    // ========================================================================

    function makeDraggable(el) {
        const header = el.querySelector("#tta-header");
        let isDragging = false,
            startX,
            startY,
            origX,
            origY;

        header.addEventListener("mousedown", startDrag);
        header.addEventListener("touchstart", startDrag, { passive: false });

        function startDrag(e) {
            if (el.classList.contains("minimized")) return;
            isDragging = true;
            const ev = e.touches ? e.touches[0] : e;
            startX = ev.clientX;
            startY = ev.clientY;
            origX = el.offsetLeft;
            origY = el.offsetTop;
            e.preventDefault();

            document.addEventListener("mousemove", drag);
            document.addEventListener("touchmove", drag, { passive: false });
            document.addEventListener("mouseup", stopDrag);
            document.addEventListener("touchend", stopDrag);
        }

        function drag(e) {
            if (!isDragging) return;
            const ev = e.touches ? e.touches[0] : e;
            const dx = ev.clientX - startX;
            const dy = ev.clientY - startY;
            el.style.left = Math.max(0, origX + dx) + "px";
            el.style.top = Math.max(0, origY + dy) + "px";
            e.preventDefault();
        }

        function stopDrag() {
            isDragging = false;
            document.removeEventListener("mousemove", drag);
            document.removeEventListener("touchmove", drag);
            document.removeEventListener("mouseup", stopDrag);
            document.removeEventListener("touchend", stopDrag);

            const c = getConfig();
            c.panelX = el.offsetLeft;
            c.panelY = el.offsetTop;
            saveConfig(c);
        }
    }

    // ========================================================================
    // UI — STATUS TAB
    // ========================================================================

    function renderStatusTab() {
        const cfg = getConfig();
        const el = document.getElementById("tta-status-content");
        if (!el) return;

        const licensed = cfg.licenseValid;
        const apiOk = !!cfg.tornApiKey;
        const sheetOk = !!cfg.sheetWebAppUrl;
        const processedCount = Object.keys(cfg.processedLogs).length;

        el.innerHTML = `
      <div class="tta-section">
        <div class="tta-status ${licensed ? "tta-status-ok" : "tta-status-err"}">
          License: ${licensed ? "✅ Active" : "❌ Not verified"}
        </div>
        <div class="tta-status ${apiOk ? "tta-status-ok" : "tta-status-warn"}">
          API Key: ${apiOk ? "✅ Set" : "⚠️ Not set"}
        </div>
        <div class="tta-status ${sheetOk ? "tta-status-ok" : "tta-status-warn"}">
          Sheet: ${sheetOk ? "✅ Connected" : "⚠️ Not set"}
        </div>
        <div class="tta-status ${cfg.enabled ? "tta-status-ok" : "tta-status-warn"}">
          Tracking: ${cfg.enabled ? "✅ Active" : "⏸ Paused"}
        </div>
      </div>
      <div class="tta-section">
        <div class="tta-section-title">Stats</div>
        <div>📋 ${cfg.whitelist.length} items whitelisted</div>
        <div>📝 ${processedCount} log entries processed</div>
        <div>📊 ${JSON.parse(getSetting("recentLog", "[]")).length} recent transactions</div>
      </div>
      <div class="tta-section">
        <button class="tta-btn tta-btn-primary" id="tta-btn-poll">🔄 Check Now</button>
        <button class="tta-btn ${cfg.enabled ? "tta-btn-danger" : "tta-btn-success"}" id="tta-btn-toggle">
          ${cfg.enabled ? "⏸ Pause" : "▶ Resume"}
        </button>
      </div>
      <div class="tta-section">
        <button class="tta-btn tta-btn-success tta-btn-sm" id="tta-btn-setstock">📦 Set Starting Stock</button>
        <button class="tta-btn tta-btn-sm" style="background:#ff9800;color:#fff;" id="tta-btn-debug">🔍 Debug Log Fetch</button>
        <button class="tta-btn tta-btn-sm" style="background:#9c27b0;color:#fff;" id="tta-btn-clearprocessed">🗑 Clear Processed IDs</button>
      </div>
      <div style="font-size:10px;color:#555;margin-top:8px;">v${VERSION}</div>
    `;

        document
            .getElementById("tta-btn-poll")
            .addEventListener("click", async () => {
                const btn = document.getElementById("tta-btn-poll");
                btn.textContent = "⏳ Checking...";
                btn.disabled = true;
                await pollLogs();
                btn.textContent = "🔄 Check Now";
                btn.disabled = false;
            });

        document.getElementById("tta-btn-toggle").addEventListener("click", () => {
            const c = getConfig();
            c.enabled = !c.enabled;
            saveConfig(c);
            if (c.enabled) startPollLoop();
            else stopPollLoop();
            renderStatusTab();
        });

        document
            .getElementById("tta-btn-setstock")
            .addEventListener("click", () => {
                showSetStockDialog();
            });

        document
            .getElementById("tta-btn-debug")
            .addEventListener("click", async () => {
                const btn = document.getElementById("tta-btn-debug");
                btn.textContent = "⏳ Fetching...";
                btn.disabled = true;
                await debugLogFetch();
                btn.textContent = "🔍 Debug Log Fetch";
                btn.disabled = false;
            });

        document
            .getElementById("tta-btn-clearprocessed")
            .addEventListener("click", () => {
                if (
                    confirm(
                        "Clear all processed log IDs? This will re-process logs on next check (may create duplicates if sheet already has them).",
                    )
                ) {
                    const c = getConfig();
                    c.processedLogs = {};
                    saveConfig(c);
                    renderStatusTab();
                    alert("✅ Cleared! Next poll will re-check all visible logs.");
                }
            });
    }

    // ========================================================================
    // UI — WHITELIST TAB
    // ========================================================================

    function renderWhitelistTab() {
        const cfg = getConfig();
        const el = document.getElementById("tta-whitelist-content");
        if (!el) return;

        let whitelistHtml = cfg.whitelist
            .map((id) => {
                const name = getItemName(id);
                return `
        <div class="tta-whitelist-item">
          <span>${name} (${id})</span>
          <button class="tta-btn tta-btn-danger tta-btn-sm tta-wl-remove" data-id="${id}">✕</button>
        </div>
      `;
            })
            .join("");

        if (!whitelistHtml) {
            whitelistHtml =
                '<div style="color:#888;padding:8px 0;">No items whitelisted yet. Search below to add.</div>';
        }

        el.innerHTML = `
      <div class="tta-section">
        <div class="tta-section-title">Search & Add Items</div>
        <input type="text" class="tta-input" id="tta-wl-search"
               placeholder="Search by name or ID (case insensitive)..." />
        <div id="tta-wl-results" style="max-height:150px;overflow-y:auto;margin:4px 0;"></div>
      </div>
      <div class="tta-section">
        <div class="tta-section-title">Tracked Items (${cfg.whitelist.length})</div>
        <div id="tta-wl-list">${whitelistHtml}</div>
      </div>
    `;

        // Search handler
        document.getElementById("tta-wl-search").addEventListener("input", (e) => {
            const query = e.target.value.toLowerCase().trim();
            const results = document.getElementById("tta-wl-results");
            if (query.length < 2) {
                results.innerHTML = "";
                return;
            }

            const matches = [];
            for (const [id, item] of Object.entries(ITEM_MAP)) {
                if (!item.tradeable) continue;
                const nameMatch = item.name.toLowerCase().includes(query);
                const idMatch = id.includes(query);
                if (nameMatch || idMatch) {
                    const alreadyAdded = cfg.whitelist.includes(Number(id));
                    matches.push({
                        id: Number(id),
                        name: item.name,
                        type: item.type,
                        added: alreadyAdded,
                    });
                }
                if (matches.length >= 20) break;
            }

            results.innerHTML = matches
                .map(
                    (m) => `
        <div class="tta-whitelist-item">
          <span>${m.name} (${m.id}) <small style="color:#666">${m.type}</small></span>
          ${
                        m.added
                            ? '<span style="color:#4caf50;font-size:10px;">✓ Added</span>'
                            : `<button class="tta-btn tta-btn-success tta-btn-sm tta-wl-add" data-id="${m.id}">+ Add</button>`
                    }
        </div>
      `,
                )
                .join("");

            // Add button handlers
            results.querySelectorAll(".tta-wl-add").forEach((btn) => {
                btn.addEventListener("click", () => {
                    const c = getConfig();
                    const itemId = Number(btn.dataset.id);
                    if (!c.whitelist.includes(itemId)) {
                        c.whitelist.push(itemId);
                        saveConfig(c);
                        renderWhitelistTab();
                    }
                });
            });
        });

        // Remove button handlers
        el.querySelectorAll(".tta-wl-remove").forEach((btn) => {
            btn.addEventListener("click", () => {
                const c = getConfig();
                const itemId = Number(btn.dataset.id);
                c.whitelist = c.whitelist.filter((id) => id !== itemId);
                saveConfig(c);
                renderWhitelistTab();
            });
        });
    }

    // ========================================================================
    // UI — SETTINGS TAB
    // ========================================================================

    function renderSettingsTab() {
        const cfg = getConfig();
        const el = document.getElementById("tta-settings-content");
        if (!el) return;

        el.innerHTML = `
      <div class="tta-section">
        <div class="tta-section-title">License Key</div>
        <input type="text" class="tta-input" id="tta-set-license"
               placeholder="XXXX-XXXX-XXXX-XXXX" value="${cfg.licenseKey}" />
        <button class="tta-btn tta-btn-primary" id="tta-btn-verify">Verify License</button>
        <div id="tta-license-status"></div>
      </div>

      <div class="tta-section">
        <div class="tta-section-title">Torn API Key</div>
        <input type="password" class="tta-input" id="tta-set-apikey"
               placeholder="Your 16-char API key" value="${cfg.tornApiKey}" />
        <div style="font-size:10px;color:#888;">Requires Limited Access (log, money, travel)</div>
      </div>

      <div class="tta-section">
        <div class="tta-section-title">Google Sheet Web App URL</div>
        <input type="text" class="tta-input" id="tta-set-sheeturl"
               placeholder="https://script.google.com/macros/s/..." value="${cfg.sheetWebAppUrl}" />
      </div>

      <div class="tta-section">
        <div class="tta-section-title">Sheet Secret Passphrase</div>
        <input type="password" class="tta-input" id="tta-set-sheetsecret"
               placeholder="Same as set in Apps Script" value="${cfg.sheetSecret}" />
      </div>

      <div class="tta-section">
        <div class="tta-section-title">Poll Interval (seconds)</div>
        <input type="number" class="tta-input" id="tta-set-interval"
               min="15" max="300" value="${cfg.pollInterval}" />
        <div style="font-size:10px;color:#888;">How often to check for new transactions (min 15s)</div>
      </div>

      <div class="tta-section">
        <button class="tta-btn tta-btn-success" id="tta-btn-save">💾 Save Settings</button>
        <button class="tta-btn tta-btn-primary" id="tta-btn-test">🔗 Test Sheet Connection</button>
      </div>

      <div class="tta-section">
        <div class="tta-section-title">Danger Zone</div>
        <button class="tta-btn tta-btn-danger tta-btn-sm" id="tta-btn-reset">🗑 Reset All Data</button>
      </div>
    `;

        // Verify license
        document
            .getElementById("tta-btn-verify")
            .addEventListener("click", async () => {
                const key = document.getElementById("tta-set-license").value.trim();
                const statusEl = document.getElementById("tta-license-status");
                statusEl.innerHTML =
                    '<div class="tta-status tta-status-warn">⏳ Verifying...</div>';

                const valid = await verifyLicense(key);
                const c = getConfig();
                c.licenseKey = key;
                c.licenseValid = valid;
                saveConfig(c);

                statusEl.innerHTML = valid
                    ? '<div class="tta-status tta-status-ok">✅ License verified!</div>'
                    : '<div class="tta-status tta-status-err">❌ Invalid license key</div>';
                renderStatusTab();
            });

        // Save settings
        document.getElementById("tta-btn-save").addEventListener("click", () => {
            const c = getConfig();
            c.tornApiKey = document.getElementById("tta-set-apikey").value.trim();
            c.sheetWebAppUrl = document
                .getElementById("tta-set-sheeturl")
                .value.trim();
            c.sheetSecret = document
                .getElementById("tta-set-sheetsecret")
                .value.trim();
            c.pollInterval = Math.max(
                15,
                parseInt(document.getElementById("tta-set-interval").value) || 30,
            );
            saveConfig(c);

            // Rebuild item map with new API key
            buildItemMap();

            alert("✅ Settings saved!");
            renderStatusTab();
        });

        // Test sheet
        document
            .getElementById("tta-btn-test")
            .addEventListener("click", async () => {
                const c = getConfig();
                if (!c.sheetWebAppUrl) {
                    alert("Set Sheet URL first!");
                    return;
                }
                try {
                    const resp = await fetchGM(c.sheetWebAppUrl, {
                        method: "POST",
                        body: JSON.stringify({ secret: c.sheetSecret, action: "ping" }),
                    });
                    if (resp.data && resp.data.success) {
                        alert("✅ Sheet connected! Response: " + resp.data.message);
                    } else {
                        alert(
                            "❌ Sheet responded but failed: " + JSON.stringify(resp.data),
                        );
                    }
                } catch (e) {
                    alert("❌ Connection failed: " + e.message);
                }
            });

        // Reset
        document.getElementById("tta-btn-reset").addEventListener("click", () => {
            if (
                confirm(
                    "⚠️ This will erase ALL settings, whitelist, and log history.\n\nAre you sure?",
                )
            ) {
                [
                    "licenseKey",
                    "licenseValid",
                    "tornApiKey",
                    "sheetWebAppUrl",
                    "sheetSecret",
                    "whitelist",
                    "lastLogCheck",
                    "processedLogs",
                    "panelX",
                    "panelY",
                    "panelMinimized",
                    "enabled",
                    "pollInterval",
                    "itemMap",
                    "itemMapTime",
                    "recentLog",
                ].forEach((k) => GM_deleteValue("tta_" + k));
                location.reload();
            }
        });
    }

    // ========================================================================
    // UI — LOG TAB
    // ========================================================================

    function renderLogTab() {
        const el = document.getElementById("tta-log-content");
        if (!el) return;

        const logs = JSON.parse(getSetting("recentLog", "[]"));

        if (logs.length === 0) {
            el.innerHTML =
                '<div style="color:#888;padding:8px 0;">No transactions logged yet.</div>';
            return;
        }

        el.innerHTML = logs
            .slice(-50)
            .reverse()
            .map((log) => {
                const cls =
                    log.type.includes("sale") || log.type === "trade_out"
                        ? "tta-log-sell"
                        : log.type === "museum_exchange"
                            ? "tta-log-museum"
                            : log.type.includes("trade")
                                ? "tta-log-trade"
                                : "tta-log-buy";
                const time = new Date(log.timestamp * 1000).toLocaleString();
                return `
        <div class="tta-log-entry ${cls}">
          <strong>${log.type.toUpperCase()}</strong>
          ${log.itemName ? log.itemName + " x" + (log.quantity || 1) : log.setType || log.action || ""}
          <br><small>${time} | ${log.source || ""}</small>
        </div>
      `;
            })
            .join("");
    }

    function addToRecentLog(transactions) {
        const logs = JSON.parse(getSetting("recentLog", "[]"));
        for (const tx of transactions) {
            logs.push(tx);
        }
        // Keep last 200
        const trimmed = logs.slice(-200);
        setSetting("recentLog", JSON.stringify(trimmed));
    }

    // ========================================================================
    // UI — SET STARTING STOCK DIALOG
    // ========================================================================

    function showSetStockDialog() {
        const existing = document.getElementById("tta-stock-dialog");
        if (existing) existing.remove();

        // Define items locally (these match the Apps Script PLUSHIES/FLOWERS arrays)
        const KNOWN_PLUSHIES = [
            { id: 186, name: "Sheep Plushie" },
            { id: 187, name: "Teddy Bear Plushie" },
            { id: 215, name: "Kitten Plushie" },
            { id: 258, name: "Jaguar Plushie" },
            { id: 618, name: "Stingray Plushie" },
            { id: 261, name: "Wolverine Plushie" },
            { id: 266, name: "Nessie Plushie" },
            { id: 268, name: "Red Fox Plushie" },
            { id: 269, name: "Monkey Plushie" },
            { id: 273, name: "Chamois Plushie" },
            { id: 274, name: "Panda Plushie" },
            { id: 384, name: "Camel Plushie" },
            { id: 281, name: "Lion Plushie" },
        ];
        const KNOWN_FLOWERS = [
            { id: 260, name: "Dahlia" },
            { id: 617, name: "Banana Orchid" },
            { id: 263, name: "Crocus" },
            { id: 264, name: "Orchid" },
            { id: 267, name: "Heather" },
            { id: 271, name: "Ceibo Flower" },
            { id: 272, name: "Edelweiss" },
            { id: 277, name: "Cherry Blossom" },
            { id: 276, name: "Peony" },
            { id: 385, name: "Tribulus Omanense" },
            { id: 282, name: "African Violet" },
        ];

        const dialog = document.createElement("div");
        dialog.id = "tta-stock-dialog";
        dialog.style.cssText = `
      position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
      z-index:9999999;background:#1a1a2e;border:2px solid #4285F4;
      border-radius:12px;padding:20px;color:#e0e0e0;
      max-height:80vh;overflow-y:auto;width:320px;
      box-shadow:0 8px 32px rgba(0,0,0,0.6);font-family:Arial;font-size:12px;
    `;

        dialog.innerHTML = `
      <h3 style="margin:0 0 12px;color:#4285F4;">📦 Set Starting Stock</h3>
      <div style="font-size:10px;color:#888;margin-bottom:10px;">
        Enter how many of each item you currently own.
        This sets the baseline for tracking.
      </div>
      <div style="font-weight:bold;color:#DB4437;margin:8px 0 4px;">🧸 Plushies</div>
      ${KNOWN_PLUSHIES.map(
            (i) => `
        <div style="display:flex;align-items:center;margin:3px 0;">
          <span style="flex:1;">${i.name}</span>
          <input type="number" min="0" value="0" style="width:60px;padding:4px;
            background:#0f0f1a;border:1px solid #333;color:#e0e0e0;border-radius:4px;"
            data-itemid="${i.id}" class="tta-stock-input" />
        </div>
      `,
        ).join("")}
      <div style="font-weight:bold;color:#0F9D58;margin:12px 0 4px;">🌸 Flowers</div>
      ${KNOWN_FLOWERS.map(
            (i) => `
        <div style="display:flex;align-items:center;margin:3px 0;">
          <span style="flex:1;">${i.name}</span>
          <input type="number" min="0" value="0" style="width:60px;padding:4px;
            background:#0f0f1a;border:1px solid #333;color:#e0e0e0;border-radius:4px;"
            data-itemid="${i.id}" class="tta-stock-input" />
        </div>
      `,
        ).join("")}
      <div style="margin-top:12px;display:flex;gap:8px;">
        <button class="tta-btn tta-btn-success" id="tta-stock-save">💾 Save to Sheet</button>
        <button class="tta-btn tta-btn-danger" id="tta-stock-cancel">Cancel</button>
      </div>
      <div id="tta-stock-status" style="margin-top:8px;"></div>
    `;

        document.body.appendChild(dialog);

        document
            .getElementById("tta-stock-cancel")
            .addEventListener("click", () => {
                dialog.remove();
            });

        document
            .getElementById("tta-stock-save")
            .addEventListener("click", async () => {
                const cfg = getConfig();
                if (!cfg.sheetWebAppUrl) {
                    alert("Set your Sheet Web App URL in settings first!");
                    return;
                }

                const statusEl = document.getElementById("tta-stock-status");
                statusEl.innerHTML =
                    '<div class="tta-status tta-status-warn">⏳ Saving...</div>';

                const stockItems = [];
                dialog.querySelectorAll(".tta-stock-input").forEach((input) => {
                    const qty = parseInt(input.value) || 0;
                    const itemId = parseInt(input.dataset.itemid);
                    if (itemId) {
                        stockItems.push({ itemId, quantity: qty });
                    }
                });

                try {
                    const resp = await fetchGM(cfg.sheetWebAppUrl, {
                        method: "POST",
                        body: JSON.stringify({
                            secret: cfg.sheetSecret,
                            action: "setInitialStock",
                            items: stockItems,
                        }),
                    });

                    if (resp.data && resp.data.success) {
                        statusEl.innerHTML = `<div class="tta-status tta-status-ok">✅ Updated ${resp.data.updated} items!</div>`;
                        setTimeout(() => dialog.remove(), 2000);
                    } else {
                        statusEl.innerHTML = `<div class="tta-status tta-status-err">❌ ${resp.data ? resp.data.error : "Unknown error"}</div>`;
                    }
                } catch (e) {
                    statusEl.innerHTML = `<div class="tta-status tta-status-err">❌ ${e.message}</div>`;
                }
            });
    }

    // ========================================================================
    // UI — STATUS BADGE
    // ========================================================================

    function updateStatusBadge(count) {
        const badge = document.getElementById("tta-badge");
        if (!badge) return;
        if (count > 0) {
            badge.style.display = "flex";
            badge.textContent = count;
            setTimeout(() => {
                badge.style.display = "none";
            }, 5000);
        }
    }

    // ========================================================================
    // PAGE MONITOR — Detect transactions on currently viewed pages
    // ========================================================================

    /**
     * COMPLIANCE NOTE:
     * This only reads data from the page the user is CURRENTLY VIEWING.
     * It does NOT make any additional requests to Torn.
     * It does NOT scrape pages not being viewed.
     * It only observes DOM changes on the active page.
     */

    function setupPageMonitor() {
        const path = window.location.pathname;

        // Monitor item market page
        if (path.includes("/imarket") || path.includes("/itemmarket")) {
            observePageForTransactions("itemmarket");
        }

        // Monitor bazaar page
        if (path.includes("/bazaar")) {
            observePageForTransactions("bazaar");
        }

        // Monitor trade page
        if (path.includes("/trade")) {
            observePageForTransactions("trade");
        }

        // Monitor museum page
        if (path.includes("/museum")) {
            observePageForTransactions("museum");
        }

        // Monitor abroad page
        if (path.includes("/travelagency") || path.includes("/abroad")) {
            observePageForTransactions("abroad");
        }
    }

    function observePageForTransactions(pageType) {
        // Use MutationObserver to detect when new content loads
        // This is compliant — we're only reading the page the user is viewing
        const observer = new MutationObserver((mutations) => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType !== 1) continue;

                    // Look for success messages / confirmation dialogs
                    const text = node.textContent || "";

                    // Item market / bazaar buy confirmations
                    if (pageType === "itemmarket" || pageType === "bazaar") {
                        if (
                            text.includes("You bought") ||
                            text.includes("purchase") ||
                            text.includes("sold") ||
                            text.includes("listed")
                        ) {
                            // Trigger a log poll to catch the transaction
                            setTimeout(() => pollLogs(), 3000);
                        }
                    }

                    // Trade completions
                    if (pageType === "trade") {
                        if (text.includes("Trade completed") || text.includes("accepted")) {
                            setTimeout(() => pollLogs(), 3000);
                        }
                    }

                    // Museum exchanges
                    if (pageType === "museum") {
                        if (
                            text.includes("exchanged") ||
                            text.includes("museum") ||
                            text.includes("points")
                        ) {
                            setTimeout(() => pollLogs(), 3000);
                        }
                    }

                    // Abroad purchases
                    if (pageType === "abroad") {
                        if (text.includes("bought") || text.includes("purchase")) {
                            setTimeout(() => pollLogs(), 3000);
                        }
                    }
                }
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true,
        });
    }

    // ========================================================================
    // ENHANCED LOG POLLER WITH SHEET SYNC
    // ========================================================================

    // Override the original pollLogs with one that also updates the log tab
    const _originalPollLogs = pollLogs;

    pollLogs = async function () {
        const cfg = getConfig();
        if (!cfg.enabled || !cfg.licenseValid || !cfg.tornApiKey) return;

        try {
            const logData = await fetchUserLog();
            if (!logData || !logData.log) return;

            const newTransactions = [];

            for (const [logId, entry] of Object.entries(logData.log)) {
                if (isLogProcessed(logId)) continue;

                const tx = processLogEntry(logId, entry, cfg.whitelist);
                if (tx) {
                    newTransactions.push(tx);
                    markLogProcessed(logId);
                } else {
                    markLogProcessed(logId);
                }
            }

            if (newTransactions.length > 0) {
                console.log(`[TTA] Found ${newTransactions.length} new transactions`);

                // Add to recent log for display
                addToRecentLog(newTransactions);

                // Sync to sheet
                await syncToSheet(newTransactions);

                // Update badge & notification
                updateStatusBadge(newTransactions.length);
                showNotification(newTransactions);
            }

            // ALWAYS refresh UI — even when 0 new transactions
            // (so processed count updates after Check Now)
            renderStatusTab();
            renderLogTab();
        } catch (e) {
            console.error("[TTA] Poll error:", e);
        }
    };

    // ========================================================================
    // NOTIFICATIONS
    // ========================================================================

    function showNotification(transactions) {
        const existing = document.getElementById("tta-notification");
        if (existing) existing.remove();

        const purchases = transactions.filter((t) =>
            ["purchase", "abroad_buy", "trade_in"].includes(t.type),
        );
        const sales = transactions.filter((t) =>
            ["sale", "trade_out"].includes(t.type),
        );
        const other = transactions.filter(
            (t) =>
                !["purchase", "abroad_buy", "trade_in", "sale", "trade_out"].includes(
                    t.type,
                ),
        );

        let msg = "";
        if (purchases.length) msg += `📥 ${purchases.length} purchase(s) logged\n`;
        if (sales.length) msg += `📤 ${sales.length} sale(s) logged\n`;
        if (other.length) msg += `📋 ${other.length} other event(s) logged\n`;

        const notif = document.createElement("div");
        notif.id = "tta-notification";
        notif.style.cssText = `
      position:fixed;bottom:20px;right:20px;z-index:9999999;
      background:#1a1a2e;border:1px solid #4285F4;border-radius:8px;
      padding:12px 16px;color:#e0e0e0;font-family:Arial;font-size:12px;
      box-shadow:0 4px 16px rgba(0,0,0,0.4);max-width:280px;
      animation:ttaSlideIn 0.3s ease;
    `;
        notif.innerHTML = `
      <div style="font-weight:bold;margin-bottom:4px;">📊 Trading Assistant</div>
      <div style="white-space:pre-line;">${msg.trim()}</div>
    `;

        // Add animation
        GM_addStyle(`
      @keyframes ttaSlideIn {
        from { transform: translateX(100%); opacity: 0; }
        to { transform: translateX(0); opacity: 1; }
      }
    `);

        document.body.appendChild(notif);
        setTimeout(() => {
            if (notif.parentNode) {
                notif.style.transition = "opacity 0.3s";
                notif.style.opacity = "0";
                setTimeout(() => notif.remove(), 300);
            }
        }, 4000);
    }

    // ========================================================================
    // MAIN POLL LOOP
    // ========================================================================

    let pollTimer = null;

    function startPollLoop() {
        const cfg = getConfig();
        if (pollTimer) clearInterval(pollTimer);

        const interval = Math.max(15, cfg.pollInterval) * 1000;

        // Initial poll after 5 seconds (let page load)
        setTimeout(() => pollLogs(), 5000);

        // Then poll on interval
        pollTimer = setInterval(() => {
            pollLogs();
        }, interval);
    }

    function stopPollLoop() {
        if (pollTimer) {
            clearInterval(pollTimer);
            pollTimer = null;
        }
    }

    // ========================================================================
    // INITIALIZATION
    // ========================================================================

    async function init() {
        console.log("[TTA] Torn Trading Assistant v" + VERSION + " starting...");

        const cfg = getConfig();

        // Build item map (from API, cached 24h)
        if (cfg.tornApiKey) {
            await buildItemMap();
        }

        // Create the UI panel
        createPanel();

        // Setup page monitoring (compliant — current page only)
        setupPageMonitor();

        // Register GM menu command for quick access
        GM_registerMenuCommand("📊 Trading Assistant Settings", () => {
            const panel = document.getElementById("tta-panel");
            if (panel) {
                panel.classList.remove("minimized");
                const c = getConfig();
                c.panelMinimized = false;
                saveConfig(c);
                // Switch to settings tab
                panel
                    .querySelectorAll(".tta-tab")
                    .forEach((t) => t.classList.remove("active"));
                panel
                    .querySelectorAll(".tta-tab-content")
                    .forEach((c) => (c.style.display = "none"));
                panel.querySelector('[data-tab="settings"]').classList.add("active");
                document.getElementById("tta-tab-settings").style.display = "block";
            }
        });

        // Auto-verify license on load (if key exists)
        if (cfg.licenseKey && !cfg.licenseValid) {
            const valid = await verifyLicense(cfg.licenseKey);
            if (valid) {
                const c = getConfig();
                c.licenseValid = true;
                saveConfig(c);
                renderStatusTab();
                console.log("[TTA] License auto-verified ✓");
            }
        }

        // Start polling if licensed and configured
        if (cfg.licenseValid && cfg.tornApiKey) {
            startPollLoop();
            console.log("[TTA] Poll loop started");
        } else {
            console.log("[TTA] Not starting poll loop — license or API key missing");
        }
    }

    // Wait for page to be ready
    if (
        document.readyState === "complete" ||
        document.readyState === "interactive"
    ) {
        setTimeout(init, 1000);
    } else {
        window.addEventListener("load", () => setTimeout(init, 1000));
    }
})();