HeyMax SubCaps Viewer

Monitor network requests and display SubCaps calculations for UOB cards on HeyMax

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         HeyMax SubCaps Viewer
// @namespace    http://tampermonkey.net/
// @version      2.0.0
// @description  Monitor network requests and display SubCaps calculations for UOB cards on HeyMax
// @author       Laurence Putra Franslay (@laurenceputra)
// @source       https://github.com/laurenceputra/heymax-subcaps-viewer-chromium/
// @update       https://github.com/laurenceputra/heymax-subcaps-viewer-chromium/raw/refs/heads/main/tampermonkey/heymax-subcaps-viewer.user.js
// @match        https://heymax.ai/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=heymax.ai
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_listValues
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    console.log('[HeyMax SubCaps Viewer] Tampermonkey script starting...');

    // ============================================================================
    // API Interception via Direct Monkey Patching
    // ============================================================================
    
    // Store original functions
    const originalFetch = window.fetch;
    const originalXHROpen = XMLHttpRequest.prototype.open;
    const originalXHRSend = XMLHttpRequest.prototype.send;

    // Helper functions for API interception
    
    /**
     * Check if URL should be logged
     */
    function shouldLogUrl(url) {
        try {
            const urlObj = new URL(url, window.location.href);
            
            if (urlObj.hostname !== 'heymax.ai') {
                return false;
            }
            
            const pathname = urlObj.pathname;
            
            if (pathname.startsWith('/cards/your-cards/')) {
                return true;
            }
            
            if (pathname.startsWith('/api/spend_tracking/cards/') && 
                (pathname.includes('/summary') || pathname.includes('/transactions'))) {
                return true;
            }
            
            if (pathname === '/api/spend_tracking/card_tracker') {
                return true;
            }
            
            return false;
        } catch (error) {
            return false;
        }
    }

    /**
     * Extract card ID from URL
     */
    function extractCardId(url) {
        const match = url.match(/\/api\/spend_tracking\/cards\/([a-f0-9]+)\//);
        if (match) {
            return match[1];
        }
        
        if (url.includes('/cards//')) {
            const pageMatch = window.location.pathname.match(/\/cards\/your-cards\/([a-f0-9]+)/);
            return pageMatch ? pageMatch[1] : null;
        }
        
        return null;
    }

    /**
     * Determine the data type from URL
     */
    function getDataType(url) {
        if (url.includes('/transactions')) {
            return 'transactions';
        } else if (url.includes('/summary')) {
            return 'summary';
        } else if (url.includes('/card_tracker')) {
            return 'card_tracker';
        }
        return null;
    }

    /**
     * Store API response data using GM storage
     */
    function storeApiData(method, url, status, data) {
        console.log('[HeyMax SubCaps Viewer] API Response intercepted:');
        console.log('  Method:', method);
        console.log('  URL:', url);
        console.log('  Status:', status);
        
        const dataType = getDataType(url);
        let cardId = extractCardId(url);
        
        // For card_tracker, try to get card ID from the current page URL
        if (dataType === 'card_tracker' && !cardId) {
            const pageMatch = window.location.pathname.match(/\/cards\/your-cards\/([a-f0-9]+)/);
            if (pageMatch) {
                cardId = pageMatch[1];
            }
        }
        
        // Get existing card data
        const cardDataStr = GM_getValue('cardData', '{}');
        const cardData = JSON.parse(cardDataStr);
        
        if (dataType && cardId) {
            // Initialize card object if it doesn't exist
            if (!cardData[cardId]) {
                cardData[cardId] = {};
            }
            
            // Store the latest data for this card ID and data type
            cardData[cardId][dataType] = {
                data: data,
                timestamp: new Date().toISOString(),
                url: url,
                status: status
            };
            
            console.log(`[HeyMax SubCaps Viewer] Stored ${dataType} for card ${cardId}`);
        } else if (dataType === 'card_tracker' && !cardId) {
            // card_tracker on main listing page (no specific card ID)
            cardData['card_tracker'] = {
                data: data,
                timestamp: new Date().toISOString(),
                url: url,
                status: status
            };
            
            console.log('[HeyMax SubCaps Viewer] Stored card_tracker data (global)');
        }
        
        // Save the updated cardData structure
        GM_setValue('cardData', JSON.stringify(cardData));
    }

    // Fetch interception
    window.fetch = async function(...args) {
        const response = await originalFetch.apply(this, args);
        const url = typeof args[0] === 'string' ? args[0] : args[0].url;
        
        if (typeof url === 'string' && shouldLogUrl(url)) {
            const clonedResponse = response.clone();
            try {
                const contentType = response.headers.get('content-type');
                if (contentType && contentType.includes('application/json')) {
                    const data = await clonedResponse.json();
                    const method = args[1]?.method || 'GET';
                    storeApiData(method, url, response.status, data);
                }
            } catch (e) {
                console.error('[HeyMax SubCaps Viewer] Error processing fetch response:', e);
            }
        }
        
        return response;
    };

    // XMLHttpRequest interception
    XMLHttpRequest.prototype.open = function(method, url, ...rest) {
        this._method = method;
        this._url = url;
        return originalXHROpen.apply(this, [method, url, ...rest]);
    };

    XMLHttpRequest.prototype.send = function(...args) {
        const url = this._url;
        
        if (url && typeof url === 'string' && shouldLogUrl(url)) {
            this.addEventListener('load', function() {
                if (this.readyState === 4 && this.status >= 200 && this.status < 300) {
                    try {
                        const contentType = this.getResponseHeader('content-type');
                        if (contentType && contentType.includes('application/json')) {
                            const data = JSON.parse(this.responseText);
                            storeApiData(this._method, url, this.status, data);
                        }
                    } catch (error) {
                        console.error('[HeyMax SubCaps Viewer] Error processing XHR response:', error);
                    }
                }
            });
        }
        
        return originalXHRSend.apply(this, args);
    };

    console.log('[HeyMax SubCaps Viewer] API interception initialized');

    // Monitor for patches being overwritten and re-apply if necessary
    setInterval(() => {
        if (window.fetch !== window.fetch || window.fetch.toString().includes('native code')) {
            // Re-check if our patch is still in place
            const fetchStr = window.fetch.toString();
            if (!fetchStr.includes('originalFetch') && !fetchStr.includes('[HeyMax SubCaps Viewer]')) {
                console.warn('[HeyMax SubCaps Viewer] fetch() was overwritten! Re-applying patch...');
                window.fetch = async function(...args) {
                    const response = await originalFetch.apply(this, args);
                    const url = typeof args[0] === 'string' ? args[0] : args[0].url;
                    
                    if (typeof url === 'string' && shouldLogUrl(url)) {
                        const clonedResponse = response.clone();
                        try {
                            const contentType = response.headers.get('content-type');
                            if (contentType && contentType.includes('application/json')) {
                                const data = await clonedResponse.json();
                                const method = args[1]?.method || 'GET';
                                storeApiData(method, url, response.status, data);
                            }
                        } catch (e) {
                            console.error('[HeyMax SubCaps Viewer] Error processing fetch response:', e);
                        }
                    }
                    
                    return response;
                };
            }
        }
    }, 5000);

    // ============================================================================
    // UI COMPONENTS
    // ============================================================================

    // Extract card ID from URL
    function extractCardIdFromUrl() {
        const match = window.location.pathname.match(/\/cards\/your-cards\/([a-f0-9]+)/);
        return match ? match[1] : null;
    }

    // Calculate buckets from transaction data
    function calculateBuckets(apiResponse, cardShortName = 'UOB PPV') {
        const ppvShoppingMcc = [4816, 5262, 5306, 5309, 5310, 5311, 5331, 5399, 5611, 5621, 5631, 5641, 5651, 5661, 5691, 5699, 5732, 5733, 5734, 5735, 5912, 5942, 5944, 5945, 5946, 5947, 5948, 5949, 5964, 5965, 5966, 5967, 5968, 5969, 5970, 5992, 5999];
        const ppvDiningMcc = [5811, 5812, 5814, 5333, 5411, 5441, 5462, 5499, 8012, 9751];
        const ppvEntertainmentMcc = [7278, 7832, 7841, 7922, 7991, 7996, 7998, 7999];
        
        const blacklistMcc = [4829, 4900, 5199, 5960, 5965, 5993, 6012, 6050, 6051, 6211, 6300, 6513, 6529, 6530, 6534, 6540, 7349, 7511, 7523, 7995, 8062, 8211, 8220, 8241, 8244, 8249, 8299, 8398, 8661, 8651, 8699, 8999, 9211, 9222, 9223, 9311, 9402, 9405, 9399];
        
        const blacklistMerchantPrefixes = [
            "AXS", "AMAZE", "AMAZE* TRANSIT", "BANC DE BINARY", "BANCDEBINARY.COM",
            "EZ LINK PTE LTD (FEVO)", "EZ Link transport", "EZ Link", "EZ-LINK (IMAGINE CARD)",
            "EZ-Link EZ-Reload (ATU)", "EZLINK", "EzLink", "EZ-LINK", "FlashPay ATU",
            "MB * MONEYBOOKERS.COM", "NETS VCASHCARD", "OANDA ASIA PAC", "OANDAASIAPA",
            "PAYPAL * BIZCONSULTA", "PAYPAL * CAPITALROYA", "PAYPAL * OANDAASIAPA",
            "Saxo Cap Mkts Pte Ltd", "SKR*SKRILL.COM", "SKR*xglobalmarkets.com", "SKYFX.COM",
            "TRANSIT", "WWW.IGMARKETS.COM.SG", "IPAYMY", "RWS-LEVY", "SMOOVE PAY",
            "SINGPOST-SAM", "RazerPay", "NORWDS"
        ];
        
        const roundDownToNearestFive = (amount) => Math.floor(amount / 5) * 5;
        
        const isBlacklisted = (transaction) => {
            const mccCode = parseInt(transaction.mcc_code, 10);
            if (blacklistMcc.includes(mccCode)) {
                return true;
            }
            
            if (transaction.merchant_name) {
                for (const prefix of blacklistMerchantPrefixes) {
                    if (transaction.merchant_name.startsWith(prefix)) {
                        return true;
                    }
                }
            }
            
            return false;
        };

        let contactlessBucket = 0;
        let onlineBucket = 0;
        let foreignCurrencyBucket = 0;

        if (cardShortName === 'UOB VS') {
            apiResponse.forEach((transactionObj) => {
                const transaction = transactionObj.transaction;
                
                if (isBlacklisted(transaction)) {
                    return;
                }

                if (transaction.original_currency && transaction.original_currency !== 'SGD') {
                    foreignCurrencyBucket += transaction.base_currency_amount;
                } else if (transaction.payment_tag === 'contactless') {
                    contactlessBucket += transaction.base_currency_amount;
                }
            });

            return { contactless: contactlessBucket, foreignCurrency: foreignCurrencyBucket };
        } else {
            apiResponse.forEach((transactionObj) => {
                const transaction = transactionObj.transaction;
                
                if (isBlacklisted(transaction)) {
                    return;
                }

                if (transaction.payment_tag === 'contactless') {
                    contactlessBucket += roundDownToNearestFive(transaction.base_currency_amount);
                } else if (transaction.payment_tag === 'online') {
                    const mccCode = parseInt(transaction.mcc_code, 10);
                    if (ppvShoppingMcc.includes(mccCode) || ppvDiningMcc.includes(mccCode) || ppvEntertainmentMcc.includes(mccCode)) {
                        onlineBucket += roundDownToNearestFive(transaction.base_currency_amount);
                    }
                }
            });

            return { contactless: contactlessBucket, online: onlineBucket };
        }
    }

    // Check if button should be visible
    function shouldShowButton(cardId) {
        const cardDataStr = GM_getValue('cardData', '{}');
        const cardData = JSON.parse(cardDataStr);

        console.log('[HeyMax SubCaps Viewer] Checking visibility - cardData:', cardData);
        console.log('[HeyMax SubCaps Viewer] Checking visibility - cardId:', cardId);

        if (!cardData || !cardId) {
            console.log('[HeyMax SubCaps Viewer] No cardData or cardId, hiding button');
            return false;
        }

        const cardInfo = cardData[cardId];
        console.log('[HeyMax SubCaps Viewer] Card info exists:', !!cardInfo);

        if (!cardInfo || !cardInfo.card_tracker) {
            console.log('[HeyMax SubCaps Viewer] No card info or card_tracker, hiding button');
            return false;
        }

        const cardTrackerData = cardInfo.card_tracker.data;
        console.log('[HeyMax SubCaps Viewer] Card tracker data exists:', !!cardTrackerData);

        if (!cardTrackerData || !cardTrackerData.card) {
            console.log('[HeyMax SubCaps Viewer] No card tracker data or card object, hiding button');
            return false;
        }

        const shortName = cardTrackerData.card.short_name;
        console.log('[HeyMax SubCaps Viewer] Card short_name:', shortName);
        const isSupportedCard = shortName === 'UOB PPV' || shortName === 'UOB VS';
        console.log('[HeyMax SubCaps Viewer] Is supported card:', isSupportedCard);
        return isSupportedCard;
    }

    // Create the SubCaps button
    function createButton() {
        const button = document.createElement('button');
        button.id = 'heymax-subcaps-button';
        button.textContent = 'Subcaps';
        button.style.cssText = `
            position: fixed;
            bottom: 20px;
            right: 20px;
            padding: 12px 24px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 8px;
            font-size: 16px;
            font-weight: bold;
            cursor: pointer;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
            z-index: 10000;
            transition: all 0.3s ease;
            display: none;
        `;

        button.addEventListener('mouseenter', function() {
            button.style.backgroundColor = '#45a049';
            button.style.transform = 'scale(1.05)';
        });

        button.addEventListener('mouseleave', function() {
            button.style.backgroundColor = '#4CAF50';
            button.style.transform = 'scale(1)';
        });

        button.addEventListener('click', function() {
            showOverlay();
        });

        return button;
    }

    // Create the overlay
    function createOverlay() {
        const overlay = document.createElement('div');
        overlay.id = 'heymax-subcaps-overlay';
        overlay.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.7);
            z-index: 10001;
            display: none;
            justify-content: center;
            align-items: center;
        `;

        const content = document.createElement('div');
        content.style.cssText = `
            background-color: white;
            padding: 30px;
            border-radius: 12px;
            max-width: 600px;
            width: 90%;
            max-height: 80vh;
            overflow-y: auto;
            position: relative;
            box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
        `;

        const closeButton = document.createElement('button');
        closeButton.textContent = '×';
        closeButton.style.cssText = `
            position: absolute;
            top: 10px;
            right: 15px;
            background: none;
            border: none;
            font-size: 32px;
            font-weight: bold;
            cursor: pointer;
            color: #666;
            line-height: 1;
            padding: 0;
            width: 32px;
            height: 32px;
            transition: color 0.3s ease;
        `;

        closeButton.addEventListener('mouseenter', function() {
            closeButton.style.color = '#000';
        });

        closeButton.addEventListener('mouseleave', function() {
            closeButton.style.color = '#666';
        });

        closeButton.addEventListener('click', function() {
            hideOverlay();
        });

        const title = document.createElement('h2');
        title.id = 'heymax-subcaps-title';
        title.textContent = 'Subcaps Analysis';
        title.style.cssText = `
            margin-top: 0;
            margin-bottom: 20px;
            color: #333;
            font-size: 24px;
        `;

        const resultsDiv = document.createElement('div');
        resultsDiv.id = 'heymax-subcaps-results';

        content.appendChild(closeButton);
        content.appendChild(title);
        content.appendChild(resultsDiv);
        overlay.appendChild(content);

        overlay.addEventListener('click', function(e) {
            if (e.target === overlay) {
                hideOverlay();
            }
        });

        return overlay;
    }

    // Show overlay with calculated data
    function showOverlay() {
        const overlay = document.getElementById('heymax-subcaps-overlay');
        const resultsDiv = document.getElementById('heymax-subcaps-results');
        const titleElement = document.getElementById('heymax-subcaps-title');

        if (!overlay || !resultsDiv) {
            console.error('[HeyMax SubCaps Viewer] Overlay elements not found');
            return;
        }

        resultsDiv.innerHTML = '<p style="text-align: center; color: #666;">Loading data...</p>';
        overlay.style.display = 'flex';

        const cardId = extractCardIdFromUrl();
        const cardDataStr = GM_getValue('cardData', '{}');
        const cardData = JSON.parse(cardDataStr);

        console.log('[HeyMax SubCaps Viewer] showOverlay - cardData:', cardData);
        console.log('[HeyMax SubCaps Viewer] showOverlay - cardId:', cardId);

        if (!cardData || !cardId || !cardData[cardId]) {
            resultsDiv.innerHTML = '<p style="color: #f44336;">Error: No card data found</p>';
            return;
        }

        const transactionsData = cardData[cardId].transactions;
        if (!transactionsData || !transactionsData.data) {
            resultsDiv.innerHTML = '<p style="color: #f44336;">Error: No transaction data available</p>';
            return;
        }

        const cardTrackerData = cardData[cardId].card_tracker;
        const cardShortName = cardTrackerData && cardTrackerData.data && cardTrackerData.data.card
            ? cardTrackerData.data.card.short_name
            : 'UOB PPV';

        if (titleElement) {
            titleElement.textContent = `${cardShortName} Subcaps Analysis`;
        }

        try {
            const transactions = transactionsData.data;
            const results = calculateBuckets(transactions, cardShortName);

            displayResults(results, transactions.length, cardShortName);
        } catch (error) {
            console.error('[HeyMax SubCaps Viewer] Error calculating data:', error);
            resultsDiv.innerHTML = '<p style="color: #f44336;">Error calculating data: ' + error.message + '</p>';
        }
    }

    // Hide overlay
    function hideOverlay() {
        const overlay = document.getElementById('heymax-subcaps-overlay');
        if (overlay) {
            overlay.style.display = 'none';
        }
    }

    // Display calculation results
    function displayResults(results, transactionCount, cardShortName = 'UOB PPV') {
        const resultsDiv = document.getElementById('heymax-subcaps-results');
        if (!resultsDiv) return;

        // Helper function to determine color based on value and card type
        function getValueColor(value, bucketType, cardType) {
            if (cardType === 'UOB VS') {
                // For UOB VS: yellow < 1000, green 1000-1200, red > 1200
                if (value < 1000) return '#FFC107'; // Yellow
                if (value <= 1200) return '#4CAF50'; // Green
                return '#f44336'; // Red
            } else {
                // For UOB PPV: green < 600, red >= 600
                if (value < 600) return '#4CAF50'; // Green
                return '#f44336'; // Red
            }
        }

        const contactlessColor = getValueColor(results.contactless, 'contactless', cardShortName);
        const contactlessLimit = cardShortName === 'UOB VS' ? '1200' : '600';

        let html = `
            <div style="margin-bottom: 20px;">
                <p style="color: #666; font-size: 14px; margin-bottom: 15px;">
                    Analyzed ${transactionCount} transaction${transactionCount !== 1 ? 's' : ''}
                </p>
            </div>

            <div style="background-color: #f5f5f5; padding: 20px; border-radius: 8px; margin-bottom: 15px;">
                <h3 style="margin-top: 0; color: #333; font-size: 18px;">Contactless Bucket</h3>
                <p style="font-size: 32px; font-weight: bold; margin: 10px 0;">
                    <span style="color: ${contactlessColor};">$${results.contactless.toFixed(2)}</span>
                    <span style="color: #333;"> / $${contactlessLimit}</span>
                </p>
                <p style="color: #666; font-size: 14px; margin-bottom: 0;">
                    Total from contactless payments${cardShortName === 'UOB PPV' ? ' (rounded down to nearest $5)' : ''}
                </p>
                ${cardShortName === 'UOB VS' && results.contactless < 1000 ? `
                <p style="color: #F57C00; font-size: 14px; margin-top: 10px; margin-bottom: 0; font-weight: 500;">
                    To start earning bonus miles, you must spend at least $1,000 in this category.
                </p>
                ` : ''}
            </div>
        `;

        if (cardShortName === 'UOB VS') {
            const foreignCurrencyColor = getValueColor(results.foreignCurrency, 'foreignCurrency', cardShortName);
            html += `
                <div style="background-color: #f5f5f5; padding: 20px; border-radius: 8px;">
                    <h3 style="margin-top: 0; color: #333; font-size: 18px;">Foreign Currency Bucket</h3>
                    <p style="font-size: 32px; font-weight: bold; margin: 10px 0;">
                        <span style="color: ${foreignCurrencyColor};">$${results.foreignCurrency.toFixed(2)}</span>
                        <span style="color: #333;"> / $1200</span>
                    </p>
                    <p style="color: #666; font-size: 14px; margin-bottom: 0;">
                        Total from non-SGD transactions
                    </p>
                    ${results.foreignCurrency < 1000 ? `
                    <p style="color: #F57C00; font-size: 14px; margin-top: 10px; margin-bottom: 0; font-weight: 500;">
                        To start earning bonus miles, you must spend at least $1,000 in this category.
                    </p>
                    ` : ''}
                </div>
            `;
        } else {
            const onlineColor = getValueColor(results.online, 'online', cardShortName);
            html += `
                <div style="background-color: #f5f5f5; padding: 20px; border-radius: 8px;">
                    <h3 style="margin-top: 0; color: #333; font-size: 18px;">Online Bucket</h3>
                    <p style="font-size: 32px; font-weight: bold; margin: 10px 0;">
                        <span style="color: ${onlineColor};">$${results.online.toFixed(2)}</span>
                        <span style="color: #333;"> / $600</span>
                    </p>
                    <p style="color: #666; font-size: 14px; margin-bottom: 0;">
                        Total from eligible online transactions (rounded down to nearest $5)
                    </p>
                </div>
            `;
        }

        html += `
            <div style="margin-top: 20px; padding: 15px; background-color: #e3f2fd; border-radius: 8px;">
                <p style="margin: 0; font-size: 14px; color: #1976D2;">
                    <strong>Note:</strong> These calculations are based on the transaction data that has been loaded so far.
                </p>
            </div>
        `;

        resultsDiv.innerHTML = html;
    }

    // Update button visibility
    function updateButtonVisibility() {
        const button = document.getElementById('heymax-subcaps-button');
        if (!button) return;

        const cardId = extractCardIdFromUrl();
        console.log('[HeyMax SubCaps Viewer] Extracted card ID:', cardId);

        if (cardId) {
            const shouldShow = shouldShowButton(cardId);
            console.log('[HeyMax SubCaps Viewer] Should show button:', shouldShow);
            if (shouldShow) {
                button.style.display = 'block';
                console.log('[HeyMax SubCaps Viewer] SubCaps button displayed for supported card');
            } else {
                button.style.display = 'none';
                console.log('[HeyMax SubCaps Viewer] SubCaps button hidden (conditions not met)');
            }
        } else {
            button.style.display = 'none';
            console.log('[HeyMax SubCaps Viewer] No card ID found in URL, button will remain hidden');
        }
    }

    // Initialize UI
    function initializeUI() {
        if (!document.body) {
            console.log('[HeyMax SubCaps Viewer] document.body not ready, waiting...');
            setTimeout(initializeUI, 100);
            return;
        }

        console.log('[HeyMax SubCaps Viewer] Initializing UI components...');

        const button = createButton();
        document.body.appendChild(button);
        console.log('[HeyMax SubCaps Viewer] Button element created and appended');

        const overlay = createOverlay();
        document.body.appendChild(overlay);
        console.log('[HeyMax SubCaps Viewer] Overlay element created and appended');

        updateButtonVisibility();

        let lastUrl = window.location.href;
        let debounceTimer = null;
        const observer = new MutationObserver(function(mutations) {
            const hasSignificantChange = mutations.some(mutation =>
                mutation.type === 'childList' && mutation.addedNodes.length > 0
            );

            if (!hasSignificantChange) {
                return;
            }

            if (debounceTimer) {
                clearTimeout(debounceTimer);
            }

            debounceTimer = setTimeout(function() {
                if (window.location.href !== lastUrl) {
                    lastUrl = window.location.href;
                    updateButtonVisibility();
                }
            }, 250);
        });

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

        // Also periodically check for button visibility (in case storage updates)
        setInterval(updateButtonVisibility, 2000);
    }

    // Wait for DOM to be ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initializeUI);
    } else {
        initializeUI();
    }

    console.log('[HeyMax SubCaps Viewer] Tampermonkey script initialized');
})();