ZedHelper

Misc helper tools for Zed City

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

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

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

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

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

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

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

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

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

// ==UserScript==
// @name         ZedHelper
// @description  Misc helper tools for Zed City
// @version      0.5.8
// @namespace    kvassh.zedhelper
// @license      MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=zed.city
// @homepage     https://greasyfork.org/en/scripts/527868-zedhelper
// @author       Kvassh
// @match        https://www.zed.city/*
// @run-at       document-end
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        window.onurlchange
// @connect      api.zed.city
// ==/UserScript==

/** 
 * ZedHelper
 * 
 * Features:
 * - Displays market value for items in inventory
 * - Calculates your inventory networth based on current market values
 * - Extra nav menu with some useful shortcuts (togglable in settings)
 * - Autopopulates gym train input fields to use maximum energy
 * - Autopopulates input field for junk shop with 360 item buy qty
 * - Show value of trades at Radio Tower
 * - Show timer for various features (Raid, Junk Store limit)
 *
 * If you have any questions, feel free to reach out to Kvassh [12853] in Zed City
 * 
 * Changelog:
 * - 0.5.8:  Faction raid report: Fix bug in date comparison - dont use local timezone
 * - 0.5.7:  Faction raid report: Redesign implementation, fixes bug when storing faction logs and generating raid report
 * - 0.5.6:  Faction raid report: Fetch all data from API instead of parsing HTML from website
 * - 0.5.5:  Faction raid report: Fetch ZedCityTime from server instead of trying to parse locale
 * - 0.5.4:  Fix bug if local date uses dd.mm.yyyy format instead of mm/dd/yyyy like ZedCity
 * - 0.5.3:  Slightly increase max height of Raid Report element
 *           Remove shortcut to ZED MART
 *           Add shortcut to Faction storage for beer rations
 * - 0.5.2:  Add fallback URL detection listener if navigation API fails (like on Firefox)
 *           Fixed bug in time comparison for Raid Report log
 * - 0.5.1:  Add Faction Raid Report on logs page
 * - 0.4.17: Add Max Rad btn on scavenge page
 *           Make icons/links smaller on the timer bar
 * - 0.4.16: Try to click MAX button to set input to max in stores
 *           Add more buttons to timer bar (Gym, RadioTower, Scavenge)
 *           Make the timer links go to the page without reloading webbrowser page
 * - 0.4.15: Add bulk scavenge buttons for 5 and 30 scavenges at a time.
 *           Fix bug in Radio Tower trade value calculation.
 * - 0.4.14: Another fix for the duplicate timer bar issue, hopefully 100% fixed now.
 * - 0.4.13: Fix error with duplicate timer bar appearing.
 *           Add link to respective functions for timers also when not ready
 * - 0.4.12: Fix another bug in time parsing for timer bar
 * - 0.4.11: Fix bug in time parsing for timer bar
 * - 0.4.10: Fix correct link for Raid timer shortcut
 * - 0.4.9: Add timer for Raid
 * - 0.4.8: Add timer for Junk Store limit
 * - 0.4.7: Fix container width for mobile in Settings page.
 * - 0.4.6: Add ZH icon to statusbar that points to new Settings page.
 *          Add setting for toggling extra nav menu on or off.
 *          Include cash on hand when calculating networth.
 * - 0.4.5: Avoid duplicate inventory networth elements
 *          Less padding for item values in inventory to fit better on mobile.
 * - 0.4.4: Change homepage and downloadURL to use greasyfork.org + change icon to zed.city favicon.
 * - 0.4.3: Use navigation navigate eventlistener instead to detect page change.
 * - 0.4.2: Try to force window eventlistener for urlchange to work on mobile.
 * - 0.4.1: Show warning if market values has not been cached yet. 
 *          Show warning on Radio Tower if the cached data is old.
 *          Indicate if the trade is good or bad with checkmark on Radio Tower.
 *          Fixed bug on inventory page where it would potentially not update prices if changing to next page in inventorylist.
 * - 0.4: Add value of trades at Radio Tower.
 * - 0.3: Fix bug in gym autopopulate + add new autopopulate in junk store for 360 items.
 * - 0.2: Add feature to autopopulate gym input fields.
 * - 0.1: Initial release.
*/

