Farm RPG Mining Progress Display

Displays the progress value from the mining bar, while collecting data in the background and graphing it.

// ==UserScript==
// @name        Farm RPG Mining Progress Display
// @namespace   http://tampermonkey.net/
// @version     4.4
// @description Displays the progress value from the mining bar, while collecting data in the background and graphing it.
// @author      ClientCoin
// @match       http://farmrpg.com/*
// @match       https://farmrpg.com/*
// @grant       GM_getValue
// @grant       GM_setValue
// @license     MIT
// @icon        https://www.google.com/s2/favicons?sz=64&domain=farmrpg.com
// ==/UserScript==

(function() {
    'use strict';
    console.log('Mining Progress Display Initiated');

    // --- CONFIGURATION ---
    const isDebugMode = false; // Set this to true to see debug logs
    const debugLog = (...args) => {
        if (isDebugMode) {
            console.log('Tampermonkey script:', ...args);
        }
    };
    // The number of most recent data points to plot on the graph.
    const pointsToPlot = 25;
    // A scaling factor for font sizes on the graph (e.g., 1.0 = normal size, 0.8 = 20% smaller)
    const fontScaleFactor = 1.2;
    // --- END CONFIGURATION ---

    // A unique key for storing data in GM_storage
    const STORAGE_KEY = 'farmrpg_mining_data';

    // ===========================================
    // SECTION 1: DATA COLLECTION LOGIC
    // - This section ONLY handles data storage.
    // - It does NOT modify the UI.
    // ===========================================
    let dataRetryCount = 0;
    const MAX_DATA_RETRIES = 5;

    // Function to get the current mining location name from the DOM
    function getMiningLocation() {
        debugLog('2. DATA: Executing getMiningLocation function...');

        const allCenterSlidingDivs = document.querySelectorAll('.center.sliding');
        debugLog(`2. DATA: - Found ${allCenterSlidingDivs.length} elements with class ".center.sliding"`);

        for (const div of allCenterSlidingDivs) {
            // Find the 'info' icon element inside the current div.
            const infoIcon = div.querySelector('a i.f7-icons');

            // This is the most reliable way to identify the mining location header.
            if (infoIcon) {
                const fullText = div.textContent;
                // The location name is the part of the string before the 'info' text.
                const locationText = fullText.substring(0, fullText.indexOf('info')).trim();
                debugLog(`2. DATA: - Found a match! Location element has 'info' icon. Location: ${locationText}`);
                return locationText;
            }
        }

        debugLog('2. DATA: - No matching element found. Returning null.');
        return null;
    }

    function getCurrentFloor() {
        const floorLabel = document.querySelector('.col-30 strong');
        const floorMatch = floorLabel.textContent.match(/(\d{1,3}(?:,\d{3})*)/);
        const currentFloor = floorMatch ? parseInt(floorMatch[1].replace(/,/g, ''), 10) : null;
        debugLog("Current Floor is: " + currentFloor);
        return currentFloor;
    }

    // Function to reset all stored data for the current mining location
    async function resetMiningData(locationName) {
        if (!locationName) {
            console.warn('Tampermonkey script: Cannot reset data, location name is not defined.');
            return;
        }

        // Confirmation prompt to prevent accidental data loss
        // Using a simple custom modal for better UX than alert/confirm
        const userConfirmed = await new Promise(resolve => {
            const modal = document.createElement('div');
            modal.style.cssText = `
                position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
                background: white; padding: 20px; border: 1px solid #ccc; border-radius: 8px;
                z-index: 10000; box-shadow: 0 4px 8px rgba(0,0,0,0.2);
            `;
            modal.innerHTML = `
                <p>Are you sure you want to delete all mining data for ${locationName}?</p>
                <button id="confirm-yes">Yes</button>
                <button id="confirm-no">No</button>
            `;
            document.body.appendChild(modal);

            document.getElementById('confirm-yes').onclick = () => {
                modal.remove();
                resolve(true);
            };
            document.getElementById('confirm-no').onclick = () => {
                modal.remove();
                resolve(false);
            };
        });

        if (userConfirmed) {
            try {
                let miningData = await GM_getValue(STORAGE_KEY, {});
                delete miningData[locationName];
                await GM_setValue(STORAGE_KEY, miningData);
                console.log(`Tampermonkey script: Successfully deleted all mining data for ${locationName}.`);

                // Reload the page to refresh the display
                window.location.reload();
            } catch (error) {
                console.error('Tampermonkey script: Error resetting data:', error);
            }
        }
    }

    async function collectAndStoreMiningData() {
        const progressBar = document.getElementById('mpb');
        const floorElement = document.querySelector('.col-30 strong span');
        const locationName = getMiningLocation();
        debugLog('Starting collectAndStoreMiningData...');
        debugLog('Location Name:', locationName);
        debugLog('Progress Bar Found:', !!progressBar);
        debugLog('Floor Element Found:', !!floorElement);

        if (!progressBar || !floorElement || !locationName) {
            if (dataRetryCount < MAX_DATA_RETRIES) {
                dataRetryCount++;
                setTimeout(collectAndStoreMiningData, 50);
            } else {
                dataRetryCount = 0;
            }
            return;
        }

        dataRetryCount = 0;
        const currentFloor = getCurrentFloor();
        const currentProgress = parseFloat(progressBar.getAttribute('data-progress'));

        try {
            let miningData = await GM_getValue(STORAGE_KEY, {});
            debugLog('Data retrieved from storage (before update):', miningData);

            // Store the data in the new nested format: location > floor > progress
            miningData[locationName] = miningData[locationName] || {};
            miningData[locationName][currentFloor] = {
                progress: currentProgress
            };

            await GM_setValue(STORAGE_KEY, miningData);
            debugLog('Data saved to storage (after update):', miningData);

        } catch (error) {
            console.error('Tampermonkey script: Error during data collection:', error);
        }
    }

    // ===========================================
    // SECTION 2: DISPLAY LOGIC (v1.1)
    // ===========================================
    function getRecentProgressPerFloor(locationData) {
        debugLog('1. DISPLAY: Calculating progress rate...');
        const ALPHA = 0.2;
        const MAD_THRESHOLD = 2.5;
        const MAD_MINIMUM_PERCENTAGE_OF_MEDIAN = 0.1;
        const allFloors = Object.keys(locationData).map(Number).sort((a, b) => a - b);
        const recentFloors = allFloors.slice(Math.max(0, allFloors.length - 100));

        if (recentFloors.length < 2) {
            debugLog('1. DISPLAY: Not enough data points to calculate a rate. (Need at least 2 floors)');
            return null;
        }

        let progressRates = [];
        for (let i = 1; i < recentFloors.length; i++) {
            const previousFloor = recentFloors[i - 1];
            const floorsGained = recentFloors[i] - previousFloor;

            if (floorsGained > 0) {
                const progressChange = locationData[recentFloors[i]].progress - locationData[previousFloor].progress;
                if (progressChange > 0) {
                    progressRates.push(progressChange / floorsGained);
                }
            }
        }
        debugLog('1. DISPLAY: Original progress rates:', progressRates.map(rate => rate.toFixed(4)));

        if (progressRates.length < 3) {
            debugLog('1. DISPLAY: Not enough data points for statistical filtering. (Need at least 3)');
            return null;
        }

        // --- Outlier detection using MAD ---
        const sortedRates = [...progressRates].sort((a, b) => a - b);
        const median = sortedRates.length % 2 === 0
            ? (sortedRates[sortedRates.length / 2 - 1] + sortedRates[sortedRates.length / 2]) / 2
            : sortedRates[Math.floor(sortedRates.length / 2)];

        const deviations = sortedRates.map(rate => Math.abs(rate - median));
        deviations.sort((a, b) => a - b);
        let mad = deviations.length % 2 === 0
            ? (deviations[deviations.length / 2 - 1] + deviations[deviations.length / 2]) / 2
            : deviations[Math.floor(deviations.length / 2)];
        if (mad < 0.01) {mad = median * MAD_MINIMUM_PERCENTAGE_OF_MEDIAN};

        const filteredRates = progressRates.filter(rate => Math.abs(rate - median) <= MAD_THRESHOLD * mad);

        const rejectedRates = progressRates.filter(rate => !filteredRates.includes(rate));
        debugLog(`1. DISPLAY: Median: ${median.toFixed(4)}, MAD: ${mad.toFixed(4)}`);
        debugLog(`1. DISPLAY: Filtering data points within ${MAD_THRESHOLD}x MAD from median.`);
        debugLog('1. DISPLAY: Rejected rates (outliers):', rejectedRates.map(rate => rate.toFixed(4)));

        if (filteredRates.length === 0) {
            debugLog('1. DISPLAY: No valid data points remain after filtering.');
            return null;
        }

        let weightedAverage = filteredRates[0];
        for (let i = 1; i < filteredRates.length; i++) {
            weightedAverage = (ALPHA * filteredRates[i]) + ((1 - ALPHA) * weightedAverage);
        }
        debugLog(`1. DISPLAY: Final calculated weighted average: ${weightedAverage.toFixed(4)}`);
        return weightedAverage;
    }


    async function updateMiningDisplay() {
        debugLog('Starting updateMiningDisplay...');
        const progressBar = document.getElementById('mpb');
        const floorElement = document.querySelector('.col-30 strong span');
        const locationName = getMiningLocation();
        debugLog('Location Name:', locationName);
        debugLog('Progress Bar Found:', !!progressBar);
        debugLog('Floor Element Found:', !!floorElement);

        if (!progressBar || !floorElement || !locationName) {
            return;
        }

        const floorLabel = document.querySelector('.col-30 strong');

        const currentFloor = getCurrentFloor();
        if (isNaN(currentFloor)) {
            debugLog('1. DISPLAY: Floor number is not a valid number. Exiting.');
            return;
        }

        if (floorLabel) {
            const textNode = Array.from(floorLabel.childNodes).find(node => node.nodeType === Node.TEXT_NODE && node.textContent.trim() === 'Floor');
            if (textNode) {
                textNode.remove();
            }
            const brElement = floorLabel.querySelector('br');
            if (brElement) {
                brElement.remove();
            }
            if (!floorLabel.querySelector('#floor-label')) {
                const labelSpan = document.createElement('span');
                labelSpan.id = 'floor-label';
                labelSpan.textContent = 'Floor: ';
                labelSpan.style.cssText = `font-size: 17px; font-weight: bold;`;
                floorLabel.prepend(labelSpan);
                const floorNumberSpan = floorLabel.querySelector('span:not(#floor-label)');
                if(floorNumberSpan) floorNumberSpan.style.fontSize = '17px';
            }
        }

        const currentProgress = parseFloat(progressBar.getAttribute('data-progress'));

        let miningData = await GM_getValue(STORAGE_KEY, {});
        debugLog('Data retrieved for display:', miningData);
        const locationData = miningData[locationName] || {};

        let floorsToGoText = 'Data gathering...';
        let targetFloorText = 'Complete 4 Floors to Start';
        let lastKnownProgress = 0;

        const allFloors = Object.keys(locationData).map(Number).sort((a, b) => b - a);
        if (allFloors.length > 0) {
            const lastFloorNumber = allFloors[0];
            lastKnownProgress = locationData[lastFloorNumber]?.progress || 0;
        }
        debugLog('Stored location data:', miningData);
        if (currentProgress < lastKnownProgress) {
            miningData[locationName] = {};
            await GM_setValue(STORAGE_KEY, miningData);
        }

        const progressRate = getRecentProgressPerFloor(locationData, currentFloor);

        if (progressRate && progressRate > 0) {
            const progressRemaining = 100 - currentProgress;
            const estimatedFloorsToGo = progressRemaining / progressRate;

            floorsToGoText = `Est. Floors: ${Math.round(estimatedFloorsToGo)}`;
            targetFloorText = `Target Floor: ${Math.round(currentFloor + estimatedFloorsToGo)}`;
        }

        // Function to create or update the display elements
        const createOrUpdateDisplay = (progressValue) => {
            var floorContainer = document.querySelector('.col-30');
            floorContainer.innerHTML = floorContainer.innerHTML.replace(/<br>\s*<br>/g, '<br>');

            if (floorContainer) {
                let progressDisplay = document.getElementById('farmrpg-progress-display');
                let estimateDisplay = document.getElementById('farmrpg-estimate-display');
                let targetFloorDisplay = document.getElementById('farmrpg-target-floor-display');
                let resetButton = document.getElementById('farmrpg-reset-button');

                // --- Create/Update Progress, Estimate, and Target Displays ---
                if (!progressDisplay) {
                    progressDisplay = document.createElement('div');
                    progressDisplay.id = 'farmrpg-progress-display';
                    floorContainer.insertBefore(progressDisplay, floorContainer.querySelector('strong'));
                    progressDisplay.style.cssText = `font-weight: bold !important; color: lightgreen !important; font-size: 14px !important; margin-bottom: 5px !important; text-shadow: 0 0 5px rgba(144, 238, 144, 0.5) !important;`;
                }
                const formattedProgress = parseFloat(progressValue).toFixed(2);
                progressDisplay.textContent = `Progress: ${formattedProgress}%`;

                if (!estimateDisplay) {
                    estimateDisplay = document.createElement('div');
                    estimateDisplay.id = 'farmrpg-estimate-display';
                    floorContainer.insertBefore(estimateDisplay, floorContainer.querySelector('strong').nextSibling);
                    estimateDisplay.style.cssText = `font-weight: bold !important; color: #add8e6 !important; font-size: 12px !important; margin-top: 5px !important;`;
                }
                estimateDisplay.textContent = floorsToGoText;

                if (!targetFloorDisplay) {
                    targetFloorDisplay = document.createElement('div');
                    targetFloorDisplay.id = 'farmrpg-target-floor-display';
                    floorContainer.insertBefore(targetFloorDisplay, estimateDisplay.nextSibling);
                    targetFloorDisplay.style.cssText = `font-weight: bold !important; color: #ffcc00 !important; font-size: 12px !important; margin-top: 5px !important;`;
                }
                targetFloorDisplay.textContent = targetFloorText;

                // --- Create/Update Reset Button ---
                if (!resetButton) {
                    resetButton = document.createElement('a');
                    resetButton.id = 'farmrpg-reset-button';
                    resetButton.className = 'button btnpurple';
                    resetButton.style.cssText = `font-size: 11px; height: 20px; line-height: 18px; width: 100px; margin: 10px auto 0 auto; display: block;`;
                    resetButton.textContent = 'Reset Data';
                    resetButton.onclick = () => resetMiningData(locationName);
                    floorContainer.insertBefore(resetButton, targetFloorDisplay.nextSibling);//.replace(/<br><br>/g, '<br>');

                }

                // --- Call the graph function ---
                createAndPlaceGraph(locationData);
            }
        };

        createOrUpdateDisplay(currentProgress);
    }

    // ===========================================
    // SECTION 3: GRAPH LOGIC (UPDATED FOR CANVAS)
    // ===========================================
    let retryCount = 0;
    const MAX_RETRIES = 10;

    /**
     * Creates and places the graph in the correct position with a retry mechanism.
     * @param {object} locationData - The data object for the current mining location.
     */
    function createAndPlaceGraph(locationData) {
        debugLog('Starting createAndPlaceGraph...');

        // Use a more specific selector to find the correct row
        const parentRow = document.querySelector('.page[data-page="mining"] .row');
        if (!parentRow) {
            console.error('Tampermonkey script: Could not find the correct parent .row element. Retrying...');
            if (retryCount < MAX_RETRIES) {
                retryCount++;
                setTimeout(() => createAndPlaceGraph(locationData), 200);
            }
            return;
        }

        const leftCol = parentRow.querySelector('.col-30:first-of-type');
        const centerCol = parentRow.querySelector('.col-40');
        const rightCol = parentRow.querySelector('.col-30:last-of-type');

        if (!leftCol || !centerCol || !rightCol) {
            console.error('Tampermonkey script: Could not find one of the required columns for re-positioning. Retrying...');
            if (retryCount < MAX_RETRIES) {
                retryCount++;
                setTimeout(() => createAndPlaceGraph(locationData), 200);
            }
            return;
        }

        // Reset retry count on success
        retryCount = 0;

        // --- Step 1: Move content from rightCol to leftCol ---
        var rightColContent = rightCol.innerHTML.replace('<span style=\u0022font-size: 12px\u0022>Items Left</span>','').replace('<span style=\u0022font-size: 12px\u0022>Attempts Left</span>','').replace('<span style=\u0022font-size: 18px\u0022 id=\u0022attempts','Attempts Left: <span style=\u0022font-size: 12px\u0022 id=\u0022attempts').replace('<span style=\u0022font-size: 18px\u0022 id=\u0022items','Items Left: <span style=\u0022font-size: 12px\u0022 id=\u0022items').replace(/<br><br>/g, '<br>').replace('</strong><br>','</strong>');

        //<span style="font-size: 12px"></span>
//<span style="font-size: 18px" id="items">3</span>
//<span style="font-size: 18px" id="attempts">22</span>

        const newContentDiv = document.createElement('div');
        newContentDiv.innerHTML = rightColContent;
        leftCol.appendChild(newContentDiv);
        debugLog('Moved content from right column to left column.');

        // --- Step 2: Remove the original rightCol ---
        parentRow.removeChild(rightCol);
        debugLog('Removed the original right column.');

        // --- Step 3: Resize the centerCol ---
        centerCol.className = 'col-20';
        debugLog('Resized center column to col-20.');

        // --- Step 4: Create a new col-50 for the graph ---
        let graphCol = document.getElementById('farmrpg-graph-col');
        if (!graphCol) {
            graphCol = document.createElement('div');
            graphCol.id = 'farmrpg-graph-col';
            graphCol.className = 'col-50';
            parentRow.insertBefore(graphCol, centerCol.nextSibling);
            debugLog('Created a new col-50 for the graph.');
        }

        // --- Step 5: Place the graph inside the new col-50 ---
        let graphContainer = document.getElementById('farmrpg-graph-container');
        if (!graphContainer) {
            graphContainer = document.createElement('div');
            graphContainer.id = 'farmrpg-graph-container';
            graphContainer.style.cssText = `
                margin-top: 20px;
                text-align: center;
            `;
            graphCol.appendChild(graphContainer);
            debugLog('Placed the graph container in the new col-50.');
        }

        let graphCanvas = document.getElementById('farmrpg-progress-graph');
        let messageDiv = document.getElementById('farmrpg-graph-message');

        // Prepare data for the graph and filter out non-numeric keys
        let floors = Object.keys(locationData)
                           .filter(key => !isNaN(Number(key))) // Filter out non-numeric keys
                           .map(Number)
                           .sort((a, b) => a - b);
        debugLog('Floors array from data (filtered):', floors);
        debugLog('Number of floors:', floors.length);

        // --- Filter to only the last X points ---
        floors = floors.slice(Math.max(0, floors.length - pointsToPlot));
        debugLog(`Filtered to last ${pointsToPlot} floors:`, floors);

        // If there are less than two data points, show a message instead of a graph
        if (floors.length < 2) {
            if (messageDiv) messageDiv.remove();
            if (graphCanvas) graphCanvas.remove();

            messageDiv = document.createElement('div');
            messageDiv.id = 'farmrpg-graph-message';
            messageDiv.textContent = 'Mine more to see your progress graph!';
            graphContainer.appendChild(messageDiv);

            return;
        }

        // If there's enough data, ensure the graph canvas exists
        if (!graphCanvas) {
            graphCanvas = document.createElement('canvas');
            graphCanvas.id = 'farmrpg-progress-graph';
            graphCanvas.width = graphCol.clientWidth;
            graphCanvas.height = 200;
            graphCanvas.style.cssText = `
                background-color: rgba(0, 0, 0, 0.4);
                border-radius: 8px;
            `;
            graphContainer.appendChild(graphCanvas);
        }
        if (messageDiv) messageDiv.remove();

        // Get the canvas context
        const ctx = graphCanvas.getContext('2d');

        // Clear the canvas for redrawing
        ctx.clearRect(0, 0, graphCanvas.width, graphCanvas.height);

        // Define drawing area and scaling
        const padding = 20;
        const xOffset = 30; // Space for Y-axis labels
        const graphWidth = graphCanvas.width - padding * 2 - xOffset;
        const graphHeight = graphCanvas.height - padding * 2;

        // Find min/max values for scaling
        const minFloor = floors[0];
        const maxFloor = floors[floors.length - 1];

        // Calculate dynamic min and max progress values
        const lastProgressValues = floors.map(floor => locationData[floor]?.progress).filter(p => p !== undefined);
        const minRawProgress = Math.min(...lastProgressValues);
        const maxRawProgress = Math.max(...lastProgressValues);

        // Add a 5% buffer to the min and max values for better visualization
        const progressRange = maxRawProgress - minRawProgress;
        const paddedMinProgress = Math.max(0, minRawProgress - progressRange * 0.05);
        const paddedMaxProgress = Math.min(100, maxRawProgress + progressRange * 0.05);

        debugLog(`minFloor: ${minFloor}, maxFloor: ${maxFloor}, floorRange: ${maxFloor - minFloor}`);
        debugLog(`minProgress: ${paddedMinProgress.toFixed(2)}, maxProgress: ${paddedMaxProgress.toFixed(2)}`);

        // Draw the graph title
        ctx.fillStyle = '#fff';
        ctx.font = `${16 * fontScaleFactor}px Arial`;
        ctx.textAlign = 'center';
        ctx.fillText('Mining Progress Over Time', graphCanvas.width / 2, padding / 2 + 10);

        // Draw axes and labels
        ctx.strokeStyle = '#ddd';
        ctx.fillStyle = '#ddd';
        ctx.lineWidth = 1;

        // Y-axis
        ctx.beginPath();
        ctx.moveTo(padding + xOffset, padding);
        ctx.lineTo(padding + xOffset, graphHeight + padding);
        ctx.stroke();

        // X-axis
        ctx.beginPath();
        ctx.moveTo(padding + xOffset, graphHeight + padding);
        ctx.lineTo(graphWidth + padding + xOffset, graphHeight + padding);
        ctx.stroke();

        // Label Y-axis
        ctx.textAlign = 'right';
        ctx.font = `${12 * fontScaleFactor}px Arial`;
        ctx.fillText(paddedMaxProgress.toFixed(1) + '%', padding + xOffset - 5, padding + 5);
        ctx.fillText(paddedMinProgress.toFixed(1) + '%', padding + xOffset - 5, graphHeight + padding - 5);

        // Label X-axis (with min/max floor numbers)
        ctx.font = `${12 * fontScaleFactor}px Arial`;
        ctx.textAlign = 'left';
        ctx.fillText(minFloor, padding + xOffset, graphHeight + padding + 15);
        ctx.textAlign = 'right';
        ctx.fillText(maxFloor, graphWidth + padding + xOffset, graphHeight + padding + 15);

        // --- Plot the line ---
        ctx.strokeStyle = '#ffcc00';
        ctx.lineWidth = 2;
        ctx.beginPath();

        floors.forEach((floor, index) => {
            const dataPoint = locationData[floor];
            if (!dataPoint) return;
            const progress = dataPoint.progress;

            let x;
            const floorRange = maxFloor - minFloor;
            if (floorRange <= 1) {
                x = padding + xOffset + (index / (floors.length - 1)) * graphWidth;
            } else {
                x = padding + xOffset + ((floor - minFloor) / floorRange) * graphWidth;
            }
            const y = graphHeight + padding - ((progress - paddedMinProgress) / (paddedMaxProgress - paddedMinProgress)) * graphHeight;

            if (index === 0) {
                ctx.moveTo(x, y);
            } else {
                ctx.lineTo(x, y);
            }
        });
        ctx.stroke();

        // --- Plot the points on top of the line ---
        floors.forEach((floor, index) => {
            const dataPoint = locationData[floor];
            if (!dataPoint) return;
            const progress = dataPoint.progress;

            let x;
            const floorRange = maxFloor - minFloor;
            if (floorRange <= 1) {
                x = padding + xOffset + (index / (floors.length - 1)) * graphWidth;
            } else {
                x = padding + xOffset + ((floor - minFloor) / floorRange) * graphWidth;
            }
            const y = graphHeight + padding - ((progress - paddedMinProgress) / (paddedMaxProgress - paddedMinProgress)) * graphHeight;

            ctx.fillStyle = '#ffcc00';
            ctx.beginPath();
            ctx.arc(x, y, 4, 0, Math.PI * 2);
            ctx.fill();
        });
    }


    // ===========================================
    // SECTION 4: ACTIVATION
    // - This section activates both functions.
    // ===========================================
    updateMiningDisplay();
    collectAndStoreMiningData();

    const targetNode = document.querySelector("#fireworks");
    if (targetNode) {
        const navObserver = new MutationObserver((mutationsList) => {
            if (mutationsList.some(m => m.attributeName === 'data-page')) {
                debugLog('3. ACTIVATION: Navigation detected via data-page attribute change.');
                updateMiningDisplay();
                collectAndStoreMiningData();
            }
        });
        navObserver.observe(targetNode, { attributes: true });
        debugLog('3. ACTIVATION: Navigation observer set up.');
    }
})();