✨ Points Museum - Fixed

Points Museum with item images in suggestions - Fixed memory leaks and refresh issues

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         ✨ Points Museum - Fixed
// @namespace    http://tampermonkey.net/
// @version      14.6.1
// @description  Points Museum with item images in suggestions - Fixed memory leaks and refresh issues
// @match        https://www.torn.com/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // Check if already initialized
    if (window.__pointsMuseumFixedInitialized) {
        console.log('Points Museum already running, skipping duplicate');
        return;
    }
    window.__pointsMuseumFixedInitialized = true;

    const UID = 'pm_' + Math.random().toString(36).substr(2, 8);
    const PREFIX = `pts_museum_${UID}`;

    function id(name) {
        return `${PREFIX}_${name}`;
    }

    // ================= CONFIG =================
    const THEME_KEY = "pts_museum_theme_preference";
    const POLL = 120000;
    const PRE_PTS = 25,
        FLO_PTS = 10,
        PLU_PTS = 10;
    const CATEGORY_ORDER_KEY = 'pts_museum_category_order';
    const COLLAPSED_CATEGORIES_KEY = 'pts_museum_collapsed_categories';
    const MUSEUM_DAY_MONTH = 5;
    const MUSEUM_DAY_DATE = 18;
    const POINTS_PRICE_CACHE_DURATION = 600000;

    const DEFAULT_CATEGORY_ORDER = ['Prehistoric', 'Flowers', 'Plushies', 'Special'];

    let currentPointsPrice = 0;
    let pointsPriceCache = {
        time: 0,
        price: 0
    };
    let isMuseumDay = false;
    let daysToMuseumDay = 0;
    let isPanelOpen = false;
    let refreshTimer = null;
    let isLoading = false;
    let isDestroyed = false;

    // Store event listeners for cleanup
    const eventListeners = new Map();
    let urlObserver = null;
    let styleInjected = false;

    // ================= DATA =================
    const GROUPS = {
        Prehistoric: {
            name: "Prehistoric",
            emoji: "🦕",
            pts: PRE_PTS,
            bonusPts: PRE_PTS * 0.10,
            items: {
                "Quartz Point": { s: "Q", flag: "🇨🇦", id: 1504 },
                "Chalcedony Point": { s: "CH", flag: "🇦🇷", id: 1503 },
                "Basalt Point": { s: "B", flag: "🏝️", id: 1502 },
                "Quartzite Point": { s: "QZ", flag: "🇿🇦", id: 1500 },
                "Chert Point": { s: "CT", flag: "🇬🇧", id: 1501 },
                "Obsidian Point": { s: "O", flag: "🇲🇽", id: 1499 }
            }
        },
        Flowers: {
            name: "Flowers",
            emoji: "🌸",
            pts: FLO_PTS,
            bonusPts: FLO_PTS * 0.10,
            items: {
                "Dahlia": { s: "DH", flag: "🇲🇽", id: 260 },
                "Orchid": { s: "OR", flag: "🏝️", id: 264 },
                "African Violet": { s: "V", flag: "🇿🇦", id: 282 },
                "Cherry Blossom": { s: "CB", flag: "🇯🇵", id: 277 },
                "Peony": { s: "P", flag: "🇨🇳", id: 276 },
                "Ceibo Flower": { s: "CE", flag: "🇦🇷", id: 271 },
                "Edelweiss": { s: "E", flag: "🇨🇭", id: 272 },
                "Crocus": { s: "CR", flag: "🇨🇦", id: 263 },
                "Heather": { s: "H", flag: "🇬🇧", id: 267 },
                "Tribulus Omanense": { s: "T", flag: "🇦🇪", id: 385 },
                "Banana Orchid": { s: "BO", flag: "🇰🇾", id: 617 }
            }
        },
        Plushies: {
            name: "Plushies",
            emoji: "🧸",
            pts: PLU_PTS,
            bonusPts: PLU_PTS * 0.10,
            items: {
                "Sheep Plushie": { s: "SH", flag: "🏝️", id: 186 },
                "Teddy Bear Plushie": { s: "TB", flag: "🏝️", id: 187 },
                "Kitten Plushie": { s: "KT", flag: "🏝️", id: 215 },
                "Jaguar Plushie": { s: "J", flag: "🇲🇽", id: 258 },
                "Wolverine Plushie": { s: "W", flag: "🇨🇦", id: 261 },
                "Nessie Plushie": { s: "N", flag: "🇬🇧", id: 266 },
                "Red Fox Plushie": { s: "F", flag: "🇬🇧", id: 268 },
                "Monkey Plushie": { s: "M", flag: "🇦🇷", id: 269 },
                "Chamois Plushie": { s: "CM", flag: "🇨🇭", id: 273 },
                "Panda Plushie": { s: "PD", flag: "🇨🇳", id: 274 },
                "Lion Plushie": { s: "L", flag: "🇿🇦", id: 281 },
                "Camel Plushie": { s: "CA", flag: "🇦🇪", id: 384 },
                "Stingray Plushie": { s: "SR", flag: "🇰🇾", id: 618 }
            }
        }
    };

    function addSafeEventListener(element, event, handler) {
        if (!element) return;
        element.addEventListener(event, handler);
        if (!eventListeners.has(element)) {
            eventListeners.set(element, []);
        }
        eventListeners.get(element).push({ event, handler });
    }

    function cleanupElementListeners(element) {
        if (eventListeners.has(element)) {
            eventListeners.get(element).forEach(({ event, handler }) => {
                element.removeEventListener(event, handler);
            });
            eventListeners.delete(element);
        }
    }

    function destroy() {
        if (isDestroyed) return;
        isDestroyed = true;

        if (refreshTimer) {
            clearTimeout(refreshTimer);
            refreshTimer = null;
        }

        for (const [element, listeners] of eventListeners) {
            if (element && element.removeEventListener) {
                listeners.forEach(({ event, handler }) => {
                    element.removeEventListener(event, handler);
                });
            }
        }
        eventListeners.clear();

        if (urlObserver) {
            urlObserver.disconnect();
            urlObserver = null;
        }

        const container = document.getElementById(id('edge_container'));
        if (container) container.remove();
    }

    window.addEventListener('beforeunload', () => destroy());

    let lastUrl = location.href;
    urlObserver = new MutationObserver(() => {
        if (location.href !== lastUrl && !isDestroyed) {
            lastUrl = location.href;
            isLoading = false;
            if (refreshTimer) {
                clearTimeout(refreshTimer);
                refreshTimer = null;
            }
            setTimeout(() => {
                if (!isDestroyed) mainLoop();
            }, 1000);
        }
    });
    urlObserver.observe(document, { subtree: true, childList: true });

    function getItemImageUrl(itemId) {
        return `https://www.torn.com/images/items/${itemId}/small.png`;
    }

    function formatMoney(value) {
        if (value === undefined || value === null) return '$0';
        return '$' + Math.round(value).toLocaleString();
    }

    function calculateDaysToMuseumDay() {
        const today = new Date();
        const currentYear = today.getFullYear();
        let museumDay = new Date(currentYear, MUSEUM_DAY_MONTH - 1, MUSEUM_DAY_DATE);
        if (today > museumDay) {
            museumDay = new Date(currentYear + 1, MUSEUM_DAY_MONTH - 1, MUSEUM_DAY_DATE);
        }
        return Math.ceil((museumDay - today) / (1000 * 60 * 60 * 24));
    }

    function checkMuseumDay() {
        const now = new Date();
        isMuseumDay = (now.getMonth() + 1 === MUSEUM_DAY_MONTH && now.getDate() === MUSEUM_DAY_DATE);
        daysToMuseumDay = calculateDaysToMuseumDay();
        return isMuseumDay;
    }

    function getLimitingItem(inventory, group) {
        let minItem = null;
        let minQty = Infinity;
        for (const [name, data] of Object.entries(group.items)) {
            const qty = inventory[name] || 0;
            if (qty < minQty) {
                minQty = qty;
                minItem = { name, id: data.id, quantity: qty };
            }
        }
        return minItem;
    }

    function getCategoryOrder() {
        const saved = GM_getValue(CATEGORY_ORDER_KEY, DEFAULT_CATEGORY_ORDER);
        const validOrder = saved.filter(cat => ['Prehistoric', 'Flowers', 'Plushies', 'Special'].includes(cat));
        DEFAULT_CATEGORY_ORDER.forEach(cat => {
            if (!validOrder.includes(cat)) validOrder.push(cat);
        });
        return validOrder;
    }

    function saveCategoryOrder(order) {
        GM_setValue(CATEGORY_ORDER_KEY, order);
    }

    function getCollapsedCategories() {
        return GM_getValue(COLLAPSED_CATEGORIES_KEY, []);
    }

    function saveCollapsedCategory(category, isCollapsed) {
        let collapsed = getCollapsedCategories();
        if (isCollapsed && !collapsed.includes(category)) collapsed.push(category);
        else if (!isCollapsed && collapsed.includes(category)) collapsed = collapsed.filter(c => c !== category);
        GM_setValue(COLLAPSED_CATEGORIES_KEY, collapsed);
    }

    function toggleCategory(category) {
        saveCollapsedCategory(category, !getCollapsedCategories().includes(category));
        render();
    }

    function moveCategoryUp(category) {
        const order = getCategoryOrder();
        const index = order.indexOf(category);
        if (index > 0) {
            [order[index - 1], order[index]] = [order[index], order[index - 1]];
            saveCategoryOrder(order);
            render();
        }
    }

    function moveCategoryDown(category) {
        const order = getCategoryOrder();
        const index = order.indexOf(category);
        if (index < order.length - 1) {
            [order[index], order[index + 1]] = [order[index + 1], order[index]];
            saveCategoryOrder(order);
            render();
        }
    }

    function applyTheme(theme) {
        const container = document.getElementById(id('edge_container'));
        if (!container) return;
        container.classList.remove(`${PREFIX}_dark_edge`, `${PREFIX}_light_edge`);
        container.classList.add(theme === 'light' ? `${PREFIX}_light_edge` : `${PREFIX}_dark_edge`);
        GM_setValue(THEME_KEY, theme);
    }

    function toggleTheme() {
        const container = document.getElementById(id('edge_container'));
        const isDark = container.classList.contains(`${PREFIX}_dark_edge`);
        applyTheme(isDark ? 'light' : 'dark');
        showToast(isDark ? "🎨 Light theme" : "🎨 Dark theme");
    }

    function loadSavedTheme() {
        applyTheme(GM_getValue(THEME_KEY, 'dark'));
    }

    function showToast(message, duration = 2000) {
        const existing = document.querySelector(`.${PREFIX}_toast`);
        if (existing) existing.remove();
        const toast = document.createElement('div');
        toast.className = `${PREFIX}_toast`;
        toast.textContent = message;
        document.body.appendChild(toast);
        setTimeout(() => toast.remove(), duration);
    }

    function updateLastUpdateTime() {
        const el = document.getElementById(id('last_update'));
        if (el) el.textContent = new Date().toLocaleTimeString();
    }

    function createHaloUI() {
        if (document.getElementById(id('edge_container'))) return;

        const container = document.createElement("div");
        container.id = id('edge_container');
        container.className = `${PREFIX}_edge_container ${PREFIX}_dark_edge`;
        container.innerHTML = `
        <div class="${PREFIX}_pull_tab ${PREFIX}_pull_tab_right" id="${id('pull_tab')}">
            <div class="${PREFIX}_tab_icon">▶</div>
            <div class="${PREFIX}_tab_text">PTS</div>
            <div class="${PREFIX}_tab_stats" id="${id('tab_sets')}">0</div>
        </div>
        <div class="${PREFIX}_edge_panel ${PREFIX}_edge_panel_right" id="${id('edge_panel')}">
            <div class="${PREFIX}_panel_header">
                <div class="${PREFIX}_brand"><span class="${PREFIX}_star">✨</span><span>POINTS MUSEUM</span></div>
                <div class="${PREFIX}_header_buttons">
                    <button class="${PREFIX}_refresh_btn_panel" id="${id('refresh_panel_btn')}" title="Refresh">⟳</button>
                    <button class="${PREFIX}_settings_btn" id="${id('settings_btn')}" title="Settings">⚙</button>
                    <button class="${PREFIX}_close_panel" id="${id('close_panel')}" title="Close">✕</button>
                </div>
            </div>
            <div class="${PREFIX}_panel_inner">
                <div class="${PREFIX}_quick_stats">
                    <div class="${PREFIX}_stat_item"><div class="${PREFIX}_stat_number" id="${id('stat_sets')}">0</div><div class="${PREFIX}_stat_label">Sets</div></div>
                    <div class="${PREFIX}_stat_item"><div class="${PREFIX}_stat_number" id="${id('stat_points')}">0</div><div class="${PREFIX}_stat_label">Points</div></div>
                    <div class="${PREFIX}_stat_item"><div class="${PREFIX}_stat_number" id="${id('stat_value')}">$0</div><div class="${PREFIX}_stat_label">Value</div></div>
                </div>
                <div class="${PREFIX}_highlight_bar" id="${id('highlight_bar')}">
                    <div class="${PREFIX}_highlight_text">-- days 🏛️ 10% -- points @ --</div>
                </div>
                <div class="${PREFIX}_suggestions_section">
                    <div class="${PREFIX}_suggestions_content" id="${id('suggestions_content')}">
                        <div class="${PREFIX}_loading">Loading...</div>
                    </div>
                </div>
                <div class="${PREFIX}_inventory_section">
                    <div class="${PREFIX}_section_header clickable" id="${id('inventory_toggle')}">
                        <span>📦 INVENTORY</span>
                        <span class="${PREFIX}_toggle_icon">▼</span>
                    </div>
                    <div class="${PREFIX}_inventory_content" id="${id('inventory_content')}">
                        <div id="${id('items_container')}"><div class="${PREFIX}_loading">Loading...</div></div>
                    </div>
                </div>
                <div class="${PREFIX}_footer">
                    <span class="${PREFIX}_update_time" id="${id('last_update')}">--:--:--</span>
                    <a href="https://www.torn.com/profiles.php?XID=2637223" target="_blank">❤️</a>
                </div>
            </div>
        </div>
        <div class="${PREFIX}_modal_overlay" id="${id('modal_overlay')}" style="display: none;">
            <div class="${PREFIX}_modal">
                <div class="${PREFIX}_modal_header"><span>⚙ Settings</span><button class="${PREFIX}_modal_close" id="${id('modal_close_btn')}">✕</button></div>
                <div class="${PREFIX}_modal_body">
                    <div class="${PREFIX}_settings_field"><label>API Key (16 chars)</label><input type="password" id="${id('api_input')}" placeholder="Enter Torn API key" class="${PREFIX}_input"><button id="${id('api_save')}" class="${PREFIX}_btn_primary">Save</button></div>
                    <div class="${PREFIX}_settings_field"><label>Appearance</label><button id="${id('theme_toggle')}" class="${PREFIX}_btn_secondary">🌓 Toggle Theme</button></div>
                    <div class="${PREFIX}_settings_field"><label style="color: var(--pts-danger);">Danger</label><button id="${id('reset_btn')}" class="${PREFIX}_btn_danger">⚠ Reset All</button></div>
                </div>
            </div>
        </div>`;
        document.body.appendChild(container);
    }

    function initEvents() {
        const panel = document.getElementById(id('edge_panel'));
        const pullTab = document.getElementById(id('pull_tab'));

        const closePanelBtn = document.getElementById(id('close_panel'));
        if (closePanelBtn) {
            addSafeEventListener(closePanelBtn, 'click', () => {
                isPanelOpen = false;
                if (panel) panel.classList.remove(`${PREFIX}_open`);
                if (pullTab) {
                    const icon = pullTab.querySelector(`.${PREFIX}_tab_icon`);
                    if (icon) icon.textContent = '▶';
                }
            });
        }

        if (pullTab) {
            addSafeEventListener(pullTab, 'click', () => {
                isPanelOpen = !isPanelOpen;
                if (panel) {
                    if (isPanelOpen) {
                        panel.classList.add(`${PREFIX}_open`);
                        const icon = pullTab.querySelector(`.${PREFIX}_tab_icon`);
                        if (icon) icon.textContent = '◀';
                    } else {
                        panel.classList.remove(`${PREFIX}_open`);
                        const icon = pullTab.querySelector(`.${PREFIX}_tab_icon`);
                        if (icon) icon.textContent = '▶';
                    }
                }
            });
        }

        const refreshBtn = document.getElementById(id('refresh_panel_btn'));
        if (refreshBtn) {
            addSafeEventListener(refreshBtn, 'click', () => {
                showToast("⟳ Refreshing...");
                forceRefresh();
            });
        }

        const settingsBtn = document.getElementById(id('settings_btn'));
        const modalOverlay = document.getElementById(id('modal_overlay'));
        if (settingsBtn && modalOverlay) {
            addSafeEventListener(settingsBtn, 'click', () => {
                modalOverlay.style.display = 'flex';
                document.body.style.overflow = 'hidden';
                const input = document.getElementById(id('api_input'));
                if (input) input.value = GM_getValue('tornAPIKey', '');
            });
        }

        const closeModal = () => {
            if (modalOverlay) modalOverlay.style.display = 'none';
            document.body.style.overflow = '';
        };

        const modalCloseBtn = document.getElementById(id('modal_close_btn'));
        if (modalCloseBtn) addSafeEventListener(modalCloseBtn, 'click', closeModal);

        if (modalOverlay) {
            addSafeEventListener(modalOverlay, 'click', (e) => {
                if (e.target === modalOverlay) closeModal();
            });
        }

        addSafeEventListener(document, 'keydown', (e) => {
            if (e.key === 'Escape' && modalOverlay && modalOverlay.style.display === 'flex') closeModal();
        });

        const themeToggle = document.getElementById(id('theme_toggle'));
        if (themeToggle) {
            addSafeEventListener(themeToggle, 'click', () => {
                toggleTheme();
                closeModal();
            });
        }

        const resetBtn = document.getElementById(id('reset_btn'));
        if (resetBtn) {
            addSafeEventListener(resetBtn, 'click', () => {
                if (confirm('Delete ALL data?')) {
                    GM_setValue('tornAPIKey', '');
                    GM_setValue(THEME_KEY, '');
                    GM_setValue(CATEGORY_ORDER_KEY, '');
                    GM_setValue(COLLAPSED_CATEGORIES_KEY, '');
                    showToast('All data reset. Refresh page.');
                    setTimeout(() => location.reload(), 1500);
                }
                closeModal();
            });
        }

        const apiSaveBtn = document.getElementById(id('api_save'));
        if (apiSaveBtn) {
            addSafeEventListener(apiSaveBtn, 'click', () => {
                const input = document.getElementById(id('api_input'));
                const key = input ? input.value.trim() : '';
                if (key && key.length === 16) {
                    GM_setValue('tornAPIKey', key);
                    showToast("🔑 API Key saved!");
                    closeModal();
                    forceRefresh();
                } else {
                    showToast("❌ API key must be 16 characters");
                }
            });
        }

        const inventoryToggle = document.getElementById(id('inventory_toggle'));
        const inventoryContent = document.getElementById(id('inventory_content'));
        if (inventoryToggle && inventoryContent) {
            addSafeEventListener(inventoryToggle, 'click', () => {
                const isVisible = inventoryContent.style.display !== 'none';
                inventoryContent.style.display = isVisible ? 'none' : 'block';
                const toggleIcon = inventoryToggle.querySelector(`.${PREFIX}_toggle_icon`);
                if (toggleIcon) toggleIcon.textContent = isVisible ? '▶' : '▼';
            });
        }
    }

    async function localItems() {
        const key = GM_getValue('tornAPIKey');
        if (!key) throw new Error('No API key');
        const response = await fetch(`https://api.torn.com/user/?selections=display&key=${key}`).then(r => r.json());
        if (response.error) throw new Error(response.error.error || 'API Error');
        const items = {}, prices = {};
        if (response.display && Array.isArray(response.display)) {
            response.display.forEach(item => {
                items[item.name] = (items[item.name] || 0) + item.quantity;
                prices[item.name] = item.market_price || 0;
            });
        }
        return { items, prices };
    }

    function gmJSON(url) {
        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: 'GET',
                url,
                onload: r => {
                    try { resolve(JSON.parse(r.responseText)); }
                    catch { resolve({}); }
                },
                onerror: () => resolve({})
            });
        });
    }

    async function abroadItems() {
        const yataData = await gmJSON('https://yata.yt/api/v1/travel/export/');
        const abroadMap = {};
        if (yataData && yataData.stocks) {
            Object.values(yataData.stocks).forEach(country => {
                if (country && country.stocks) {
                    country.stocks.forEach(item => {
                        abroadMap[item.name] = (abroadMap[item.name] || 0) + (item.quantity || 0);
                    });
                }
            });
        }
        return abroadMap;
    }

    async function fetchPointsPrice(apiKey) {
        const now = Date.now();
        if (pointsPriceCache.time && (now - pointsPriceCache.time) < POINTS_PRICE_CACHE_DURATION) {
            return pointsPriceCache.price;
        }
        try {
            const response = await fetch(`https://api.torn.com/v2/market/pointsmarket?key=${apiKey}`);
            const data = await response.json();
            if (data && data.pointsmarket) {
                const listings = Object.values(data.pointsmarket).filter(l => l.quantity > 0).map(l => l.cost).sort((a, b) => a - b);
                if (listings.length > 0) {
                    const avgPrice = Math.round(listings.slice(0, 5).reduce((s, p) => s + p, 0) / Math.min(5, listings.length));
                    pointsPriceCache = { time: now, price: avgPrice };
                    return avgPrice;
                }
            }
        } catch (error) {}
        return currentPointsPrice || 0;
    }

    function calcSet(inventory, items) {
        const values = Object.keys(items).map(key => inventory[key] || 0);
        return { sets: values.length ? Math.min(...values) : 0 };
    }

    function getStatusClass(abroadCount) {
        if (abroadCount === 0) return `${PREFIX}_status_red`;
        if (abroadCount > 1000) return `${PREFIX}_status_green`;
        return `${PREFIX}_status_orange`;
    }

    async function forceRefresh() {
        if (isLoading) {
            showToast("Already refreshing...");
            return;
        }
        pointsPriceCache = { time: 0, price: 0 };
        await render(true);
    }

    async function render(force = false) {
        if (isLoading && !force) return;
        if (isDestroyed) return;
        isLoading = true;

        const container = document.getElementById(id('items_container'));
        if (!container) {
            isLoading = false;
            return;
        }

        const apiKey = GM_getValue('tornAPIKey');
        if (!apiKey) {
            if (container) container.innerHTML = `<div class="${PREFIX}_edge_empty">🔑 No API key - Click ⚙</div>`;
            const suggestionsContent = document.getElementById(id('suggestions_content'));
            if (suggestionsContent) suggestionsContent.innerHTML = `<div class="${PREFIX}_suggest_note">🔑 Set API key first</div>`;
            updateStats(0, 0, 0);
            updateLastUpdateTime();
            isLoading = false;
            return;
        }

        try {
            const { items: inventory, prices: itemPrices } = await localItems();
            const abroad = await abroadItems();
            let pointsPrice = await fetchPointsPrice(apiKey);
            checkMuseumDay();

            let totalSets = 0, totalPoints = 0, totalItemValue = 0;
            let categoryHtml = {};

            const processCat = (catName, group) => {
                const { sets } = calcSet(inventory, group.items);
                let html = `<div class="${PREFIX}_category_t" data-category="${catName}">
                    <div class="${PREFIX}_category_title"><span>${catName === 'Prehistoric' ? '🦕' : catName === 'Flowers' ? '🌸' : '🧸'}</span> ${catName} <span class="${PREFIX}_category_sets">${sets.toLocaleString()}</span></div>
                    <div class="${PREFIX}_category_controls"><span class="${PREFIX}_category_btn up" data-category="${catName}">▲</span><span class="${PREFIX}_category_btn down" data-category="${catName}">▼</span></div>
                </div><div class="${PREFIX}_category_content" data-category="${catName}">`;

                let totalVal = 0;
                const sorted = Object.entries(group.items).map(([name, data]) => ({
                    name, data,
                    remaining: (inventory[name] || 0) - sets
                })).sort((a, b) => a.remaining - b.remaining);

                for (const { name, data, remaining } of sorted) {
                    const price = itemPrices[name] || 0;
                    const abroadCount = abroad[name] || 0;
                    totalVal += (inventory[name] || 0) * price;
                    html += `<div class="${PREFIX}_item_row">
                        <img src="${getItemImageUrl(data.id)}" class="${PREFIX}_item_img" alt="${name}">
                        <span class="${PREFIX}_item_local">${remaining.toLocaleString()}</span>
                        <span class="${PREFIX}_item_abroad ${getStatusClass(abroadCount)}">${abroadCount.toLocaleString()}</span>
                        <span class="${PREFIX}_item_flag">${data.flag}</span>
                    </div>`;
                }
                html += `</div>`;
                return { sets, points: sets * group.pts, html, totalValue: totalVal };
            };

            const prehistoric = processCat('Prehistoric', GROUPS.Prehistoric);
            const flowers = processCat('Flowers', GROUPS.Flowers);
            const plushies = processCat('Plushies', GROUPS.Plushies);

            categoryHtml['Prehistoric'] = prehistoric.html;
            categoryHtml['Flowers'] = flowers.html;
            categoryHtml['Plushies'] = plushies.html;

            totalSets = prehistoric.sets + flowers.sets + plushies.sets;
            totalPoints = prehistoric.points + flowers.points + plushies.points;
            totalItemValue = prehistoric.totalValue + flowers.totalValue + plushies.totalValue;

            const meteorite = inventory["Meteorite Fragment"] || 0;
            const fossil = inventory["Patagonian Fossil"] || 0;
            const meteoritePrice = itemPrices["Meteorite Fragment"] || 0;
            const fossilPrice = itemPrices["Patagonian Fossil"] || 0;
            totalItemValue += (meteorite * meteoritePrice) + (fossil * fossilPrice);
            totalPoints += (meteorite * 15) + (fossil * 20);

            categoryHtml['Special'] = `<div class="${PREFIX}_category_t" data-category="Special">
                <div class="${PREFIX}_category_title"><span>⭐</span> Special <span class="${PREFIX}_category_sets">${(meteorite + fossil).toLocaleString()}</span></div>
                <div class="${PREFIX}_category_controls"><span class="${PREFIX}_category_btn up" data-category="Special">▲</span><span class="${PREFIX}_category_btn down" data-category="Special">▼</span></div>
            </div><div class="${PREFIX}_category_content" data-category="Special">
                <div class="${PREFIX}_item_row"><img src="${getItemImageUrl(1488)}" class="${PREFIX}_item_img"><span class="${PREFIX}_item_local">${meteorite.toLocaleString()}</span><span class="${PREFIX}_item_abroad ${getStatusClass(abroad["Meteorite Fragment"] || 0)}">${(abroad["Meteorite Fragment"] || 0).toLocaleString()}</span><span class="${PREFIX}_item_flag">🇦🇷</span></div>
                <div class="${PREFIX}_item_row"><img src="${getItemImageUrl(1487)}" class="${PREFIX}_item_img"><span class="${PREFIX}_item_local">${fossil.toLocaleString()}</span><span class="${PREFIX}_item_abroad ${getStatusClass(abroad["Patagonian Fossil"] || 0)}">${(abroad["Patagonian Fossil"] || 0).toLocaleString()}</span><span class="${PREFIX}_item_flag">🇦🇷</span></div>
            </div>`;

            const pointsValue = totalPoints * pointsPrice;
            const bonusPoints = Math.round(totalPoints * 0.10);
            const bonusValue = Math.round(bonusPoints * pointsPrice);

            const statSets = document.getElementById(id('stat_sets'));
            const statPoints = document.getElementById(id('stat_points'));
            const statValue = document.getElementById(id('stat_value'));
            const tabSets = document.getElementById(id('tab_sets'));

            if (statSets) statSets.textContent = totalSets.toLocaleString();
            if (statPoints) statPoints.textContent = totalPoints.toLocaleString();
            if (statValue) statValue.textContent = formatMoney(pointsValue);
            if (tabSets) tabSets.textContent = totalSets.toLocaleString();

            const highlightBar = document.getElementById(id('highlight_bar'));
            if (highlightBar) {
                highlightBar.innerHTML = `<div class="${PREFIX}_highlight_text">${daysToMuseumDay} days 🏛️ 10%  ${bonusPoints.toLocaleString()} points @ ${formatMoney(bonusValue)}</div>`;
            }

            const suggestionsContainer = document.getElementById(id('suggestions_content'));
            if (suggestionsContainer) {
                let suggestionsHtml = '';
                for (const catName of ['Plushies', 'Flowers', 'Prehistoric']) {
                    const group = GROUPS[catName];
                    const bonusValuePerSet = Math.round(group.bonusPts * pointsPrice);
                    const limitingItem = getLimitingItem(inventory, group);
                    const bonusPtsDisplay = Number.isInteger(group.bonusPts) ? group.bonusPts : group.bonusPts.toFixed(1);
                    suggestionsHtml += `<div class="${PREFIX}_suggestion_row">
                        <img src="${getItemImageUrl(limitingItem.id)}" class="${PREFIX}_suggestion_img">
                        <span class="${PREFIX}_suggestion_emoji">${group.emoji}</span>
                        <span class="${PREFIX}_suggestion_text">+${bonusPtsDisplay} pts → ${formatMoney(bonusValuePerSet)}</span>
                    </div>`;
                }
                suggestionsContainer.innerHTML = suggestionsHtml;
            }

            let inventoryHtml = '';
            const categoryOrder = getCategoryOrder();
            const collapsedCategories = getCollapsedCategories();

            for (const cat of categoryOrder) {
                if (categoryHtml[cat]) {
                    inventoryHtml += categoryHtml[cat];
                }
            }

            if (!inventoryHtml) inventoryHtml = `<div class="${PREFIX}_edge_empty">📦 No items</div>`;
            if (container) container.innerHTML = inventoryHtml;

            // Apply collapsed states
            for (const cat of categoryOrder) {
                const content = document.querySelector(`.${PREFIX}_category_content[data-category="${cat}"]`);
                if (collapsedCategories.includes(cat) && content) {
                    content.style.maxHeight = '0';
                    content.style.overflow = 'hidden';
                } else if (content) {
                    content.style.maxHeight = '';
                    content.style.overflow = '';
                }
            }

            updateLastUpdateTime();

        } catch (error) {
            console.error('Points Museum error:', error);
            if (container) container.innerHTML = `<div class="${PREFIX}_edge_empty">⚠️ Error: ${error.message}</div>`;
            updateStats(0, 0, 0);
        } finally {
            isLoading = false;
        }
    }

    function updateStats(totalSets, totalPoints, totalValue) {
        const setsEl = document.getElementById(id('stat_sets'));
        const pointsEl = document.getElementById(id('stat_points'));
        const valueEl = document.getElementById(id('stat_value'));
        const tabSetsEl = document.getElementById(id('tab_sets'));
        if (setsEl) setsEl.textContent = totalSets.toLocaleString();
        if (pointsEl) pointsEl.textContent = totalPoints.toLocaleString();
        if (valueEl) valueEl.textContent = formatMoney(totalValue);
        if (tabSetsEl) tabSetsEl.textContent = totalSets.toLocaleString();
    }

    async function mainLoop() {
        if (isDestroyed) return;
        if (refreshTimer) clearTimeout(refreshTimer);
        const apiKey = GM_getValue('tornAPIKey');
        if (apiKey) await fetchPointsPrice(apiKey).catch(() => {});
        await render();
        if (!isDestroyed) refreshTimer = setTimeout(() => mainLoop(), POLL);
    }

    function injectStyles() {
        if (styleInjected) return;
        styleInjected = true;
        GM_addStyle(`
        .${PREFIX}_edge_container { position: fixed; top: 0; right: 0; height: 100vh; width: 0; z-index: 99999; pointer-events: none; }
        .${PREFIX}_dark_edge { --pts-bg: rgba(8,12,20,0.98); --pts-border: rgba(52,152,219,0.35); --pts-text: #e0e0e0; --pts-text-dim: #7f8c8d; --pts-accent: #3498db; --pts-accent-glow: rgba(52,152,219,0.12); --pts-success: #2ecc71; --pts-warning: #f39c12; --pts-danger: #e74c3c; }
        .${PREFIX}_light_edge { --pts-bg: rgba(255,255,255,0.98); --pts-border: rgba(52,152,219,0.4); --pts-text: #2c3e50; --pts-text-dim: #95a5a6; --pts-accent: #2980b9; --pts-accent-glow: rgba(52,152,219,0.08); --pts-success: #27ae60; --pts-warning: #f39c12; --pts-danger: #e74c3c; }
        .${PREFIX}_pull_tab_right { position: fixed; right: 0; top: 20%; width: 20px; background: var(--pts-bg); backdrop-filter: blur(12px); border: 1px solid var(--pts-border); border-right: none; border-radius: 6px 0 0 6px; padding: 8px 2px; display: flex; flex-direction: column; align-items: center; gap: 4px; cursor: pointer; pointer-events: auto; z-index: 100000; }
        .${PREFIX}_tab_icon { font-size: 7px; color: var(--pts-accent); }
        .${PREFIX}_tab_text { font-size: 6px; font-weight: 700; writing-mode: vertical-rl; color: var(--pts-accent); }
        .${PREFIX}_tab_stats { font-size: 8px; font-weight: 700; text-align: center; color: var(--pts-accent); background: var(--pts-accent-glow); padding: 2px 2px; border-radius: 3px; writing-mode: vertical-rl; }
        .${PREFIX}_edge_panel_right { position: fixed; right: 0; top: 50%; transform: translateY(-50%) translateX(100%); width: 200px; max-height: 90vh; background: var(--pts-bg); backdrop-filter: blur(16px); border-left: 1px solid var(--pts-border); border-radius: 8px 0 0 8px; transition: transform 0.25s ease; pointer-events: auto; display: flex; flex-direction: column; overflow: hidden; z-index: 100000; }
        .${PREFIX}_edge_panel_right.${PREFIX}_open { transform: translateY(-50%) translateX(0); }
        .${PREFIX}_panel_header { display: flex; justify-content: space-between; align-items: center; padding: 5px 7px; border-bottom: 1px solid var(--pts-border); background: var(--pts-accent-glow); }
        .${PREFIX}_brand { font-size: 9px; font-weight: 700; color: var(--pts-accent); }
        .${PREFIX}_star { font-size: 9px; animation: ptsSpinStar 3s linear infinite; }
        @keyframes ptsSpinStar { 100% { transform: rotate(360deg); } }
        .${PREFIX}_header_buttons { display: flex; gap: 4px; }
        .${PREFIX}_settings_btn, .${PREFIX}_refresh_btn_panel, .${PREFIX}_close_panel { background: rgba(0,0,0,0.3); border: none; border-radius: 3px; padding: 2px 5px; font-size: 8px; cursor: pointer; color: var(--pts-text-dim); }
        .${PREFIX}_refresh_btn_panel:hover { background: var(--pts-success); color: white; }
        .${PREFIX}_settings_btn:hover, .${PREFIX}_close_panel:hover { background: var(--pts-accent); color: white; }
        .${PREFIX}_close_panel:hover { background: var(--pts-danger); }
        .${PREFIX}_quick_stats { display: flex; padding: 5px; gap: 4px; border-bottom: 1px solid var(--pts-border); background: var(--pts-accent-glow); }
        .${PREFIX}_stat_item { flex: 1; text-align: center; background: rgba(0,0,0,0.2); border-radius: 4px; padding: 3px 1px; }
        .${PREFIX}_stat_number { font-size: 9px; font-weight: 700; color: var(--pts-accent); }
        .${PREFIX}_stat_label { font-size: 6px; color: var(--pts-text-dim); text-transform: uppercase; }
        .${PREFIX}_highlight_bar { padding: 5px 5px; margin: 5px 6px; background: rgba(241,196,15,0.15); border-radius: 5px; text-align: center; }
        .${PREFIX}_highlight_text { font-size: 8px; font-weight: 700; color: #f1c40f; }
        .${PREFIX}_suggestions_section { padding: 5px 6px; border-bottom: 1px solid var(--pts-border); }
        .${PREFIX}_suggestions_content { max-height: 110px; overflow-y: auto; }
        .${PREFIX}_suggestions_content::-webkit-scrollbar { width: 2px; }
        .${PREFIX}_suggestions_content::-webkit-scrollbar-thumb { background: var(--pts-accent); border-radius: 2px; }
        .${PREFIX}_suggestion_row { display: flex; align-items: center; gap: 6px; padding: 4px 6px; background: rgba(0,0,0,0.2); border-radius: 4px; margin-bottom: 4px; }
        .${PREFIX}_suggestion_img { width: 16px; height: 16px; object-fit: contain; }
        .${PREFIX}_suggestion_emoji { font-size: 12px; }
        .${PREFIX}_suggestion_text { font-size: 7px; font-weight: 700; color: var(--pts-success); }
        .${PREFIX}_inventory_section { flex: 1; display: flex; flex-direction: column; min-height: 0; padding: 0 5px; }
        .${PREFIX}_section_header.clickable { cursor: pointer; display: flex; justify-content: space-between; align-items: center; border-radius: 3px; padding: 4px 0; font-size: 7px; font-weight: 700; color: var(--pts-accent); }
        .${PREFIX}_section_header.clickable:hover { background: var(--pts-accent-glow); }
        .${PREFIX}_toggle_icon { font-size: 7px; color: var(--pts-text-dim); }
        .${PREFIX}_inventory_content { flex: 1; overflow-y: auto; max-height: 220px; }
        .${PREFIX}_inventory_content::-webkit-scrollbar { width: 2px; }
        .${PREFIX}_inventory_content::-webkit-scrollbar-thumb { background: var(--pts-accent); border-radius: 2px; }
        .${PREFIX}_category_t { padding: 3px 4px; background: var(--pts-accent-glow); color: var(--pts-accent); font-weight: 700; font-size: 6.5px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; border-radius: 3px; margin-top: 3px; }
        .${PREFIX}_category_title { display: flex; align-items: center; gap: 3px; }
        .${PREFIX}_category_sets { font-size: 5.5px; color: var(--pts-text-dim); margin-left: 3px; }
        .${PREFIX}_category_controls { display: flex; gap: 2px; }
        .${PREFIX}_category_btn { width: 11px; height: 11px; background: rgba(0,0,0,0.3); border-radius: 2px; color: var(--pts-text-dim); font-size: 5px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
        .${PREFIX}_category_btn:hover { background: var(--pts-accent); color: white; }
        .${PREFIX}_category_content { transition: max-height 0.2s; overflow: hidden; }
        .${PREFIX}_item_row { display: grid; grid-template-columns: 22px 42px 42px 22px; gap: 6px; align-items: center; padding: 4px 3px; font-size: 7px; border-bottom: 1px solid var(--pts-border); }
        .${PREFIX}_item_img { width: 20px; height: 20px; object-fit: contain; justify-self: center; }
        .${PREFIX}_item_local { color: var(--pts-success); font-weight: 700; text-align: center; }
        .${PREFIX}_item_abroad { text-align: center; padding: 2px 4px; border-radius: 3px; font-size: 6px; font-weight: 600; }
        .${PREFIX}_item_flag { font-size: 11px; text-align: center; justify-self: center; }
        .${PREFIX}_status_green { color: var(--pts-success); background: rgba(46,204,113,0.15); }
        .${PREFIX}_status_orange { color: var(--pts-warning); background: rgba(243,156,18,0.15); }
        .${PREFIX}_status_red { color: var(--pts-danger); background: rgba(231,76,60,0.15); }
        .${PREFIX}_loading, .${PREFIX}_edge_empty { text-align: center; padding: 10px 4px; font-size: 6px; color: var(--pts-text-dim); }
        .${PREFIX}_footer { padding: 4px; text-align: center; border-top: 1px solid var(--pts-border); font-size: 5.5px; display: flex; justify-content: space-between; align-items: center; }
        .${PREFIX}_footer a { color: var(--pts-accent); text-decoration: none; }
        .${PREFIX}_update_time { color: var(--pts-text-dim); font-size: 5.5px; }
        .${PREFIX}_modal_overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); backdrop-filter: blur(4px); z-index: 200000; display: none; align-items: center; justify-content: center; }
        .${PREFIX}_modal { background: var(--pts-bg); border: 1px solid var(--pts-border); border-radius: 8px; width: 240px; max-width: 90%; }
        .${PREFIX}_modal_header { padding: 8px 10px; border-bottom: 1px solid var(--pts-border); display: flex; justify-content: space-between; color: var(--pts-accent); font-weight: 600; font-size: 10px; }
        .${PREFIX}_modal_close { background: rgba(0,0,0,0.3); border: none; border-radius: 3px; width: 20px; height: 20px; cursor: pointer; color: var(--pts-text-dim); font-size: 9px; }
        .${PREFIX}_modal_close:hover { background: var(--pts-danger); color: white; }
        .${PREFIX}_modal_body { padding: 10px; }
        .${PREFIX}_settings_field { margin-bottom: 10px; }
        .${PREFIX}_settings_field label { display: block; font-size: 8px; color: var(--pts-text-dim); margin-bottom: 3px; }
        .${PREFIX}_input { width: 100%; padding: 6px; background: rgba(0,0,0,0.3); border: 1px solid var(--pts-border); border-radius: 4px; color: var(--pts-text); font-size: 8px; box-sizing: border-box; }
        .${PREFIX}_input:focus { outline: none; border-color: var(--pts-accent); }
        .${PREFIX}_btn_primary, .${PREFIX}_btn_secondary, .${PREFIX}_btn_danger { padding: 5px 8px; border-radius: 4px; font-size: 8px; font-weight: 600; cursor: pointer; border: none; }
        .${PREFIX}_btn_primary { background: var(--pts-accent); color: white; }
        .${PREFIX}_btn_primary:hover { filter: brightness(1.1); }
        .${PREFIX}_btn_secondary { background: rgba(0,0,0,0.3); border: 1px solid var(--pts-border); color: var(--pts-text); }
        .${PREFIX}_btn_secondary:hover { background: var(--pts-accent-glow); }
        .${PREFIX}_btn_danger { background: var(--pts-danger); color: white; }
        .${PREFIX}_btn_danger:hover { filter: brightness(1.1); }
        .${PREFIX}_toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: var(--pts-bg); border: 1px solid var(--pts-accent); padding: 5px 10px; border-radius: 14px; font-size: 8px; color: var(--pts-accent); z-index: 200001; white-space: nowrap; pointer-events: none; animation: ptsFadeOut 2s forwards; }
        @keyframes ptsFadeOut { 0% { opacity: 1; } 70% { opacity: 1; } 100% { opacity: 0; visibility: hidden; } }
        `);
    }

    function init() {
        if (window.__pointsMuseumFixedRunning) {
            console.log('Points Museum already running');
            return;
        }
        window.__pointsMuseumFixedRunning = true;

        console.log('✨ Points Museum v14.6 - Fixed');
        injectStyles();
        createHaloUI();
        initEvents();
        loadSavedTheme();

        const savedKey = GM_getValue('tornAPIKey');
        if (!savedKey) {
            const key = prompt('Enter your Torn API key (16 chars):');
            if (key && key.length === 16) {
                GM_setValue('tornAPIKey', key);
                showToast('API key saved!');
                mainLoop();
            } else if (key) {
                showToast('Invalid API key');
            }
        } else {
            mainLoop();
        }
    }

    // CRITICAL FIX: Add event listeners for category buttons AFTER render
    // This is done in a MutationObserver to catch dynamically added buttons
    function attachCategoryButtonListeners() {
        // Up buttons
        document.querySelectorAll(`.${PREFIX}_category_btn.up`).forEach(btn => {
            if (btn.hasAttribute('data-listener-attached')) return;
            const category = btn.getAttribute('data-category');
            cleanupElementListeners(btn);
            addSafeEventListener(btn, 'click', (e) => {
                e.stopPropagation();
                if (category) moveCategoryUp(category);
            });
            btn.setAttribute('data-listener-attached', 'true');
        });

        // Down buttons
        document.querySelectorAll(`.${PREFIX}_category_btn.down`).forEach(btn => {
            if (btn.hasAttribute('data-listener-attached')) return;
            const category = btn.getAttribute('data-category');
            cleanupElementListeners(btn);
            addSafeEventListener(btn, 'click', (e) => {
                e.stopPropagation();
                if (category) moveCategoryDown(category);
            });
            btn.setAttribute('data-listener-attached', 'true');
        });

        // Category titles (for collapse/expand)
        document.querySelectorAll(`.${PREFIX}_category_t`).forEach(title => {
            if (title.hasAttribute('data-listener-attached')) return;
            const category = title.getAttribute('data-category');
            cleanupElementListeners(title);
            addSafeEventListener(title, 'click', (e) => {
                if (e.target.classList && e.target.classList.contains(`${PREFIX}_category_btn`)) return;
                if (category) toggleCategory(category);
            });
            title.setAttribute('data-listener-attached', 'true');
        });
    }

    // Watch for DOM changes to attach listeners to new buttons
    const observer = new MutationObserver(() => {
        attachCategoryButtonListeners();
    });
    observer.observe(document.body, { childList: true, subtree: true });

    // Override render to call attach after HTML is set
    const originalRender = render;
    render = async function(force = false) {
        await originalRender(force);
        attachCategoryButtonListeners();
    };

    window.addEventListener('pagehide', () => {
        if (refreshTimer) clearTimeout(refreshTimer);
        window.__pointsMuseumFixedRunning = false;
        window.__pointsMuseumFixedInitialized = false;
        observer.disconnect();
    });

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();

})();