(function() {
    'use strict';

    // Add CSS for displaying prices (optional, but makes it look nicer)
    GM_addStyle(`
        .market-price {
            color:#999999;
            float:right;
            position:absolute;
            top:18px;
            right:100px;
        }
        .green {
            color: #00cc66;
        }
        .red {
            color: #ff6666;
        }
        .gray {
            color: #888;
        }
        .zedhelper-networth {
            text-align: center;
            margin: 10px auto;
            color: #ccc;
            font-size: 1.6rem;
        }
        .zedhelper-inventory-warning {
            text-align: center;
            margin: 10px auto;
            color: #ccc;
            font-size: 0.8rem;
        }
        .radio-warning {
            text-align: center;
        }
        .zedhelper-timer-bar {
            margin-top:0px;
        }
        .zedhelper-timer-span {
            padding: 0 10px;
        }
        .zedhelper-timer-span a {
            text-decoration:none;
        }
    `);

    const baseApiUrl = 'https://api.zed.city';

    /** Dont modify anything below this line */

    let module = "index";
    let checkForInventoryUpdates = null;








    /** Utils */

    function get(key) {
        return localStorage.getItem(`kvassh.zedhelper.${key}`);
    }
    function set(key, value) {
        localStorage.setItem(`kvassh.zedhelper.${key}`, value);
    }

    function log(msg) {
        const spacer = "          ";
        const ts = new Date();
        console.log("ZedHelper (" + ts.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' } )+ ") " +
            "[" + module + "]" + ((module.length < spacer.length) ? spacer.substring(0, spacer.length - module.length) : "") + ": " +
            (typeof msg === 'object' ? JSON.stringify(msg) : msg));
    }

    function waitForElement(selector) {
        return new Promise(resolve => {
            if (document.querySelector(selector)) {
                return resolve(document.querySelector(selector));
            }
    
            const observer = new MutationObserver(mutations => {
                if (document.querySelector(selector)) {
                    observer.disconnect();
                    resolve(document.querySelector(selector));
                }
            });
    
            // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        });
    }

    function getCodename(itemName) {
        let codename = itemName.toString().toLowerCase().replace(' ', '_').trim().split(/\n/)[0];

        let nametable = {
            "arrows": "ammo_arrows",
            "bows": "ammo_bows",
            "logs": "craft_log",
            "nails": "craft_nails",
            "rope": "craft_rope",
            "scrap": "craft_scrap",
            "wire": "craft_wire",
            "army_helmet": "defense_army_helmet",
            "camo_hat": "defense_camo_hat",
            "camo_vest": "defense_camo_vest",
            "e-cola": "ecola",
            "lighter":"misc_lighter",
            "lockpick":"misc_lockpick",
            "pickaxe":"misc_pickaxe",
            "security_card":"defense_security_card",
            "zed_coin": "points",
            "baseball_bat": "weapon_baseball_bat",
            "bow":"weapon_bow",
            "chainsaw":"weapon_chainsaw",
            "spear":"weapon_spear",
            "switchblade":"weapon_switchblade",
        };

        for (const [key, value] of Object.entries(nametable)) {
            if (codename === key) {
                return value;
            }
        }
        return codename;
    }

    function formatNumber(number) {
        const formatter = new Intl.NumberFormat('nb-NO', {
            maximumFractionDigits: 0,
            

        });
        return formatter.format(number);
    }


















    /** XHR Interceptor */

    const originalXHR = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (...args) {
        this.addEventListener('load', async function () {
            const url = this.responseURL;

            // if (url.includes("/getOffers")) {
            //     const item = JSON.parse(this.responseText)[0];
            //     log(`Caching market value for: ${item['name']} (${item['codename']})`);
            //     set(`mv_${item["codename"]}`, JSON.stringify({ "name": item["name"], "marketValue": item["market_price"], "tz": Date.now() }));
            // }

            if (url.endsWith("/getMarket")) {
                const items = JSON.parse(this.responseText).items;
                let itemsCached = 0;
                for (let item of items) {
                    let codename = getCodename(item["name"]);
                    set(`mv_${codename}`, JSON.stringify({ "name": item["name"], "marketValue": item["market_price"], "tz": Date.now() }));
                    itemsCached++;
                }
                set(`mv_lastupdate`, Date.now());
                log(`Cached market value for ${itemsCached} items.`);
            }

            else if (url.endsWith("/loadItems")) {
                const data = JSON.parse(this.responseText);
                const items = data.items;
                let networthVendor = 0;
                let networthMarket = 0;
                for (let item of items) {

                    networthVendor += item.value * item.quantity;

                    const codename = item.codename;
                    if(get(`mv_${codename}`)) {
                        const mv = JSON.parse(get(`mv_${codename}`));
                        networthMarket += mv.marketValue * item.quantity;
                    } else {
                        networthMarket += item.value * item.quantity;
                    }
                }
                set(`mv_networth_vendor`, networthVendor);
                set(`mv_networth_market`, networthMarket);
                log(`cached inventory networth (vendor: ${networthVendor}, market: ${networthMarket})`);
            }

            else if (url.endsWith("/getStats")) {
                const data = JSON.parse(this.responseText);
                set(`energy`, data.energy);
                set(`morale`, data.morale);
                set(`rad`, data.rad);
                set(`refills`, data.refills);
                set(`money`, data.money);
                set(`xpUntilNextRank`, parseInt(data.xp_end-data.experience));

                set(`raidCooldownSecondsLeft`, data.raid_cooldown); 
                set(`raidCooldownTime`, Date.now()); 
            }

            else if (url.endsWith("/getRadioTower")) {
                const data = JSON.parse(this.responseText);
                saveCurrentTradeValues(data);
                set(`radio_lastupdate`, Date.now());
            }

            else if (url.endsWith("/getStore?store_id=junk")) {
                const data = JSON.parse(this.responseText);
                if (data.hasOwnProperty('limits')) {
                    set(`junkStoreLimitSecondsLeft`, data.limits.reset_time); 
                    set(`junkStoreLimitTime`, Date.now()); 
                } else {
                    set(`junkStoreLimitSecondsLeft`, 0); 
                    set(`junkStoreLimitTime`, 0); 
                }
            }

            else if (url.endsWith("/getStore?store_id=zedmart")) {
                const data = JSON.parse(this.responseText);
                if (data.hasOwnProperty('limits')) {
                    set(`zedMartLimitSecondsLeft`, data.limits.reset_time); 
                    set(`zedMartLimitTime`, Date.now()); 
                } else {
                    set(`zedMartLimitSecondsLeft`, 0); 
                    set(`zedMartLimitTime`, 0); 
                }
            }

            else if (url.endsWith("/getFactionMembers")) {
                const data = JSON.parse(this.responseText);
                log("Intercepting faction members!!!");
                if (data.hasOwnProperty('members')) {
                    set('factionMembers', JSON.stringify(data.members));
                }
            }

            else if (url.indexOf("getFactionNotifications") !== -1) {

                showRaidReport(true);
                await storeFactionLogs(url, this.responseText);
                setTimeout(() => { showRaidReport() }, 250);
            }

        });
        originalXHR.apply(this, args);
    };


















    /** Main script */

    log("Starting up ZedHelper!");

    let navigationTimeout = null;

    let urlChangeHandler = async () => {
        if (navigationTimeout === null) {

            const page = location.pathname;

            // Ensure we dont watch for inventory updates after changing subpage
            clearInterval(checkForInventoryUpdates);
            checkForInventoryUpdates = null;

            // Update the timer bar
            addZedHelperIconAndTimerBar();

            if (page.includes("inventory")) {
                module = "inventory";
                // log("Waiting for inventory list...");
                
                waitForElement("#q-app > div > div.q-page-container > main > div > div:nth-child(4) > div > div.grid-cont.no-padding").then(() => {
                    waitForElement(".item-row").then(() => {
                        log("Inventory list loaded! Adding market prices...");
                        addMarketPrices();
                    });
                });

                waitForElement("#q-app > div > div.q-page-container > main > div").then(() => {
                    showNetworth();
                });
            }
            else if (page.includes("market-listings")) {
                module = "market";
                log("Navigated to Market Listings - Watching for element to add new listing...");
                waitForElement("div > div > button.q-btn.q-btn-item.bg-positive").then(() => {
                    log("Detected form for adding new market listing... showing market values for inventory!");
                    addMarketPrices();
                })
            }
            else if (page.includes("stronghold/2375014")) {
                module = "gym";
                log("Navigated to Gym");
                autoPopulateTrainInput();
            }
            else if (page.includes("stronghold/2375016")) {
                module = "crafting";
                log("Navigated to Crafting Bench");
            }
            else if (page.includes("stronghold/2375017")) {
                module = "furnace";
                log("Navigated to Furnace");
            }
            else if (page.includes("stronghold/2375019")) {
                module = "radio";
                log("Navigated to Radio Tower");
                setTimeout(() => {
                    showTradeValues();
                },1000);
            }
            else if (page.includes("/store/")) {
                module = "store";
                log("Setting up auto input for store - click max btn automatically");
                // autoPopulate360Items();
                autoPopulateMaxItems();
            }
            else if (page.includes("/zedhelper")) {
                showSettingsPage();
            }
            else if (/\/scavenge\/\d+$/.test(page)) {
                module = "scavenge";
                log("Navigated to Scavenge");
                addBulkScavengeButtons();
            }
            else if (page.includes('/faction/logs')) {
                module = "faction-logs";
                log("Navigated to Faction Logs");
                clearFactionLogsCache();
                /* XHR interceptor will automatically fetch logs
                then call on the showRaidReport */
            }
            else {
                module = "unknown";
                log(`Unknown subpage: ${page}`);
            }

            navigationTimeout = setTimeout(() => {
                clearTimeout(navigationTimeout);
                navigationTimeout = null;
            }, 250);
        }
    }

    try {
        navigation.addEventListener('navigate', () => {
            setTimeout(() => {
                urlChangeHandler();
            },100);
        });
    } catch (error) {
        log("FATAL ERROR: Could not add EventListener for navigation navigate: " + JSON.stringify(error));
        log("Trying to use fallback method");
        try {
            let currentPage = '';
            let prevPage = '';
            setInterval(() => {
                currentPage = location.pathname;
                if (currentPage !== prevPage) {
                    log("URL changed - handle it!");
                    urlChangeHandler();
                }
                prevPage = currentPage;
            },1000);
            log("Activated fallback method for listening to URL change events.");
        } catch(e) {
            log("FATAL ERROR: Fallback method for URL changes didn't work. Sorry, you're SOL...");
        }
    }













    /** Add a second nav menu with some useful shortcuts */

    // document.querySelector("#q-app > div > header > div:nth-child(2) > div > div > div").app
    const secondNavBar = document.createElement('div');
    secondNavBar.innerHTML = `
<div>
    <div class="gt-xs bg-grey-3 text-grey-5 text-h6">
        <div class="q-tabs row no-wrap items-center q-tabs--not-scrollable q-tabs--horizontal q-tabs__arrows--inside q-tabs--mobile-with-arrows q-tabs--dense" role="tablist" inside-arrows="">

            <div class="q-tabs__content scroll--mobile row no-wrap items-center self-stretch hide-scrollbar relative-position q-tabs__content--align-center">

                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/stronghold/2375017">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                        <div class="q-tab__label">Furnace</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>
                
                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/stronghold/2375014">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                        <div class="q-tab__label">Gym</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>

                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/scavenge/2">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                    <div class="q-tab__label">Scrapyard</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>

                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/market">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                    <div class="q-tab__label">Market</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>

                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/store/junk">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                    <div class="q-tab__label">Junk Store</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>

                <a class="q-tab relative-position self-stretch flex flex-center text-center q-tab--inactive q-tab--full q-focusable q-hoverable cursor-pointer menu-link" tabindex="0" role="tab" aria-selected="false" href="/zedhelper">
                    <div class="q-focus-helper" tabindex="-1"></div>
                    <div class="q-tab__content self-stretch flex-center relative-position q-anchor--skip non-selectable column">
                    <div class="q-tab__label">Settings</div>
                    </div>
                    <div class="q-tab__indicator absolute-bottom text-transparent"></div>
                </a>

            </div>
        </div>
    </div>
    `;  

    if(get('extraNavMenu') && (get('extraNavMenu') === 'true' || get('extraNavMenu') === true)) {
        log("Enabling extra navigation menu");
        waitForElement("#q-app > div > header").then(() => {
            document.querySelector("#q-app > div > header").appendChild(secondNavBar);
        });
    }











    /** Add icon for ZedHelper settings + timer bar */

    function addZedHelperIconAndTimerBar() {

        const zedHelperIcon = document.createElement('div');
        zedHelperIcon.classList = 'zedhelper-icon-bar';
        zedHelperIcon.innerHTML = `
    <div class="row items-center">
        <b><a href="/zedhelper" title="ZedHelper Settings" style="color:dodgerblue;text-decoration:none;font-weight:bold;">ZH</a></b>
    </div>
        `;

        const timerBar = document.createElement('div');
        timerBar.classList = "row q-col-gutter-md justify-center items-center zedhelper-timer-bar";

        let timeDiff = 0;
        let timeLeft = 0;
        let timeLeftFormatted = "";

        let html = `<div class="q-tab__label">`;

        /** GYM */
        // const maxEnergy = 150;
        // const currentEnergy = get('energy') || 0; // Retrieve current energy from storage
        // const energyRegenRate = 5; // Energy regenerated per interval
        // const regenIntervalMinutes = 10; // Interval in minutes
        // const missingEnergy = maxEnergy - currentEnergy;
        // const timeToFullEnergyMinutes = Math.ceil((missingEnergy / energyRegenRate) * regenIntervalMinutes);
        // if (timeToFullEnergyMinutes > 0) {
        //     const hours = Math.floor(timeToFullEnergyMinutes / 60);
        //     const minutes = timeToFullEnergyMinutes % 60;
        //     timeLeftFormatted = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
        // }
        // html += `<span class="zedhelper-timer-span">Gym: <a id="zhOpenGym" style="cursor:pointer;">${timeToFullEnergyMinutes > 0 ? `<span class="red">${timeLeftFormatted}</span>` : `<span class="green">Ready</span>`}</a></span>`;
        // html += `<span class="zedhelper-timer-span"><a id="zhOpenGym" style="cursor:pointer;">⚡</a></span>`;
        // html += `<span class="zedhelper-timer-span"><a id="zhOpenGym" style="cursor:pointer;"><img src="https://www.zed.city/assets/gym-cOgAonBN.png" style="width:20px;position:relative;top:5px;"></a></span>`;
        html += `<span class="zedhelper-timer-span"><a id="zhOpenGym" style="cursor:pointer;">💪</a></span>`;

        /** RADIO TOWER */
        // const last = get("radioTower_last_visited") || 0;
        // const ready = parseInt(last) + (12*60*60*1000);
        // timeLeft = ready - Date.now();
        // const h = Math.floor(timeLeft / 36e5);
        // const m = Math.floor((timeLeft % 36e5) / 6e4);
        // const s = Math.floor((timeLeft % 6e4) / 1000);
        // // return `${h}h ${m}m ${s}s left`;
        // console.log(`last: ${last} - ready: ${ready} - now: ${Date.now()} - timeLeft: ${timeLeft}`);
        // // console.log("timeLeft: " + timeLeft);
        // try {
        //     timeLeftFormatted = new Date(timeLeft * 1000).toISOString().substr(11, 5);
        // } catch (error) {
        //     timeLeftFormatted = "00:00";
        // }
        // html += `<span class="zedhelper-timer-span">Radio&nbsp;Tower: <a id="zhOpenRadioTower" style="cursor:pointer;">${timeLeft > 0 ? `<span class="red">${timeLeftFormatted}</span>` : `<span class="green">Ready</span>`}</a></span>`;
        // html += `<span class="zedhelper-timer-span"><a id="zhOpenRadioTower" style="cursor:pointer;"><img src="https://www.zed.city/assets/radio_tower-DZgBlHS5.png" style="width:20px;position:relative;top:5px;"></a></span>`;
        html += `<span class="zedhelper-timer-span"><a id="zhOpenRadioTower" style="cursor:pointer;">📡</a></span>`;

        
        /** SCAVENGE */
        // const maxRad = 50;
        const currentRad = get('rad') || 0; // Retrieve current rad from storage
        // const radRegenRate = 1;             // Rad regenerated per interval
        // const regenIntervalMinutesRad = 5;    // Interval in minutes
        // const missingRad = maxRad - currentRad;
        // const timeToFullRadMinutes = Math.ceil((missingRad / radRegenRate) * regenIntervalMinutesRad);

        // let radTimeLeftFormatted = '';
        // if (timeToFullRadMinutes > 0) {
        //     const hours = Math.floor(timeToFullRadMinutes / 60);
        //     const minutes = timeToFullRadMinutes % 60;
        //     radTimeLeftFormatted = `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
        // }
        // html += `<span class="zedhelper-timer-span">Scavenge: <a id="zhOpenScavenge" style="cursor:pointer;">${timeToFullRadMinutes > 0 ? 
        //     `<span class="red">${radTimeLeftFormatted}</span>`
        //     : `<span class="green">Ready</span>`
        // }</a></span>`;
        // html += `<span class="zedhelper-timer-span"><a id="zhOpenScavenge" style="cursor:pointer;"><img src="https://www.zed.city/assets/scrapyard-BrTM3-qI.jpg" style="width:20px;position:relative;top:5px;"></a></span>`;
        // html += `<span class="zedhelper-timer-span"><a id="zhOpenScavenge" style="cursor:pointer;">${currentRad < 10 ? `<span class="red">Scavenge</span>` : `<span class="green">Scavenge</span>`}</a></span>`;
        html += `<span class="zedhelper-timer-span"><a id="zhOpenScavenge" style="cursor:pointer;">${currentRad < 10 ? `<span class="red">🛢️</span>` : `<span class="green">🛢️</span>`}</a></span>`;
                

        /** BEER RATIONS */
        html += `<span class="zedhelper-timer-span"><a id="zhOpenFactionStorage" style="cursor:pointer;">🍺</a></span>`;

        /** RAID */
        const raidCooldownSecondsLeft = get('raidCooldownSecondsLeft');
        const raidCooldownTime = get('raidCooldownTime');
        if (raidCooldownSecondsLeft && raidCooldownTime) {
            timeDiff = (Date.now() - raidCooldownTime)/1000;
            timeLeft = raidCooldownSecondsLeft - Math.round(timeDiff);
            try {
                timeLeftFormatted = new Date(timeLeft * 1000).toISOString().substr(11, 5);
            } catch (error) {
                timeLeftFormatted = "00:00";
            }
        }
        html += `<span class="zedhelper-timer-span" tooltip="${timeLeftFormatted}"><a id="zhOpenRaid" style="cursor:pointer;">${timeLeft > 0 ? `<span class="red">🪖&nbsp;${timeLeftFormatted}</span>` : `<span class="green">🪖✅</span>`}</a></span>`;

        /** JUNK STORE */
        const junkStoreLimitSecondsLeft = get('junkStoreLimitSecondsLeft');
        const junkStoreLimitTime = get('junkStoreLimitTime');
        if (junkStoreLimitSecondsLeft && junkStoreLimitTime) {
            timeDiff = (Date.now() - junkStoreLimitTime)/1000;
            timeLeft = junkStoreLimitSecondsLeft - Math.round(timeDiff);
            try {
                timeLeftFormatted = new Date(timeLeft * 1000).toISOString().substr(11, 5);
            } catch (error) {
                timeLeftFormatted = "00:00";
            }
        }
        html += `<span class="zedhelper-timer-span"><a id="zhOpenJunkStore" style="cursor:pointer;">${timeLeft > 0 ? `<span class="red">🏪&nbsp;${timeLeftFormatted}</span>` : `<span class="green">🏪✅</span>`}</a></span>`;
        
        /** ZED MART */
        // const zedMartLimitSecondsLeft = get('zedMartLimitSecondsLeft');
        // const zedMartLimitTime = get('zedMartLimitTime');
        // if (zedMartLimitSecondsLeft && zedMartLimitTime) {
        //     timeDiff = (Date.now() - zedMartLimitTime)/1000;
        //     timeLeft = zedMartLimitSecondsLeft - Math.round(timeDiff);
        //     try {
        //         timeLeftFormatted = new Date(timeLeft * 1000).toISOString().substr(11, 5);
        //     } catch (error) {
        //         timeLeftFormatted = "00:00";
        //     }
        // }
        // html += `<span class="zedhelper-timer-span"><a id="zhOpenZedMart" style="cursor:pointer;">${timeLeft > 0 ? `<span class="red">🏬&nbsp;${timeLeftFormatted}</span>` : `<span class="green">🏬✅</span>`}</a></span>`;

        html += `</div>`;

        timerBar.innerHTML = html;

        const selector = ".q-col-gutter-md.justify-center.items-center.currency-stats";
        log("Searching for statusbar...");
        waitForElement(selector).then((el) => {

            try {
                document.querySelectorAll('.zedhelper-icon-bar').entries().forEach((entry) => {
                    log("Remove entry of icon bar: " + entry[1]);
                    entry[1].remove();
                });
                document.querySelectorAll('.zedhelper-timer-bar').entries().forEach((entry) => {
                    log("Remove entry of timer bar: " + entry[1]);
                    entry[1].remove();
                });
            } catch (error) {
                // eat exception
            }
            log("Appending ZedHelper icon to statusbar + adding new bar for timers!");
            el.appendChild(zedHelperIcon);
            el.parentElement.appendChild(timerBar);

            // document.querySelector(selector).parentElement.appendChild(zedHelperIcon);

            /** Setup event listener to detect clicks on the timer buttons */
            setTimeout(() => {
                document.querySelector('#zhOpenGym').addEventListener('click', (event) => {
                    event.preventDefault();
                    openGym();
                });
                document.querySelector('#zhOpenRadioTower').addEventListener('click', (event) => {
                    event.preventDefault();
                    openRadioTower();
                });
                document.querySelector('#zhOpenScavenge').addEventListener('click', (event) => {
                    event.preventDefault();
                    openScavenge();
                });
                document.querySelector('#zhOpenFactionStorage').addEventListener('click', (event) => {
                    event.preventDefault();
                    openFactionStorage();
                });
                document.querySelector('#zhOpenRaid').addEventListener('click', (event) => {
                    event.preventDefault();
                    openRaid();
                });
                document.querySelector('#zhOpenJunkStore').addEventListener('click', (event) => {
                    event.preventDefault();
                    openJunkStore();
                });
                // document.querySelector('#zhOpenZedMart').addEventListener('click', (event) => {
                //     event.preventDefault();
                //     openZedMart();
                // });
            },10);

        });
    }
    function openGym() {
        log("Opening Gym...");
        // Navigate to Faction
        const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "stronghold");
        link.click();
        waitForElement('div.building-cont').then(() => {
            setTimeout(() => {
                const gymDiv = [...document.querySelectorAll("div.building-cont")].find(el => el.textContent.trim().includes("Gym"));
                gymDiv.click();
            },250);
        });
    }
    function openRadioTower() {
        log("Opening Radio Tower...");
        // Navigate to Faction
        const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "stronghold");
        link.click();
        waitForElement('div.building-cont').then(() => {
            setTimeout(() => {
                const link = [...document.querySelectorAll("div.building-cont")].find(el => el.textContent.trim().includes("Radio Tower"));
                link.click();
            },250);
        });
    }
    function openScavenge() {
        log("Opening Scavenge...");
        // Navigate to Faction
        const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "scavenge");
        link.click();
        waitForElement('.job-cont').then(() => {
            setTimeout(() => {
                const link = [...document.querySelectorAll(".job-cont")].find(el => el.textContent.includes("Scrapyard"));
                link.click();
            },250);
        });
    }
    function openRaid() {
        log("Opening Raid...");
        // Navigate to Faction
        const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "faction");
        link.click();
        // Then click on Raid
        waitForElement('a[href="/raids"]').then((el) => {
            el.click();
        });
    }
    function openFactionStorage() {
        log("Opening Faction Storage...");
        // Navigate to Faction
        const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "faction");
        link.click();
        // Then click on Storage
        //document.querySelector("#q-app > div > div.q-page-container > main > div > div:nth-child(16) > div.row.items-stretch.q-col-gutter-xs > div:nth-child(2) > div > div > div.building-cont.idle.click-event")
        waitForElement('.building-cont').then(() => {
            setTimeout(() => {
                const link = [...document.querySelectorAll(".building-cont")].find(el => el.textContent.includes("Storage"));
                link.click();
            },250);
        });
    }
    function openJunkStore() {
        log("Opening Junk Store...");
        // Navigate to City
        const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "city");
        link.click();
        // Then click on Junk store
        waitForElement('a[data-cy="citymenu-junk-store"]').then((el) => {
            el.click();
        });
    }
    function openZedMart() {
        log("Opening Zed Mart...");
        // Navigate to City
        const link = [...document.querySelectorAll("a.menu-link")].find(a => a.textContent.trim().toLowerCase() === "city");
        link.click();
        // Then click on Zed Mart
        waitForElement('a[data-cy="citymenu-zed-mart"]').then((el) => {
            el.click();
        });
    }
    
    // addZedHelperIconAndTimerBar();









    /** Settings page */

    function showSettingsPage() {
        const selector = "#q-app > div > div.q-page-container > div";
        waitForElement(selector).then((el) => {
            el.style.top = "40%";
            el.style.width = "90%";
            el.style.border = "2px inset #333";
            el.style.padding = "10px";
            el.innerHTML = `
            <h3>ZedHelper Settings</h3>
            <p>Userscript written by <a href="https://www.zed.city/profile/12853">Kvassh</a><br>
            For any questions or feedback, please reach out to me in Zed City or Discord.</p>

            <br><br><hr><br>
            
            <div style="text-align:left;">
                <label>Enable extra nav menu? <input type="checkbox" id="extraNavMenu" name="extraNavMenu" value="true" ${get('extraNavMenu') === true | get('extraNavMenu') === 'true' ? 'checked' : ''}></label>
            </style>

            <br><br>
            <div id="zedhelper-settings-output" style="height:50px; display:block;"> </div>
            
            `;
            setTimeout(() => {
                document.querySelector("#extraNavMenu").addEventListener('change', (event) => {
                    set('extraNavMenu', event.target.checked);
                    document.querySelector('#zedhelper-settings-output').innerHTML = '<b class="green">Settings saved! &check;<br>You might need to refresh page for some settings like the extra nav menu.</b>';
                    setTimeout(() => {
                        document.querySelector('#zedhelper-settings-output').innerHTML = " ";
                    },1000);
                });
            }, 100);
        });
    }










    /** Gym functions */

    function autoPopulateTrainInput() {

        const energy = get("energy");
        if (energy > 5) {
            const trainsAvailable = Math.floor(energy/5);
            log(`Current energy: ${energy} - Autopopulating ${trainsAvailable} into the input fields`);

            waitForElement("input.q-field__native").then(() => {
                
                const inputs = document.querySelectorAll("input.q-field__native");
                for (let input of inputs) {
                    input.value = trainsAvailable;
                    input.dispatchEvent(new Event("input", { bubbles: true }));
                }

            });
        } else {
            log("Current energy is 5 or lower, don't autopopulate input fields");
        }
    }
                    






    /** Scavenge functions */
    function addBulkScavengeButtons() {
        const selector = "#q-app > div > div.q-page-container > main > div > div > div.full-width > div > div.q-mt-lg";
        waitForElement(selector).then(() => {
            // Add buttons for 1, 5, 10, 25, 50, 100 scavenges
            const container = document.querySelector(selector);
            const doScavengeBtn = document.querySelector("#q-app > div > div.q-page-container > main > div > div > div.full-width > div > div.q-mt-lg > button:nth-child(1)");

            const btn5 = document.createElement('button');
            btn5.innerHTML = `<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row"><span class="block">x5</span></span>`;
            btn5.classList = "q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle bg-positive text-white q-btn--actionable q-focusable q-hoverable";
            btn5.style.margin = "5px";
            btn5.addEventListener('click', () => {
                console.log("Clicking 5 times...");
                for (let i = 0; i < 5; i++) {
                    doScavengeBtn.click();
                }
            });
            container.append(btn5);

            const btn30 = document.createElement('button');
            btn30.innerHTML = `<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row"><span class="block">x30</span></span>`;
            btn30.classList = "q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle bg-positive text-white q-btn--actionable q-focusable q-hoverable";
            btn30.style.margin = "5px";
            btn30.addEventListener('click', () => {
                console.log("Clicking 30 times...");
                for (let i = 0; i < 30; i++) {
                    doScavengeBtn.click();
                }
            });
            container.append(btn30);

            const currentRad = get('rad') || 0;
            const btnMax = document.createElement('button');
            btnMax.innerHTML = `<span class="q-btn__content text-center col items-center q-anchor--skip justify-center row"><span class="block">Rad (x${currentRad})</span></span>`;
            btnMax.classList = "q-btn q-btn-item non-selectable no-outline q-btn--standard q-btn--rectangle bg-positive text-white q-btn--actionable q-focusable q-hoverable";
            btnMax.style.margin = "5px";
            btnMax.addEventListener('click', () => {
                console.log(`Current rad: ${currentRad} - Clicking ${currentRad} times...`);
                for (let i = 0; i < currentRad; i++) {
                    doScavengeBtn.click();
                }
            });
            container.append(btnMax);
        });
    }



    /** Radio Tower functions */
    function showTradeValues() {
        try {

            const timeDiff = get("radio_lastupdate") ? (Date.now() - get("radio_lastupdate"))/1000 : 0;
            log(`Trade values last updated: ${timeDiff} sec ago`);
            if (timeDiff > 60*60*12) {
                log("Trade values are old. Please visit the Radio Tower to cache new values.");

                const el = document.createElement('div');
                el.classList.add('radio-warning');
                waitForElement("div.overlay-cont").then(() => {
                    const container = document.querySelector("div.overlay-cont");
                    // document.querySelector("#q-app > div > div.q-page-container > main > div > div:nth-child(11) > div.overlay-cont > div > div > div > div > div.text-center.text-no-bg-light.subtext-large.q-my-md")
                    el.innerHTML = `Radio trades data are old - please refresh <a href="stronghold/2375019">Radio Tower</a> to cache new values.`;
                    container.prepend(el);
                });
                return;
            }

            const trades = JSON.parse(get(`tradeValues`));
            // [{"give":96,"return":460},{"give":1425,"return":11900},{"give":3000,"return":2380}]
            log("Current trades to show:");
            log(trades);

            waitForElement(".q-pa-md").then(() => {
                const tradeContainers = document.querySelectorAll(".q-pa-md");
                let i = 0;
                for (let tradeContainer of tradeContainers) {
                    const valueEl = document.createElement('div');
                    valueEl.classList.add('trade-value');
                    valueEl.innerHTML = `
                    <div style="float:left;">
                        ${trades[i].giveqty} items worth<br><span class="red">$</span> ${formatNumber(trades[i].give)}
                    </div>
                    <div style="float:right;">
                        ${trades[i].returnqty} items worth<br><span class="green">$</span> ${formatNumber(trades[i].return)}
                    </div>
                    <div style="clear:both;font-size:1.2rem;">
                        ${parseInt(trades[i].return) > parseInt(trades[i].give) ? '<span class="green">&check;</span>' : '<span class="red">&cross;</span>'}
                    </div>
                    `;
                    tradeContainer.appendChild(valueEl);
                    i++;
                }

                // Update radiotower last purchased date
                set(`radioTower_last_visited`, Date.now());
            });
        } catch(error) {
            log("No trade values found");
        }
    }

    function saveCurrentTradeValues(data) {
        try {
            const trades = [];
            for (let trade of data.items) {
                // trade -> vars -> items -> <item_requirement_1> -> codename/req_qty
                // trade -> vars -> output -> <item_list-1> -> codename/quantity
                let worthGive = 0;
                let worthReturn = 0;
                let qtyGive = 0;
                let qtyReturn = 0;
                const items = trade.vars.items;
                Object.keys(items).forEach( (key,val) => {
                    const marketValue = JSON.parse(get(`mv_${items[key].codename}`)).marketValue;
                    worthGive += (marketValue*items[key].req_qty);
                    qtyGive += items[key].req_qty;
                });
                const output = trade.vars.output;
                Object.keys(output).forEach( (key,val) => {
                    const marketValue = JSON.parse(get(`mv_${output[key].codename}`)).marketValue;
                    worthReturn += (marketValue*output[key].quantity);
                    qtyReturn += output[key].quantity;
                });
                log(`Trade: ${trade.name} - Give: ${worthGive} - Return: ${worthReturn}`);
                trades.push({ "give": worthGive, "return": worthReturn, "giveqty": qtyGive, "returnqty": qtyReturn });
            }
            set(`tradeValues`, JSON.stringify(trades));
        } catch(error) {
            log("Error saving trade values");
            set(`tradeValues`, null);
        }
    }



    /** Store functions */

    function autoPopulate360Items() {
        const selector = "input[type=number].q-placeholder";
        waitForElement(selector).then(() => {
            const el = document.querySelector(selector);
            el.value = 360;
            el.dispatchEvent(new Event("input", { bubbles: true }));
        });
    }
    function autoPopulateMaxItems() {
        const selector = "input[type=number].q-placeholder";
        waitForElement(selector).then(() => {
            const maxButton = [...document.querySelectorAll("button")].find(btn => btn.textContent.toLowerCase().includes("max"));
            maxButton.click();
            // const el = document.querySelector(selector);
            // el.value = 360;
            // el.dispatchEvent(new Event("input", { bubbles: true }));
        });
    }



    /** Functions related to market/inventory */

    // Function to process inventory items and add prices
    async function addMarketPrices() {

        const items = document.querySelectorAll('.item-row');

        if (!items) {
        log("No inventory items found. Check your selectors.");
        return;
        }

        const mvLastUpdateEl = document.createElement('div');
        mvLastUpdateEl.classList.add('zedhelper-inventory-warning');
        const mvLastUpdated = get('mv_lastupdate');
        if (mvLastUpdated) {
            const timeDiff = (Date.now() - mvLastUpdated)/1000;
            log(`Market values last updated: ${timeDiff} sec ago`);
            if (timeDiff > 60*60*24) {
                log("Market values are older than 24 hours. Please visit the market page to cache new values.");
                mvLastUpdateEl.innerHTML = `Market values are older than a day - please visit the <a href="market">Market</a> page to cache new values.`;
            }
        }
        else {
            log("Market values not cached. Please visit the market page to cache values.");
            mvLastUpdateEl.innerHTML = `
            Market value has not been cached yet.<br>
            Please visit the <a href="market">Market</a> page first to calculate worth on your inventory.
            `;
        }
        const selector = "#q-app > div > div.q-page-container > main > div > div:nth-child(2)";
        waitForElement(selector).then(() => {
            document.querySelector(selector).prepend(mvLastUpdateEl);
        });

        // Delete any existing market value elements
        const existingMarketValues = document.querySelectorAll('.market-price');
        for (let mvEl of existingMarketValues) {
            mvEl.remove();
        }

        for (let item of items) {

            const codename = getCodename(item.querySelector('.q-item__label').innerText);
            let qty = 1;
            try {
                qty = item.querySelector('.item-qty').innerText;
                if (qty.includes("%")) {
                    qty = 1;
                } else {
                    qty = parseInt(qty.replace(/[^0-9]/g, ''));
                }
            } catch (error) {
                // eat exception
            }
            if (Number.isNaN(qty)) {
                qty = 1;
            }
            log(`Adding market value for ${codename} x ${qty}`);
            
            let data = null;
            if(get(`mv_${codename}`)) {
                data = JSON.parse(get(`mv_${codename}`));
            } 

            const priceElement = document.createElement('span');
            priceElement.classList.add('market-price');
            
            if (data !== null) {
                const datetime = new Date(data.tz).toISOString();
                priceElement.innerHTML = `<span title="${datetime}">
<b class="green">$</b> ${formatNumber(data.marketValue * qty)} 
<small>(<b class="green">$</b> ${formatNumber(data.marketValue)})</small>
</span>`;
            } else {
                priceElement.innerHTML = `<span class="gray">N/A</span>`;
            }
            item.querySelector('.q-item__label').appendChild(priceElement);
        }

        // Setup interval to check if inventory list changes
        let firstItemRowCodename = "";
        try {
            firstItemRowCodename = getCodename(items[0].querySelector('.q-item__label').innerText);
        } catch (error) {
            // eat exception
        }

        checkForInventoryUpdates = setInterval(() => {
            let newItems = document.querySelectorAll('.item-row');
            if (newItems.length !== items.length) {
                log("Inventory list has changed. Updating prices...");
                clearInterval(checkForInventoryUpdates);
                checkForInventoryUpdates = null;
                addMarketPrices();
                return;
            }
            let newFirstItemRowCodename = ""; 
            try {
                newFirstItemRowCodename = getCodename(newItems[0].querySelector('.q-item__label').innerText);
            } catch (error) {
                // eat exception
            }
            if (firstItemRowCodename != newFirstItemRowCodename) {
                log("Inventory list has changed. Updating prices...");
                clearInterval(checkForInventoryUpdates);
                checkForInventoryUpdates = null;
                addMarketPrices();
                return;
            }
        },250);
    }
    function showNetworth() {
        const networthVendor = get(`mv_networth_vendor`) || 0;
        const networthMarket = get(`mv_networth_market`) || 0;
        const networthCash = get(`money`) || 0;
        const networth = parseInt(networthMarket) + parseInt(networthCash);

        const existingElement = document.querySelector('.zedhelper-networth');
        if (existingElement) {
            existingElement.remove8();
        }

        const el = document.createElement('div');
        el.classList.add('zedhelper-networth');
        
        el.innerHTML = `
        Networth:
        <span title="Value of items only if sold to vendor: $ ${formatNumber(networthVendor)}">
            <b class="green">$</b> ${formatNumber(networth)}
        </span>
        `;
        
        const selector = "#q-app > div > div.q-page-container > main > div > div:nth-child(2)";
        waitForElement(selector).then(() => {
            document.querySelector(selector).prepend(el);
        });
    }






    /** 
     * Stuff related to faction logs
     */

    function handleFactionNotificationsResponse(json) {
        if (!json?.notify || !Array.isArray(json.notify)) return;

        const parsed = json.notify.map(entry => {
            const { type, data, date, viewed } = entry;
            const timestamp = Number(date);

            // Convert the timestamp (looks like Unix epoch-ish) to a Date
            // but Zed uses a custom epoch, not standard Unix seconds
            // let's test and detect it:
            const dateMs = timestamp < 2000000000 ? timestamp * 1000 : timestamp; // if smaller, assume seconds
            const formattedDate = new Date(dateMs * 1000); // if Zed uses seconds-since-something

            return {
                type,
                viewed,
                date: formattedDate.toLocaleString(),
                rawTimestamp: date,
                user: data.username || null,
                details: data,
            };
        });

        console.log("✅ Parsed Faction Notifications:", parsed);
        return parsed;
    }

    async function parseFactionNotifications(json) {
        if (!json?.notify || !Array.isArray(json.notify)) return [];

        const serverNow = await getServerTime(); // get current Zed City time
        const serverNowMs = serverNow.getTime();

        return json.notify.map(entry => {
            const { type, data, date, viewed } = entry;

            // Zed's date field seems like a seconds-based timestamp in their own epoch
            const zedTimestampSec = Number(date);

            // compute a Date relative to server time
            // assume the largest timestamp in current notifications is roughly 'now'
            const oldestZedTimestamp = Math.max(...json.notify.map(n => n.date));
            const offsetMs = serverNowMs - oldestZedTimestamp * 1000; // rough offset
            const eventDate = new Date(zedTimestampSec * 1000 + offsetMs);

            return {
                type,
                viewed,
                date: eventDate,           // actual JS Date object
                formatted: eventDate.toLocaleString(),
                rawTimestamp: zedTimestampSec,
                user: data.username || null,
                details: data
            };
        });
    }








    /** === Debug Overlay === */
    let debugOverlay, closeBtn, hideTimer;
    function showDebug(msg, replace = false) {
        if (!debugOverlay) {
            debugOverlay = document.createElement('div');
            Object.assign(debugOverlay.style, {
                position: 'fixed',
                bottom: '10px',
                left: '10px',
                background: 'rgba(20,20,20,0.85)',
                color: '#0f0',
                padding: '30px 10px',
                borderRadius: '6px',
                fontSize: '12px',
                fontFamily: 'monospace',
                zIndex: 99999,
                maxHeight: '50vh',
                overflowY: 'auto',
                whiteSpace: 'pre-line',
                transition: 'opacity 1s ease',
                opacity: '1',
            });
            document.body.appendChild(debugOverlay);
            debugOverlay.addEventListener('click', () => {
                debugOverlay.remove(); 
                debugOverlay = null; 
            });
        }

        debugOverlay.style.opacity = '1';
        if (replace) debugOverlay.innerText = '';
        debugOverlay.innerText = `[${new Date().toLocaleTimeString()}] ${msg}\n` + debugOverlay.innerText;

        // Auto-hide after 30s
        clearTimeout(hideTimer);
        hideTimer = setTimeout(() => {
            debugOverlay.style.opacity = '0';
        }, 60000);
    }







    /** TEST
     * 
    let container = document.querySelector('#q-app > div > div.q-page-container > main > div > div.q-infinite-scroll > div.zed-grid.has-title.has-content > div.grid-cont');
    let rows = Array.from(container.querySelectorAll('.tbl-row'));
    let row = rows[0];
    console.log(row);
    let textCol = row.querySelector('.col');
    let dateBox = row.querySelector('.row .col-shrink');
    let timeText = dateBox.textContent.trim().toLowerCase();
    console.log(`timeText: ${timeText}`);
    console.log(`textCol: `, row.querySelector('.col'));
    console.log(`dateBox: `, row.querySelector('.row .col-shrink'));
    */



    let cachedServerTime = null;
    let cachedServerTimeAt = 0;

    /** Parse server time string as UTC Date */
    function parseServerTimeAsUTC(str) {
        // Convert "2025-10-17 10:32:45" => "2025-10-17T10:32:45Z"
        return new Date(str.replace(" ", "T") + "Z");
    }

    /** Get server time directly from Zed API (cached 10 minutes) */
    async function getServerTime() {
        const now = Date.now();
        const cacheValidMs = 10 * 60 * 1000; // 10 minutes

        if (cachedServerTime && (now - cachedServerTimeAt) < cacheValidMs) {
            log("🕒 Using cached server time:", cachedServerTime);
            return new Date(cachedServerTime);
        }

        try {
            log("🕒 Fetching server time from Zed API...");
            const response = await fetch("https://api.zed.city/serverTime", {
                headers: { "accept": "application/json, text/plain, */*" },
                credentials: "include",
            });
            const data = await response.json();

            let serverTime;
            if (data.serverTime) serverTime = parseServerTimeAsUTC(data.serverTime);
            else if (data.server_time) serverTime = parseServerTimeAsUTC(data.server_time);
            else if (data.time) serverTime = new Date(data.time);
            else serverTime = new Date(data); // fallback

            if (isNaN(serverTime.getTime())) {
                throw new Error("Invalid server time received");
            }

            cachedServerTime = serverTime.toISOString();
            cachedServerTimeAt = now;
            return serverTime;

        } catch (err) {
            console.warn("⚠️ Could not fetch server time, fallback to local:", err);
            return new Date();
        }
    }

    function formatServerTimestamp(ts) {
        // Accepts UNIX seconds or milliseconds
        const d = new Date(ts > 1e12 ? ts : ts * 1000);

        // Format as UTC (ZedCity/server time)
        const yyyy = d.getUTCFullYear();
        const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
        const dd = String(d.getUTCDate()).padStart(2, '0');
        const hh = String(d.getUTCHours()).padStart(2, '0');
        const mi = String(d.getUTCMinutes()).padStart(2, '0');
        const ss = String(d.getUTCSeconds()).padStart(2, '0');

        return `${yyyy}-${mm}-${dd} ${hh}:${mi}:${ss}`;
    }

    /** Helper function to check if a time string (like "15 hours ago") belongs to yesterday, with debug */
    async function isFromYesterday(timeText) {
        const serverNow = await getServerTime();
        const lower = timeText.toLowerCase().trim();

        // Keyword checks
        if (lower.includes("yesterday") || lower.includes("a day ago")) return true;
        if (lower.includes("days ago")) return false;

        // Relative time parsing
        const hourMatch = lower.match(/(\d+)\s*hours?\s*ago/);
        const minuteMatch = lower.match(/(\d+)\s*minutes?\s*ago/);
        const secondMatch = lower.match(/(\d+)\s*seconds?\s*ago/);

        let diffMs = 0;
        if (hourMatch) diffMs = parseInt(hourMatch[1], 10) * 60 * 60 * 1000;
        else if (minuteMatch) diffMs = parseInt(minuteMatch[1], 10) * 60 * 1000;
        else if (secondMatch) diffMs = parseInt(secondMatch[1], 10) * 1000;
        else return false; // Could not parse

        // Event time in UTC
        const eventDate = new Date(serverNow.getTime() - diffMs);

        // Compute start of today/yesterday in server's UTC
        const startOfToday = new Date(Date.UTC(
            serverNow.getUTCFullYear(),
            serverNow.getUTCMonth(),
            serverNow.getUTCDate()
        ));
        const startOfYesterday = new Date(startOfToday.getTime() - 24*60*60*1000);

        const classification = eventDate >= startOfYesterday && eventDate < startOfToday ? "Yesterday"
                            : eventDate >= startOfToday ? "Today" : "Older than Yesterday";

        console.log(`🔹 "${timeText}" => eventDate: ${eventDate.toISOString()}, classified as: ${classification}`);
        return classification === "Yesterday";
    }


    /** Clears old faction notification logs */
    function clearFactionLogsCache() {
        set('factionNotifications', "[]");   
    }

    /** Displays yesterday's raid report (from already-filtered localStorage data) */
    async function showRaidReport(pending = false) {

        /* Remove any existing elements first */
        document.querySelectorAll('.zedHelperFactionRaidReport1').forEach(el => el.remove());

        const summary = document.createElement('div');
        Object.assign(summary.style, {
            position: 'fixed',
            bottom: '50px',
            right: '10px',
            background: 'rgba(20, 20, 20, 0.95)',
            color: '#fff',
            padding: '15px',
            borderRadius: '12px',
            boxShadow: '0 0 15px rgba(0,0,0,0.5)',
            fontSize: '14px',
            zIndex: 9999,
            maxHeight: '70vh',
            overflowY: 'auto',
            backdropFilter: 'blur(4px)',
            fontFamily: 'monospace',
            transition: 'opacity 0.3s ease'
        });
        summary.classList = 'zedHelperFactionRaidReport1';

        const closeBtn = document.createElement('div');
        closeBtn.textContent = '×';
        Object.assign(closeBtn.style, {
            position: 'absolute',
            top: '5px',
            right: '10px',
            cursor: 'pointer',
            fontSize: '20px',
            color: '#bbb'
        });
        closeBtn.addEventListener('click', () => summary.remove());
        summary.appendChild(closeBtn);

        document.body.appendChild(summary);
        summary.innerHTML = '⏳ Loading yesterday\'s raids...';
        summary.appendChild(closeBtn);

        if (pending) {
            return;
        }

        const raidRegexes = {
            store: /raid.*store/i,
            farm: /raid.*farm/i,
            hospital: /raid.*hospital/i
        };

        // Load data
        const notifications = JSON.parse(get('factionNotifications') || '[]');
        if (!notifications.length) {
            return;
        }

        // Load members if available
        const factionMembers = JSON.parse(get('factionMembers')) || [];
        const allNames = factionMembers.map(m => m.username);
        const raidCounts = {};

        // Initialize from members (so everyone appears)
        allNames.forEach(name => {
            raidCounts[name] = { store: 0, farm: 0, hospital: 0 };
        });

        // Tally raids
        for (const n of notifications) {
            const user = n.data.username || 'Unknown';
            if (!raidCounts[user]) raidCounts[user] = { store: 0, farm: 0, hospital: 0 };

            for (const [type, regex] of Object.entries(raidRegexes)) {
                if (regex.test(n.data.name)) {
                    raidCounts[user][type]++;
                }
            }
        }

        // Build table
        const rows = Object.entries(raidCounts)
            .map(([name, data]) => `
                <tr>
                    <td style="padding:4px;">${name}</td>
                    <td style="text-align:right;padding:4px;">${data.store}</td>
                    <td style="text-align:right;padding:4px;">${data.farm}</td>
                    <td style="text-align:right;padding:4px;">${data.hospital}</td>
                    <td style="text-align:right;padding:4px;">
                        ${data.store > 0 ? '✅' : (data.farm > 0 || data.hospital > 0 ? '⚠️' : '❌')}
                    </td>
                </tr>
            `).join('');

        summary.innerHTML = `
            <div style="font-weight:bold;margin-bottom:4px;">Yesterday's Raids</div>
            <table style="border-collapse:collapse;width:100%;margin-bottom:8px;">
                <thead>
                    <tr style="border-bottom:1px solid #444;">
                        <th style="text-align:left;padding:4px;">Name</th>
                        <th style="text-align:right;padding:4px;">S</th>
                        <th style="text-align:right;padding:4px;">F</th>
                        <th style="text-align:right;padding:4px;">H</th>
                        <th style="text-align:right;padding:4px;">☐</th>
                    </tr>
                </thead>
                <tbody>${rows}</tbody>
            </table>
        `;
        summary.appendChild(closeBtn);

        // Click-to-copy summary
        summary.addEventListener('click', e => {
            if (e.target === closeBtn) return;
            const text =
                "Yesterday's Raids:\n" +
                Object.entries(raidCounts)
                    .map(([name, d]) =>
                        `${d.store > 0 ? '✅' : (d.farm > 0 || d.hospital > 0 ? '⚠️' : '❌')} ${name}: ` +
                        `Store ${d.store}, Farm ${d.farm}, Hospital ${d.hospital}`)
                    .join('\n');

            navigator.clipboard.writeText(text)
                .then(() => {
                    console.log('✅ Raid report copied to clipboard');
                    summary.style.opacity = '0.6';
                    setTimeout(() => summary.style.opacity = '1', 300);
                })
                .catch(e => console.warn('Unable to copy:', e));
        });
    }

    /** Generates raid report from faction logs (Yesterday only) */
    // async function generateRaidReportFromFactionLogs() {

    //     const summary = document.createElement('div');
    //     Object.assign(summary.style, {
    //         position: 'fixed',
    //         bottom: '50px',
    //         right: '10px',
    //         background: 'rgba(20, 20, 20, 0.95)',
    //         color: '#fff',
    //         padding: '15px',
    //         borderRadius: '12px',
    //         boxShadow: '0 0 15px rgba(0,0,0,0.5)',
    //         fontSize: '14px',
    //         zIndex: 9999,
    //         maxHeight: '70vh',
    //         overflowY: 'auto',
    //         backdropFilter: 'blur(4px)',
    //         fontFamily: 'monospace',
    //         transition: 'opacity 0.3s ease'
    //     });

    //     const closeBtn = document.createElement('div');
    //     closeBtn.textContent = '×';
    //     Object.assign(closeBtn.style, {
    //         position: 'absolute',
    //         top: '5px',
    //         right: '10px',
    //         cursor: 'pointer',
    //         fontSize: '20px',
    //         color: '#bbb'
    //     });
    //     closeBtn.addEventListener('click', () => summary.remove());
    //     document.body.appendChild(summary);
    //     summary.innerHTML = '⏳ Loading data...';
    //     summary.appendChild(closeBtn);

    //     const raidRegexes = {
    //         store: /raid.*store/i,
    //         farm: /raid.*farm/i,
    //         hospital: /raid.*hospital/i
    //     };

    //     let lastProcessedIds = new Set();
    //     let newDataChecks = 0;

    //     const factionMembers = JSON.parse(get('factionMembers')) || [];
    //     if (!factionMembers.length) {
    //         summary.innerHTML = `❌ No faction members stored in memory.<br><br>Please go to <a href="https://www.zed.city/faction/2232340">member list</a> before loading logs.`;
    //         summary.appendChild(closeBtn);
    //         return;
    //     }

    //     const yesterdayRaiders = Object.fromEntries(
    //         factionMembers.map(m => [m.username, { store: 0, farm: 0, hospital: 0 }])
    //     );
    //     console.log(yesterdayRaiders);

    //     const interval = setInterval(async () => {
    //         newDataChecks++;

    //         const notifications = JSON.parse(get('factionNotifications') || '[]').filter(n => n.type === "faction_raid");
    //         if (!notifications.length) {
    //             summary.innerHTML = '❌ No faction notifications stored locally.';
    //             summary.appendChild(closeBtn);
    //             return;
    //         }

    //         // const serverTime = await getServerTime();
    //         // const yesterday = new Date(serverTime);
    //         // yesterday.setDate(yesterday.getDate() - 1);
    //         // yesterday.setHours(0, 0, 0, 0);

    //         const serverTime = await getServerTime();
    //         const yesterdayStart = new Date(serverTime);
    //         yesterdayStart.setDate(yesterdayStart.getDate() - 1);
    //         yesterdayStart.setHours(0, 0, 0, 0);
    //         const yesterdayEnd = new Date(yesterdayStart);
    //         yesterdayEnd.setHours(23, 59, 59, 999);

    //         let newDataFound = false;

    //         console.log(`🕒 Server time: ${serverTime.toISOString()}, Yesterday start: ${yesterdayStart.toISOString()}, Yesterday end: ${yesterdayEnd.toISOString()}`)
    //         console.log(`🔍 Scanning ${notifications.length} faction notifications for yesterday's raids...`);
    //         console.log(notifications);

    //         for (const entry of notifications) {

    //             // if (entry.data.username === "Will") {
    //             //     console.log(JSON.stringify(entry));
    //             // }

    //             const uniqueId = `${entry.data.id}_${entry.date}`;

    //             if (lastProcessedIds.has(uniqueId)) {
    //                 log(`Skipping already processed entry: ${uniqueId}`);
    //                 continue;
    //             }

    //             const entryDate = new Date(entry.date > 1e12 ? entry.date : entry.date * 1000);
    //             console.log(yesterdayStart);                
    //             console.log(yesterdayEnd);                
    //             console.log(entryDate);                
    //             // Only count raids done "yesterday" server time
    //             if (entryDate < yesterdayStart || entryDate > yesterdayEnd) {
    //                 log(`Skipping entry ID: ${entry.data.id} - Outside of yesterday range.`);
    //                 continue;
    //             }

    //             lastProcessedIds.add(uniqueId);
    //             newDataFound = true;

    //             const userName =
    //                 entry.data.username ||
    //                 entry.data.user_name ||
    //                 entry.data.user?.username ||
    //                 entry.data.attacker_name ||
    //                 'Unknown';

    //             if (!yesterdayRaiders[userName]) {
    //                 yesterdayRaiders[userName] = { store: 0, farm: 0, hospital: 0 };
    //             }

    //             for (const [type, regex] of Object.entries(raidRegexes)) {
    //                 if (regex.test(entry.data.name)) {
    //                     yesterdayRaiders[userName][type]++;
    //                 }
    //             }
    //         }

    //         if (newDataFound) {
    //             log(`✅ Updated raid report with new data:`, yesterdayRaiders);
    //             const names = Object.keys(yesterdayRaiders);
    //             const rows = names.map(name => `
    //                 <tr>
    //                     <td style="padding:4px;">${name}</td>
    //                     <td style="text-align:right;padding:4px;">${yesterdayRaiders[name].store}</td>
    //                     <td style="text-align:right;padding:4px;">${yesterdayRaiders[name].farm}</td>
    //                     <td style="text-align:right;padding:4px;">${yesterdayRaiders[name].hospital}</td>
    //                     <td style="text-align:right;padding:4px;">${yesterdayRaiders[name].store > 0 ? '✅' : (yesterdayRaiders[name].farm > 0 || yesterdayRaiders[name].hospital > 0 ? '⚠️' : '❌')}</td>
    //                 </tr>
    //             `).join('');

    //             summary.innerHTML = `
    //                 <div style="font-weight:bold;margin-bottom:4px;">Yesterday's Raids</div>
    //                 <table style="border-collapse:collapse;width:100%;margin-bottom:8px;">
    //                     <thead>
    //                         <tr style="border-bottom:1px solid #444;">
    //                             <th style="text-align:left;padding:4px;">Name</th>
    //                             <th style="text-align:right;padding:4px;">S</th>
    //                             <th style="text-align:right;padding:4px;">F</th>
    //                             <th style="text-align:right;padding:4px;">H</th>
    //                             <th style="text-align:right;padding:4px;">☐</th>
    //                         </tr>
    //                     </thead>
    //                     <tbody>${rows}</tbody>
    //                 </table>
    //             `;
    //             summary.appendChild(closeBtn);
    //         } else {
    //             log(`ℹ️ No new raid data found since last check.`);
    //         }

    //         // Stop after no new rows appear for 2 seconds
    //         if (!newDataFound && newDataChecks > 10) {
    //             log(`Stopping listener after scanning 30 times.`);
    //             clearInterval(interval);
    //         }

    //     }, 3000);


    //     // Copy on click
    //     summary.addEventListener('click', e => {
    //         if (e.target === closeBtn) return;
    //         const yestNames = Object.keys(yesterdayRaiders);
    //         const text =
    //             "Yesterday's Raids:\n" +
    //             (yestNames.length
    //                 ? yestNames.map(n => `${yesterdayRaiders[n].store > 0 ? '✅' : (yesterdayRaiders[n].farm > 0 || yesterdayRaiders[n].hospital > 0 ? '⚠️' : '❌')} ${n}: Store ${yesterdayRaiders[n].store}, Farm ${yesterdayRaiders[n].farm}, Hospital ${yesterdayRaiders[n].hospital}`).join('\n')
    //                 : 'None');

    //         navigator.clipboard.writeText(text)
    //             .then(() => {
    //                 console.log('✅ Raid report copied to clipboard');
    //                 summary.style.opacity = '0.6';
    //                 setTimeout(() => summary.style.opacity = '1', 300);
    //             })
    //             .catch(e => console.warn('Unable to copy:', e));
    //     });

    // }




    async function storeFactionLogs(url, data) {
   
        try {
            const match = url.match(/page=(\d+)/);
            const page = match ? parseInt(match[1]) : 1;

            let existing = JSON.parse(get('factionNotifications') || "[]");

            // Cleanup: remove notifications older than 2 days
            const serverTime = cachedServerTime ? new Date(cachedServerTime) : await getServerTime();

            // Define yesterday range
            const yesterdayStart = new Date(serverTime);
            yesterdayStart.setUTCDate(yesterdayStart.getUTCDate() - 1);
            yesterdayStart.setUTCHours(0, 0, 0, 0);
            const yesterdayEnd = new Date(yesterdayStart);
            yesterdayEnd.setUTCHours(23, 59, 59, 999);

            // Parse new data
            const newData = JSON.parse(data)?.notify || [];
            let newEntries = 0,
                duplicateCount = 0,
                filteredNonRaid = 0,
                filteredOutsideTime = 0,
                totalEntries = 0;

            // Build ID cache
            const existingIds = new Set(existing.map(n =>
                `${n.date}_${n.data.id}_${n.data.username}`
            ));

            // Merge new entries
            for (const n of newData) {
                totalEntries++;
                const id = `${n.date}_${n.data.id}_${n.data.username}`;
                const type = n.type;
                const entryDate = new Date(n.date > 1e12 ? n.date : n.date * 1000);
                // const formatted = entryDate.getFullYear() + '-' +
                //     String(entryDate.getMonth() + 1).padStart(2, '0') + '-' +
                //     String(entryDate.getDate()).padStart(2, '0') + ' ' +
                //     String(entryDate.getHours()).padStart(2, '0') + ':' +
                //     String(entryDate.getMinutes()).padStart(2, '0') + ':' +
                //     String(entryDate.getSeconds()).padStart(2, '0');
                const formatted = formatServerTimestamp(n.date);

                if (type !== 'faction_raid') {
                    showDebug(`❌ ${formatted} - Non raid: ${n.data.username} | ${n.data.name} | ${JSON.stringify(n.data.items)}`);
                    filteredNonRaid++;
                    continue;
                }
                // Time check — only store if within yesterday range
                if (entryDate < yesterdayStart || entryDate > yesterdayEnd) {
                    filteredOutsideTime++;
                    showDebug(`❌ ${formatted} - Out of range: ${n.data.username} | ${n.data.name} | ${JSON.stringify(n.data.items)}`);
                    continue;
                }
                if (existingIds.has(id)) {
                    duplicateCount++;
                    showDebug(`❌ ${formatted} - Duplicate: ${n.data.username} | ${n.data.name} | ${JSON.stringify(n.data.items)}`);
                    continue;
                }
                existing.push(n);
                existingIds.add(id);
                newEntries++;
                showDebug(`✅ ${formatted} - Saved: ${n.data.username} | ${n.data.name} | ${JSON.stringify(n.data.items)}`);
            }

            /* Delay save to avoid flooding */
            setTimeout(() => {
                set('factionNotifications', JSON.stringify(existing));
                const msg = [
                    `📄 Page ${page}`,
                    `${totalEntries} logs`,
                    `+${newEntries} saved`,
                    `${duplicateCount} duplicates`,
                    `${filteredNonRaid} non-raid`,
                    `${filteredOutsideTime} out-of-range`,
                    `(${existing.length} total)`
                ].join(' | ');
                showDebug(msg);
                showDebug("");
                log(msg);
            }, page * 200);

        } catch (err) {
            showDebug(`⚠️ Error parsing notifications: ${err.message}`);
            console.error(err);
        }
                

    }





    
})();

































/* EXAMPLE RESPONSE /getOffers:
    [
        {
            "name":"Advanced Tools",
            "codename":"advanced_tools",
            "type":"resources_craft_basic",
            "quantity":2,
            "value":10,
            "vars":{
                "buy":10,"sell":5,"desc":"","weight":"1","ash_value":"20"
            },
            "market_id":15490,
            "market_price":19500,
            "quantity_sold":3,
            "user":{
                "id":11703,"username":"bump","avatar":"","online":1739285402
            }
        },
    ]
*/

/* EXAMPLE RESPONSE /getMarket
{
    "items": 
    [
        {
            "name":"Advanced Tools",
            "codename":"advanced_tools",
            "type":"resources_craft_basic",
            "quantity":35,
            "value":10,
            "vars": {
                "buy":10,
                "sell":5,
                "desc":"",
                "weight":"1",
                "ash_value":"20"
            },
            "market_id":14020,
            "market_price":19500,
            "quantity_sold":0
        },      
    ]
}   

*/