✨ Points Museum - Fixed

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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();

})();