Steam Badge Enhancer

Enhances Steam badges with detailed data, crafted highlight, IndexedDB for local caching, immediate cached display, and optional manual re-queue button. Seamless data hot-swapping during background refresh.

// ==UserScript==
// @name         Steam Badge Enhancer
// @namespace    https://github.com/encumber
// @version      2.1
// @description  Enhances Steam badges with detailed data, crafted highlight, IndexedDB for local caching, immediate cached display, and optional manual re-queue button. Seamless data hot-swapping during background refresh.
// @author       Nitoned
// @match        https://steamcommunity.com/*/badges/*
// @match        https://steamcommunity.com/*/badges*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const STEAMSETS_API_KEY = ''; // Get api key from https://steamsets.com/settings/developer-apps
    const STEAMSETS_API_URL = 'https://api.steamsets.com/v1/app.listBadges';
    const STEAM_BADGE_INFO_URL = 'https://steamcommunity.com/my/ajaxgetbadgeinfo/';
    const STEAMSETS_API_CALL_DELAY_MS = 1000; // 1 second delay before Steamsets API calls
    const STEAM_API_CALL_DELAY_MS = 200; // 200ms delay between Steam's ajaxgetbadgeinfo calls
    const CACHE_EXPIRATION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds
    const SCRIPT_TOGGLE_KEY = 'steamBadgeEnhancerEnabled'; // Key for the main script toggle
    const REQUEUE_BUTTON_TOGGLE_KEY = 'steamBadgeRequeueButtonEnabled'; // Key for the re-queue button toggle

    // IndexedDB Configuration
    const DB_NAME = 'SteamBadgeCacheDB';
    const DB_VERSION = 1; // Increment this if you change the database structure
    const STORE_NAME = 'badgeCache';
    // --- End Configuration ---

    // Get the current state of the toggle settings
    let isScriptEnabled = GM_getValue(SCRIPT_TOGGLE_KEY, true); // Default script enabled to true
    let isRequeueButtonEnabled = GM_getValue(REQUEUE_BUTTON_TOGGLE_KEY, true); // Default re-queue button enabled to true
     // Variable to hold the IndexedDB database instance
    let db = null;

    // --- IndexedDB Functions ---

    function openDatabase() {
        return new Promise((resolve, reject) => {
            const request = indexedDB.open(DB_NAME, DB_VERSION);

            request.onerror = (event) => {
                console.error("IndexedDB database error:", event.target.error);
                reject(event.target.error);
            };

            request.onupgradeneeded = (event) => {
                const db = event.target.result;
                 // Create the object store if it doesn't exist
                if (!db.objectStoreNames.contains(STORE_NAME)) {
                    db.createObjectStore(STORE_NAME, { keyPath: 'appId' });
                    console.log(`IndexedDB object store "${STORE_NAME}" created.`);
                }
                // Future versions could add indexes here if needed for querying
            };

            request.onsuccess = (event) => {
                db = event.target.result;
                console.log("IndexedDB database opened successfully.");
                resolve(db);
            };
        });
    }

     async function getCacheEntry(appId) {
         if (!db) {
             console.warn("IndexedDB not initialized. Cannot get cache entry.");
             return null;
         }

         return new Promise((resolve, reject) => {
             const transaction = db.transaction([STORE_NAME], 'readonly');
             const store = transaction.objectStore(STORE_NAME);
             const request = store.get(appId);

             request.onerror = (event) => {
                 console.error(`Error getting cache entry for appId ${appId} from IndexedDB:`, event.target.error);
                 resolve(null); // Resolve with null on error
             };

             request.onsuccess = (event) => {
                 const cachedEntry = event.target.result;
                 // console.log(`IndexedDB get success for appId ${appId}:`, cachedEntry); // Too chatty
                 resolve(cachedEntry);
             };
         });
     }

     async function setCacheEntry(appId, cacheEntry) {
         if (!db) {
             console.warn("IndexedDB not initialized. Cannot set cache entry.");
             return false;
         }

         return new Promise((resolve, reject) => {
             const transaction = db.transaction([STORE_NAME], 'readwrite');
             const store = transaction.objectStore(STORE_NAME);

             // Add the appId to the object since it's the keyPath
             cacheEntry.appId = appId;

             const request = store.put(cacheEntry); // put() adds or updates

             request.onerror = (event) => {
                 console.error(`Error setting cache entry for appId ${appId} in IndexedDB:`, event.target.error);
                 resolve(false); // Resolve with false on error
             };

             request.onsuccess = (event) => {
                 // console.log(`IndexedDB set success for appId ${appId}.`); // Too chatty
                 resolve(true); // Resolve with true on success
             };

             transaction.oncomplete = () => {
                 // console.log(`IndexedDB transaction complete for appId ${appId}.`); // Too chatty
                 // The resolve is already called in onsuccess
             };
         });
     }

     async function removeCacheEntry(appId) {
         if (!db) {
             console.warn("IndexedDB not initialized. Cannot remove cache entry.");
             return false;
         }

         return new Promise((resolve, reject) => {
             const transaction = db.transaction([STORE_NAME], 'readwrite');
             const store = transaction.objectStore(STORE_NAME);
             const request = store.delete(appId);

             request.onerror = (event) => {
                 console.error(`Error removing cache entry for appId ${appId} from IndexedDB:`, event.target.error);
                 resolve(false); // Resolve with false on error
             };

             request.onsuccess = (event) => {
                 console.log(`Removed cache entry for App ID: ${appId} from IndexedDB.`);
                 resolve(true); // Resolve with true on success
             };

              transaction.oncomplete = () => {
                 // console.log(`IndexedDB delete transaction complete for appId ${appId}.`); // Too chatty
                 // The resolve is already called in onsuccess
             };
         });
     }

     // --- End IndexedDB Functions ---


    // Add the toggle buttons
    function addToggleButtons() {
        const profileHeader = document.querySelector('.profile_header_actions, .profile_header_actions_secondary');
        if (!profileHeader) {
             console.warn("Could not find profile header actions to add toggle buttons.");
             return;
        }

        // Add Main Script Toggle Button
        const scriptToggleButton = document.createElement('div');
        scriptToggleButton.id = 'steam-badge-enhancer-toggle';
        scriptToggleButton.style.cssText = `
            display: inline-block;
            margin-left: 10px;
            padding: 5px 10px;
            background-color: ${isScriptEnabled ? '#5cb85c' : '#d9534f'}; /* Green for enabled, Red for disabled */
            color: white;
            border-radius: 3px;
            cursor: pointer;
            font-size: 12px;
            line-height: 1.2;
            user-select: none;
            margin-bottom: 5px; /* Space between buttons */
        `;
        scriptToggleButton.textContent = `Enhancer: ${isScriptEnabled ? 'Enabled' : 'Disabled'}`;
        scriptToggleButton.title = `Click to ${isScriptEnabled ? 'disable' : 'enable'} the Steam Badge Enhancer script.`;

        scriptToggleButton.addEventListener('click', () => {
            isScriptEnabled = !isScriptEnabled;
            GM_setValue(SCRIPT_TOGGLE_KEY, isScriptEnabled);
            scriptToggleButton.style.backgroundColor = isScriptEnabled ? '#5cb85c' : '#d9534f';
            scriptToggleButton.textContent = `Enhancer: ${isScriptEnabled ? 'Enabled' : 'Disabled'}`;
            scriptToggleButton.title = `Click to ${isScriptEnabled ? 'disable' : 'enable'} the Steam Badge Enhancer script.`;
            console.log(`Steam Badge Enhancer ${isScriptEnabled ? 'enabled' : 'disabled'}. Reload the page for changes to take full effect.`);
            // Reloading is recommended for the main toggle
        });

        profileHeader.appendChild(scriptToggleButton);

        // Add Re-queue Button Toggle Button
        const requeueToggle = document.createElement('div');
        requeueToggle.id = 'steam-badge-requeue-toggle';
         requeueToggle.style.cssText = `
            display: inline-block;
            margin-left: 10px;
            padding: 5px 10px;
            background-color: ${isRequeueButtonEnabled ? '#5cb85c' : '#d9534f'}; /* Green for enabled, Red for disabled */
            color: white;
            border-radius: 3px;
            cursor: pointer;
            font-size: 12px;
            line-height: 1.2;
            user-select: none;
             /* Position relative to allow margin-left */
             position: relative;
             top: 0px; /* Align with the first button if needed */
        `;
        requeueToggle.textContent = `Re-queue Button: ${isRequeueButtonEnabled ? 'Shown' : 'Hidden'}`;
        requeueToggle.title = `Click to ${isRequeueButtonEnabled ? 'hide' : 'show'} the Re-queue Data Fetch buttons.`;

        requeueToggle.addEventListener('click', () => {
            isRequeueButtonEnabled = !isRequeueButtonEnabled;
            GM_setValue(REQUEUE_BUTTON_TOGGLE_KEY, isRequeueButtonEnabled);
            requeueToggle.style.backgroundColor = isRequeueButtonEnabled ? '#5cb85c' : '#d9534f';
            requeueToggle.textContent = `Re-queue Button: ${isRequeueButtonEnabled ? 'Shown' : 'Hidden'}`;
            requeueToggle.title = `Click to ${isRequeueButtonEnabled ? 'hide' : 'show'} the Re-queue Data Fetch buttons.`;
            console.log(`Re-queue buttons ${isRequeueButtonEnabled ? 'shown' : 'hidden'}.`);

            // Immediately hide/show buttons without requiring a reload
            const buttons = document.querySelectorAll('.requeue_button');
            buttons.forEach(button => {
                button.style.display = isRequeueButtonEnabled ? '' : 'none';
            });
        });

         // Find the parent of the first button and insert the second button after it
         if (scriptToggleButton.parentNode) {
              scriptToggleButton.parentNode.insertBefore(requeueToggle, scriptToggleButton.nextSibling);
         } else {
              // Fallback if parent not found (less likely)
              profileHeader.appendChild(requeueToggle);
         }


         console.log("Steam Badge Enhancer toggle buttons added.");
    }


    // Add script styles
    GM_addStyle(`
        /* Style for the container holding the appended badge details */
        .enhanced_badge_details_container {
            display: flex;
            flex-wrap: wrap;
            justify-content: center; /* Center the individual badges */
            align-items: flex-start; /* Align items to the top */
            margin-top: 10px; /* Space between original content and new content */
            padding-top: 10px;
            border-top: 1px solid #303030; /* Separator line */
            width: 100%; /* Take full width of the badge row */
             min-height: 80px; /* Ensure container has some height even when empty or loading */
             position: relative; /* Needed for absolute positioned loading indicators */
        }

        .badge_info_container {
            display: flex;
            flex-direction: column;
            align-items: center;
            margin: 5px 15px; /* Increased horizontal margin (left and right) */
            text-align: center;
            width: 120px; /* Increased width of each badge container */
            flex-shrink: 0; /* Prevent shrinking */
             position: relative; /* Needed for highlight pseudo-element */
        }

        .badge_image {
            width: 64px; /* Adjust image size as needed */
            height: 64px; /* Adjust image size as needed */
            object-fit: contain;
            margin-bottom: 5px;
        }

        .badge_name {
            font-weight: bold;
            margin-bottom: 2px;
            font-size: 0.8em; /* Adjust font size for name */
            /* Added text truncation styles */
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            max-width: 100%; /* Ensure it respects the container width */
        }

        .badge_xp, .badge_scarcity, .badge_completion_date, .badge_level_display { /* Added .badge_level_display */
            font-size: 0.8em; /* Adjust font size for these elements */
            color: #8f98a0; /* Adjust secondary text color */
        }

        .badge_completion_date {
             font-size: 0.75em; /* Slightly smaller font for the date */
             white-space: nowrap; /* Prevent wrapping for the date */
             overflow: hidden; /* Hide overflow */
             text-overflow: ellipsis; /* Show ellipsis if still overflows */
             max-width: 100%; /* Ensure it respects the container width */
        }

        /* Style for highlighting the crafted badge */
        .badge_info_container.crafted {
            box-shadow: 0 0 8px 2px rgb(154 155 255 / 50%);
            border: 1px solid #8e8e95;
            padding: 0px 5px 15px 5px;
        }

        /* Style for the re-queue button */
        .requeue_button {
            padding: 3px 8px;
            max-width: 15px;
            background-color: #22558f;
            color: #ffffff;
            border: 1px solid #505050;
            border-radius: 3px;
            cursor: pointer;
            font-size: 1.3em;
            text-align: center;
             /* IMPORTANT: Stop click event propagation */
            z-index: 99999; /* Increased z-index */
            position: relative; /* Needed for z-index to work */
        }

        .requeue_button:hover {
            background-color: #404040;
        }

        /* Style for the initial loading/cached indicator */
         .initial_indicator {
             color: #8f98a0;
             font-style: italic;
             margin: 10px;
             width: 100%;
             display: block;
             text-align: center;
         }

         /* Style for the loading overlay */
         .enhancer_loading_overlay {
              position: absolute;
              top: 0;
              left: 0;
              right: 0;
              bottom: 0;
              background-color: rgba(0, 0, 0, 0.7); /* Semi-transparent dark overlay */
              display: flex;
              justify-content: center;
              align-items: center;
              color: white;
              font-size: 1.2em;
              z-index: -10; /* Above badge details but below the container */
         }

    `);

    function extractAppIdFromBadgeLink(badgeRow) {
        const badgeLink = badgeRow.querySelector('a.badge_row_overlay');
        if (badgeLink) {
            const match = badgeLink.href.match(/\/gamecards\/(\d+)\//);
            if (match && match[1]) {
                return parseInt(match[1], 10);
            }
        }

        // Fallback if primary link not found or doesn't have appid
        const badgeImageLink = badgeRow.querySelector('.badge_info_image a'); // Link around the badge image
        if (badgeImageLink) {
            const steamStoreLinkMatch = badgeImageLink.href.match(/\/app\/(\d+)\//);
            if (steamStoreLinkMatch && steamStoreLinkMatch[1]) {
                return parseInt(steamStoreLinkMatch[1], 10);
            }
        }

        return null;
    }

    async function getBadgeData(appId) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'POST',
                url: STEAMSETS_API_URL,
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${STEAMSETS_API_KEY}` // Use the single API key
                },
                data: JSON.stringify({ appId: appId }),
                onload: function(response) {
                    try {
                        const data = JSON.parse(response.responseText);

                        if (data && Array.isArray(data.badges)) {
                            console.log(`Successfully fetched Steamsets badge data for appId ${appId}.`);
                            resolve(data.badges);
                        } else {
                            console.error(`Steamsets API response for appId ${appId} did not contain a valid 'badges' array. Response data:`, data);
                            resolve([]); // Resolve with empty array if data is not as expected
                        }
                    } catch (e) {
                        console.error(`Error parsing Steamsets API response for appId ${appId}:`, e);
                        resolve([]); // Resolve with empty array on parsing error
                    }
                },
                onerror: function(error) {
                    console.error(`GM_xmlhttpRequest error for appId ${appId}:`, error);
                    resolve([]); // Resolve with empty array on request error
                }
            });
        });
    }

     async function getCraftedBadgeInfo(appId, isFoil = false) {
         return new Promise((resolve, reject) => {
             let url = `${STEAM_BADGE_INFO_URL}${appId}`;
             if (isFoil) {
                 url += '?border=1'; // Add parameter for foil badge info
             }

             GM_xmlhttpRequest({
                 method: 'GET',
                 url: url,
                 onload: function(response) {
                     try {
                         const data = JSON.parse(response.responseText); // Parse the JSON response

                         let craftedLevel = 0;
                         let isCrafted = false;

                         // Check if badgedata and level exist in the JSON response
                         if (data && data.badgedata && typeof data.badgedata.level === 'number') {
                              craftedLevel = data.badgedata.level;
                              // Consider it crafted if the level is greater than 0
                              isCrafted = craftedLevel > 0;
                         }

                         // console.log(`Fetched crafted info for appId ${appId} (Foil: ${isFoil}): Crafted Level = ${craftedLevel}, Is Crafted = ${isCrafted}`); // Too chatty

                         // Return the crafted level and whether a badge (at any level) is crafted
                         resolve({ craftedLevel: craftedLevel, isCrafted: isCrafted });

                     } catch (e) {
                         console.error(`Error parsing Steam badge info JSON for appId ${appId} (Foil: ${isFoil}):`, e);
                         resolve({ craftedLevel: 0, isCrafted: false }); // Resolve with default values on error
                     }
                 },
                 onerror: function(error) {
                     console.error(`GM_xmlhttpRequest error fetching Steam badge info for appId ${appId} (Foil: ${isFoil}):`, error);
                     resolve({ craftedLevel: 0, isCrafted: false }); // Resolve with default values on error
                 }
             });
         });
     }


    // Helper function for introducing a delay
    function delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

     // Helper function to format date for display
     function formatDateForDisplay(dateString) {
         try {
             const date = new Date(dateString);
             if (isNaN(date.getTime())) {
                 return 'Date unavailable';
             }

             const options = {
                 month: 'short', // 'Jan', 'Feb', etc.
                 day: 'numeric',
                 year: 'numeric',
                 hour: 'numeric', // 12-hour format
                 minute: 'numeric',
                 hour12: true // Use 12-hour format with AM/PM
             };

             // Use locale-specific formatting, then replace commas for the desired format
             let formatted = date.toLocaleString(undefined, options);

             // The default toLocaleString with 'short' month and numeric day/year
             // often results in "Month Day, Year, HH:MM AM/PM".
             // We'll return this format directly.

             return formatted;

         } catch (e) {
             console.error("Error formatting date:", e);
             return 'Date Formatting Error';
         }
     }


    // --- IndexedDB Caching Functions (Replacing Local Storage) ---

    function isCacheValid(cachedEntry) {
        if (!cachedEntry) {
            return false; // Entry doesn't exist
        }
        // Check for basic structure required
        // Note: The appId is now part of the stored object due to keyPath
        if (typeof cachedEntry.timestamp !== 'number' ||
            !cachedEntry.steamsetsData ||
            typeof cachedEntry.craftedNormalInfo !== 'object' ||
            typeof cachedEntry.craftedFoilInfo !== 'object' ||
             typeof cachedEntry.appId !== 'number') { // Check for appId as well
             console.log("Cache entry has invalid structure:", cachedEntry);
            return false; // Invalid structure
        }

        const now = Date.now();
        const isValid = (now - cachedEntry.timestamp) < CACHE_EXPIRATION_MS;
        return isValid;
    }


    // Map to store badge rows grouped by App ID (needed globally for re-queue)
    const badgeRowsByAppId = new Map();
     // Queue for App IDs to process (fetch new data)
    const fetchQueue = [];
     // Flag to prevent multiple fetch loops running simultaneously
    let isFetching = false;
     // Set to track App IDs currently being fetched
    const currentlyFetching = new Set();


     // Function to add an App ID to the fetch queue
     function queueAppIdForFetch(appId) {
         // Only add if not already in the queue or being processed
         if (!fetchQueue.includes(appId) && !currentlyFetching.has(appId)) {
              fetchQueue.push(appId);
              console.log(`App ID ${appId} added to fetch queue. Queue size: ${fetchQueue.length}`);
              // If not already fetching, start the fetch loop
              if (!isFetching) {
                  processFetchQueue();
              }
         } else {
              // console.log(`App ID ${appId} is already in the fetch queue or being fetched.`); // Too chatty
         }
     }

    // Main function to process the fetch queue
    async function processFetchQueue() {
        if (isFetching || fetchQueue.length === 0) {
            return; // Don't start if already fetching or queue is empty
        }

        isFetching = true;
        console.log("Starting fetch queue processing.");

        while (fetchQueue.length > 0) {
            const appId = fetchQueue[0]; // Peek at the next App ID

            // Ensure the App ID is added to the currentlyFetching set before processing
            currentlyFetching.add(appId);

            const rowsForApp = badgeRowsByAppId.get(appId) || [];

            if (rowsForApp.length === 0) {
                console.log(`No badge rows found for App ID ${appId} in badgeRowsByAppId map for fetch. Removing from queue.`);
                 fetchQueue.shift(); // Remove the item if no rows found
                 currentlyFetching.delete(appId); // Remove from fetching set
                continue; // Skip
            }

            console.log(`Processing App ID ${appId} from fetch queue.`);

             // Add loading indicator overlay to each row
            rowsForApp.forEach(badgeRow => {
                 let enhancedBadgeDetailsContainer = badgeRow.querySelector('.enhanced_badge_details_container');
                 if (!enhancedBadgeDetailsContainer) {
                     console.error(`Container not found for App ID ${appId} during fetch process.`);
                     return; // Skip this row if container is missing (shouldn't happen after initialSetup)
                 }

                 // Check if an overlay already exists
                 let loadingOverlay = enhancedBadgeDetailsContainer.querySelector('.enhancer_loading_overlay');
                 if (!loadingOverlay) {
                     loadingOverlay = document.createElement('div');
                     loadingOverlay.classList.add('enhancer_loading_overlay');
                     loadingOverlay.textContent = "Updating data...";
                     enhancedBadgeDetailsContainer.appendChild(loadingOverlay);
                 } else {
                     loadingOverlay.textContent = "Updating data..."; // Update text if it exists
                     loadingOverlay.style.display = 'flex'; // Ensure it's visible
                 }
            });


            // --- Fetch Steamsets data with 1-second delay ---
            await delay(STEAMSETS_API_CALL_DELAY_MS);
            const badgeData = await getBadgeData(appId);

            // --- Fetch Crafted Badge Info (Normal) with shorter delay ---
             await delay(STEAM_API_CALL_DELAY_MS);
             const craftedNormalInfo = await getCraftedBadgeInfo(appId, false);

             // --- Fetch Crafted Badge Info (Foil) with shorter delay ---
             await delay(STEAM_API_CALL_DELAY_MS);
             const craftedFoilInfo = await getCraftedBadgeInfo(appId, true);

             // Prepare cache entry
             const cacheEntry = {
                 timestamp: Date.now(),
                 steamsetsData: badgeData,
                 craftedNormalInfo: craftedNormalInfo,
                 craftedFoilInfo: craftedFoilInfo
             };

            // Store fetched data in IndexedDB
            await setCacheEntry(appId, cacheEntry); // Use the new IndexedDB set function

             // Display the updated data *before* removing the overlay
            displayBadgeDetails(appId, rowsForApp, badgeData, craftedNormalInfo, craftedFoilInfo, false); // Displaying fresh data

            // Remove the loading overlay after displaying new data
             rowsForApp.forEach(badgeRow => {
                 const enhancedBadgeDetailsContainer = badgeRow.querySelector('.enhanced_badge_details_container');
                 const loadingOverlay = enhancedBadgeDetailsContainer ? enhancedBadgeDetailsContainer.querySelector('.enhancer_loading_overlay') : null;
                 if (loadingOverlay) {
                     loadingOverlay.remove(); // Remove the overlay element
                 }
             });


             fetchQueue.shift(); // Remove the item from the queue AFTER successful fetch and cache
             currentlyFetching.delete(appId); // Remove from fetching set

        }

        isFetching = false;
        console.log("Finished fetch queue processing.");
    }


    // Function to display badge details (extracted for reusability)
    function displayBadgeDetails(appId, rowsForApp, badgeData, craftedNormalInfo, craftedFoilInfo, isCached) {
        console.log(`Displaying details for App ID ${appId}. Data is cached: ${isCached}`);

        // Find the existing container (created in initialSetup or processFetchQueue)
        rowsForApp.forEach(badgeRow => {
             const container = badgeRow.querySelector('.enhanced_badge_details_container');
             if(container) {
                 // Keep the container, but replace its *content* (excluding the overlay if it exists)
                 const loadingOverlay = container.querySelector('.enhancer_loading_overlay'); // Find existing overlay

                 // Create a temporary container to hold the new badge details HTML
                 const tempDiv = document.createElement('div');

                 if (badgeData.length > 0) {
                     // Sort badges: Levels 1-5 (non-foil) then Foil
                     const sortedBadges = badgeData.sort((a, b) => {
                         if (a.isFoil === b.isFoil) {
                             return a.baseLevel - b.baseLevel; // Sort by level if same foil status
                         }
                         return a.isFoil ? 1 : -1; // Foil comes after non-foil
                     });

                     // Create the HTML content for the detailed badges
                     const detailedBadgesHtml = sortedBadges.map(badge => {
                         const formattedCompletionDate = badge.firstCompletion ? formatDateForDisplay(badge.firstCompletion) : 'Date unavailable';

                         // Determine if this badge level should be highlighted
                         let isCraftedHighlight = false;
                         if (!badge.isFoil && craftedNormalInfo.isCrafted && craftedNormalInfo.craftedLevel === badge.baseLevel) {
                             isCraftedHighlight = true;
                         } else if (badge.isFoil && craftedFoilInfo.isCrafted && craftedFoilInfo.craftedLevel === badge.baseLevel) {
                              isCraftedHighlight = true;
                         }

                         // Add 'crafted' class if it needs highlighting
                         const containerClass = isCraftedHighlight ? 'badge_info_container crafted' : 'badge_info_container';

                         return `
                             <div class="${containerClass}">
                                 <div class="badge_name" title="${badge.name}">${badge.name}</div>
                                 <img class="badge_image" src="https://cdn.fastly.steamstatic.com/steamcommunity/public/images/items/${appId}/${badge.badgeImage}" alt="${badge.name}">
                                 <div class="badge_completion_date">${formattedCompletionDate}</div> <!-- Added First Completion Date -->
                                 <div class="badge_scarcity">Scarcity: ${badge.scarcity}</div>
                                 <div class="badge_level_display">Level: ${badge.baseLevel}${badge.isFoil ? ' (Foil)' : ''}</div> <!-- Added Level Display -->
                             </div>
                         `;
                     }).join(''); // Join the array of HTML strings into a single string

                     tempDiv.innerHTML = detailedBadgesHtml;

                 } else {
                      console.log(`No detailed badge data found for appId ${appId}. Displaying "No data available".`);
                      // Display a message
                      const noDataMessage = document.createElement('div');
                      noDataMessage.textContent = `No detailed badge data available for this game (App ID: ${appId}).`;
                      noDataMessage.style.color = '#8f98a0';
                      noDataMessage.style.margin = '10px auto'; // Center the message
                      tempDiv.appendChild(noDataMessage);
                 }

                 // Add a visual indicator if data was from cache
                 if (isCached) {
                      const cachedIndicator = document.createElement('div');
                      cachedIndicator.textContent = `⠀`; // Text indicating cache
                      cachedIndicator.style.fontSize = '0.7em';
                      cachedIndicator.style.color = '#8f98a0';
                      cachedIndicator.style.textAlign = 'center';
                      cachedIndicator.style.marginTop = '5px';
                       // Prepend to the temporary container
                       tempDiv.insertBefore(cachedIndicator, tempDiv.firstChild);
                 }

                 // Replace the *content* of the main container with the content from tempDiv
                 // This avoids removing and re-adding the container itself or the overlay
                 while(container.firstChild && container.firstChild !== loadingOverlay) {
                     container.removeChild(container.firstChild);
                 }
                 while(tempDiv.firstChild) {
                     container.insertBefore(tempDiv.firstChild, loadingOverlay); // Insert before the overlay if it exists
                 }


                  // Add the re-queue button if enabled, ENSURING IT DOESN'T ALREADY EXIST
                  // This part remains the same as it's added to badge_title_stats, not the enhanced_badge_details_container
                     if (isRequeueButtonEnabled) {
                          const badgeTitleStatsContainer = badgeRow.querySelector('.badge_title_stats');
                          // Check if a re-queue button already exists within this container
                          const existingRequeueButton = badgeTitleStatsContainer ? badgeTitleStatsContainer.querySelector('.requeue_button') : null;

                          if (badgeTitleStatsContainer && !existingRequeueButton) { // Only add if container exists and button doesn't exist
                               const requeueButton = document.createElement('div');
                               requeueButton.classList.add('requeue_button');
                               requeueButton.textContent = ' ↻​ ';
                               // Store the appId on the button for easy access in the event listener
                               requeueButton.dataset.appId = appId;
                               badgeTitleStatsContainer.appendChild(requeueButton);

                               // Add click listener to the button
                               requeueButton.addEventListener('click', handleRequeueClick);
                          } else if (!badgeTitleStatsContainer) {
                               console.warn(`Could not find .badge_title_stats container for App ID ${appId} to add re-queue button.`);
                          }
                     }


             } else {
                 console.error(`Could not find .enhanced_badge_details_container for App ID ${appId} during display.`);
             }
        });
    }


     // Event handler for the re-queue button
     function handleRequeueClick(event) {
         event.preventDefault(); // Prevent default link behavior
         event.stopPropagation(); // *** IMPORTANT: Stop click event from bubbling up ***

         const appId = parseInt(event.target.dataset.appId, 10);
         if (!isNaN(appId)) {
             console.log(`Manual re-queue requested for App ID: ${appId}`);
             removeCacheEntry(appId); // Remove the old cache entry
             queueAppIdForFetch(appId); // Add to the fetch queue
         } else {
             console.error("Could not get App ID from re-queue button data.");
         }
     }

    // Function to delete elements with class 'badge_title_stats_drops' and 'badge_title_stats_playtime'
    function deleteBadgeStats() {
        const dropsElements = document.querySelectorAll('.badge_title_stats_drops');
        dropsElements.forEach(element => {
            element.remove();
            // console.log("Removed element with class 'badge_title_stats_drops'"); // Too chatty
        });
        const playtimeElements = document.querySelectorAll('.badge_title_stats_playtime');
         playtimeElements.forEach(element => {
             element.remove();
             // console.log("Removed element with class 'badge_title_stats_playtime'"); // Too chatty
         });
    }


    // Initial setup: Collect all badge rows, display cached, and queue uncached/expired
    async function initialSetup() { // Make initialSetup async because it awaits openDatabase and getCacheEntry

         // Add the toggle buttons first
         addToggleButtons();

         // Check if the script is enabled AFTER adding toggle buttons
         if (!isScriptEnabled) {
             console.log("Steam Badge Enhancer is disabled. Toggle buttons are available.");
              // Still delete the unnecessary stats elements even if enhancement is disabled
             deleteBadgeStats();
             return; // Exit the function if disabled
         }

        console.log("Steam Badge Enhancer: Initial setup.");

         // --- Initialize IndexedDB ---
         try {
             await openDatabase();
         } catch (error) {
             console.error("Failed to open IndexedDB. Caching will not be available.", error);
             // Decide how to proceed if IndexedDB fails. For now, we'll continue
             // but caching functions will log warnings.
         }
         // --- End IndexedDB Initialization ---

        // Delete the unnecessary stats elements immediately
        deleteBadgeStats();


        const allBadgeRows = document.querySelectorAll('.badge_row.is_link');

        // Group badge rows by App ID
        allBadgeRows.forEach(badgeRow => {
            const appId = extractAppIdFromBadgeLink(badgeRow);
            if (appId) {
                if (!badgeRowsByAppId.has(appId)) {
                    badgeRowsByAppId.set(appId, []);
                }
                badgeRowsByAppId.get(appId).push(badgeRow);
            } else {
                 console.warn("Could not extract App ID from a badge row:", badgeRow);
            }
        });

         console.log(`Initial setup found ${badgeRowsByAppId.size} unique App IDs.`);

         const appIdsOnPage = new Set(badgeRowsByAppId.keys()); // Get all App IDs found on the page

         // Process App IDs on the page
         for (const appId of appIdsOnPage) { // Use for...of loop to allow await inside
             const rowsForApp = badgeRowsByAppId.get(appId);
             if (!rowsForApp || rowsForApp.length === 0) {
                  console.warn(`No rows found for App ID ${appId} during initial setup.`);
                  continue; // Skip if no rows found
             }

             // Add the initial container to each row immediately
             rowsForApp.forEach(badgeRow => {
                 const enhancedBadgeDetailsContainer = document.createElement('div');
                 enhancedBadgeDetailsContainer.classList.add('enhanced_badge_details_container');
                 // Add an initial indicator message
                 const initialIndicator = document.createElement('div');
                 initialIndicator.classList.add('initial_indicator');
                 initialIndicator.textContent = "Checking cache..."; // Initial state
                 enhancedBadgeDetailsContainer.appendChild(initialIndicator);
                 badgeRow.appendChild(enhancedBadgeDetailsContainer);
             });


             // Try to get cache entry from IndexedDB (async)
             const cachedEntry = await getCacheEntry(appId); // Await the IndexedDB get

             if (isCacheValid(cachedEntry)) {
                  console.log(`Found valid cache for App ID: ${appId}. Displaying immediately.`);
                 // Immediately display cached data
                 displayBadgeDetails(appId, rowsForApp, cachedEntry.steamsetsData, cachedEntry.craftedNormalInfo, cachedEntry.craftedFoilInfo, true);
                 // Queue for background refresh if cache is old but still valid
                 const now = Date.now();
                 if ((now - cachedEntry.timestamp) > (CACHE_EXPIRATION_MS / 2)) { // Example: refresh if cache is older than half the expiration time
                     console.log(`Cache for App ID ${appId} is older than half the expiration, queueing for background refresh.`);
                     queueAppIdForFetch(appId);
                 }


             } else {
                  console.log(`No valid cache for App ID: ${appId}. Queueing for fetch.`);
                 // Update indicator to reflect API loading
                 rowsForApp.forEach(badgeRow => {
                        const indicator = badgeRow.querySelector('.enhanced_badge_details_container .initial_indicator');
                        if(indicator) indicator.textContent = "Loading detailed badge data...";
                   });
                 // Queue for background fetching
                 queueAppIdForFetch(appId);
             }
         }

         // The processFetchQueue function will start automatically if the fetchQueue is not empty
    }


    // Run the initial setup when the page is loaded
    window.addEventListener('load', initialSetup);

})();