WhatNot Username Parser

Parse sold events and send them to the system

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         WhatNot Username Parser
// @namespace    http://tampermonkey.net/
// @version      1.29.2
// @description  Parse sold events and send them to the system
// @author       You
// @match        https://www.whatnot.com/dashboard/live/*
// @match        https://www.whatnot.com/live/*
// @match        https://www.whatnot.com/user/*/widgets/breaks
// @match        http://localhost:3000/break/*
// @match        https://whatnot-frontend.vercel.app/break/*
// @match        https://whatnot-frontend-psi.vercel.app/break/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_addStyle
// @run-at document-start
// ==/UserScript==

GM_addStyle(`
.mob-mobile-chat {
    height: 75vh;
}
.mob-chat {
 // position: absolute;
 max-height: 90% !important;
 top: 10% !important;
}
.mob-chat > * > * > div {
 // background-color: rgba(0, 0, 0, 0.75);
    border-radius: 10px;
    padding: 2px; 
}
.mob-price {
 position: absolute;
 z-index: 999;
 background-color: black;
 padding: 1%;
}
.mob-price * {
 font-size: 45px !important;
}
.mob-price-child {
 top: -20px;
 position:relative;
}
.bottom-container {
 background-color: rgba(0, 0, 0, 0);
 height: 100% !important;
}
.mob-chat:first-child {
 height: 78% !important;
 background: '';
}
.mob-last-buyer-container > div:first-of-type {
 position: absolute !important;
 right: 1% !important;
 top: 1% !important;
 width: 50% !important;
}
.mob-last-buyer-container > div:first-of-type > :first-child {
 background-color: black;
}
.mob-chat-parent {
 justify-content: end !important;
}
.mob-online {
 font-size: 25px;
 position: absolute;
 right: 2%;
 top: 2%;
 display: flex;
 align-items: center;
 gap: 10px;
}
.mob-online-icon {
 width: 45px !important;
}
`);

(function() {
    'use strict';

    // Set to true to enable verbose [MOB Debug] console tracing (userscript + injected page layer)
    const MOB_DEBUG = false
    const dbg = (...args) => { if (MOB_DEBUG) console.log('[MOB Debug]', ...args) }

    // Temporarily disabled: Break Parser sending SPOT sale events (the
    // payment_succeeded + randomizer chain) via newEvent. Giveaways are always
    // sent. Set back to true to re-enable spots. Overview sync is unaffected.
    const SEND_SPOT_EVENTS = false

    const currentURL = document.URL.toString()
    dbg('url:', currentURL)

    // --- WebSocket interception ---
    // Must inject a <script> tag to patch WebSocket in the real page context,
    // because Tampermonkey sandboxes the userscript window from the page window.
    const wsFrames = []
    let wsCapturing = false

    let globalLastBreakData = null
    if (currentURL.indexOf('widgets/breaks') !== -1) {
        window.addEventListener('__wnBreakDetails', function(e) {
            globalLastBreakData = e.detail
            dbg('Break Parser early capture stored', globalLastBreakData?.data?.getBreak?.id)
        })
    }

    const DBG_INTERESTING = ['auction_ended', 'payment_succeeded', 'randomizer_result_event', 'giveaway_won', 'break_updated']

    if (MOB_DEBUG) {
        let wsPatchReady = false
        window.addEventListener('__wnWsPatchReady', () => {
            wsPatchReady = true
            dbg('page-context WS patch confirmed running')
        })

        const debugDecodeFrame = detail => {
            const raw = detail && detail.data
            const dataType = typeof raw === 'string' ? 'string' : Object.prototype.toString.call(raw)
            let d = raw
            if (typeof raw === 'string') {
                try { d = JSON.parse(raw) } catch (e) { return { type: '<unparseable string>', topic: '', payload: null, dataType } }
            }
            if (Array.isArray(d) && d.length >= 5) return { type: d[3], topic: d[2], payload: d[4], dataType }
            return { type: '<non-phoenix frame>', topic: '', payload: null, dataType }
        }

        let dbgFrameCount = 0
        window.addEventListener('__wnWsFrame', e => {
            dbgFrameCount++
            const info = debugDecodeFrame(e.detail)
            if (DBG_INTERESTING.indexOf(info.type) !== -1) {
                dbg('interesting frame:', info.type, '| topic=' + info.topic, '| dataType=' + info.dataType, info.payload)
            } else if (info.type === '<unparseable string>' || info.type === '<non-phoenix frame>') {
                dbg('odd frame:', info.type, '| dataType=' + info.dataType)
            }
        })

        setInterval(() => {
            dbg('heartbeat: frames seen=' + dbgFrameCount + ', wsPatchReady=' + wsPatchReady + ', capturing=' + wsCapturing)
            if (!wsPatchReady) {
                console.warn('[MOB Debug] WS patch NOT confirmed — injected <script> likely blocked (CSP?)')
            }
        }, 15000)
    }

    ;(function injectWsPatch() {
        const script = document.createElement('script')
        script.textContent = `(function() {
            if (window.__wnParserWsPatched) return;
            window.__wnParserWsPatched = true;
            var MOB_DEBUG = ${MOB_DEBUG};
            function dbg() { if (MOB_DEBUG) console.log.apply(console, ['[MOB Debug]'].concat(Array.prototype.slice.call(arguments))); }
            var wsConnCount = 0;
            var NativeWS = window.WebSocket;
            function bufToBase64(buf) {
                var s = '', bytes = new Uint8Array(buf);
                for (var i = 0; i < bytes.byteLength; i++) s += String.fromCharCode(bytes[i]);
                return btoa(s);
            }
            // The new widgets/breaks page ships break state as an RSC flight chunk
            // ('N:{"getBreak":{...}}') inside its own route responses instead of a
            // GetBreakDetailsOBS GraphQL call. Extract it and re-emit the old shape.
            function extractJsonObjectAt(text, braceIdx) {
                var depth = 0, inStr = false, esc = false;
                for (var i = braceIdx; i < text.length; i++) {
                    var c = text.charCodeAt(i);
                    if (inStr) {
                        if (esc) esc = false;
                        else if (c === 92) esc = true;
                        else if (c === 34) inStr = false;
                    } else {
                        if (c === 34) inStr = true;
                        else if (c === 123) depth++;
                        else if (c === 125) { depth--; if (depth === 0) return text.slice(braceIdx, i + 1); }
                    }
                }
                return null;
            }
            function extractBreakOverview(text, sourceLabel) {
                var idx = text.indexOf('"getBreak"');
                if (idx === -1) return null;
                var braceIdx = text.lastIndexOf('{', idx);
                if (braceIdx === -1) return null;
                var jsonStr = extractJsonObjectAt(text, braceIdx);
                if (!jsonStr) return null;
                var chunk;
                try { chunk = JSON.parse(jsonStr); } catch(e) {
                    dbg('getBreak chunk found in ' + sourceLabel + ' but JSON.parse failed:', e.message);
                    return null;
                }
                var gb = chunk.getBreak;
                if (!gb || !Array.isArray(gb.spots)) return null;
                var unresolved = false;
                function deref(v) {
                    // RSC dedup refs look like "$1:getBreak:spots:0:buyer"
                    if (typeof v === 'string' && v.charCodeAt(0) === 36) {
                        var parts = v.slice(1).split(':');
                        parts.shift();
                        var cur = chunk;
                        for (var i = 0; i < parts.length; i++) {
                            if (cur == null) { unresolved = true; return null; }
                            cur = cur[parts[i]];
                        }
                        if (cur === undefined) { unresolved = true; return null; }
                        return cur;
                    }
                    return v;
                }
                var spots = gb.spots.map(function(s) {
                    var buyer = deref(s.buyer);
                    return { title: s.title, buyer: buyer && buyer.username ? { username: buyer.username } : null };
                });
                if (unresolved) {
                    console.warn('[MOB] getBreak chunk in ' + sourceLabel + ' has unresolvable buyer refs, skipping overview so customers do not get wiped');
                    return null;
                }
                return { data: { getBreak: { id: String(gb.id), spots: spots } } };
            }
            // Note (researched 2026-08-05): the widgets/breaks RSC payload contains NO
            // per-spot prices — getBreak.spots carry only id/title/description/buyer,
            // and the only soldPrice in the payload is the break's parent product.
            // Spot prices are therefore only available live, via payment_succeeded.
            function PatchedWebSocket() {
                var ws = new (Function.prototype.bind.apply(NativeWS, [null].concat(Array.from(arguments))))();
                var connId = ++wsConnCount;
                var connUrl = arguments[0];
                dbg('[WS Capture] new connection #' + connId + ':', connUrl);
                ws.addEventListener('message', function(e) {
                    var raw = e.data, frame;
                    if (typeof raw === 'string') {
                        frame = { connId: connId, connUrl: connUrl, ts: Date.now(), encoding: 'text', data: raw };
                        window.dispatchEvent(new CustomEvent('__wnWsFrame', { detail: frame }));
                    } else if (raw instanceof ArrayBuffer) {
                        frame = { connId: connId, connUrl: connUrl, ts: Date.now(), encoding: 'base64', data: bufToBase64(raw) };
                        window.dispatchEvent(new CustomEvent('__wnWsFrame', { detail: frame }));
                    } else if (raw instanceof Blob) {
                        raw.arrayBuffer().then(function(buf) {
                            frame = { connId: connId, connUrl: connUrl, ts: Date.now(), encoding: 'base64', data: bufToBase64(buf) };
                            window.dispatchEvent(new CustomEvent('__wnWsFrame', { detail: frame }));
                        });
                    } else {
                        dbg('WS frame with UNEXPECTED data type:', Object.prototype.toString.call(raw), raw);
                        frame = { connId: connId, connUrl: connUrl, ts: Date.now(), encoding: 'object', data: raw };
                        window.dispatchEvent(new CustomEvent('__wnWsFrame', { detail: frame }));
                    }
                });
                return ws;
            }
            PatchedWebSocket.prototype = NativeWS.prototype;
            PatchedWebSocket.CONNECTING = NativeWS.CONNECTING;
            PatchedWebSocket.OPEN = NativeWS.OPEN;
            PatchedWebSocket.CLOSING = NativeWS.CLOSING;
            PatchedWebSocket.CLOSED = NativeWS.CLOSED;
            window.WebSocket = PatchedWebSocket;
            dbg('[WS Capture] WebSocket patched in page context');
            window.dispatchEvent(new CustomEvent('__wnWsPatchReady'));
            if (!window.__wnParserFetchPatched) {
                window.__wnParserFetchPatched = true;
                var _fetch = window.fetch;
                window.fetch = function() {
                    var args = arguments;
                    var url = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url) || '';
                    var p = _fetch.apply(this, args);
                    if (typeof url === 'string' && url.indexOf('/widgets/breaks') !== -1) {
                        p.then(function(res) {
                            var clone = res.clone();
                            clone.text().then(function(text) {
                                var overview = extractBreakOverview(text, 'widgets/breaks refresh');
                                if (overview) {
                                    dbg('break overview from widgets/breaks refresh: break ' + overview.data.getBreak.id + ', ' + overview.data.getBreak.spots.length + ' sold spots');
                                    window.dispatchEvent(new CustomEvent('__wnBreakDetails', { detail: overview }));
                                }
                            }).catch(function(){});
                        }).catch(function(){});
                    }
                    return p;
                };
            }
            if (location.pathname.indexOf('/widgets/breaks') !== -1) {
                var scanInitialFlight = function(label) {
                    try {
                        var chunks = (self.__next_f || []).map(function(f) { return f && f[1]; }).filter(function(s) { return typeof s === 'string'; }).join('');
                        var overview = extractBreakOverview(chunks, 'initial page data (' + label + ')');
                        if (overview) {
                            dbg('break overview from initial page data (' + label + '): break ' + overview.data.getBreak.id + ', ' + overview.data.getBreak.spots.length + ' sold spots');
                            window.dispatchEvent(new CustomEvent('__wnBreakDetails', { detail: overview }));
                        } else {
                            dbg('no getBreak in initial page data (' + label + '), scanned ' + chunks.length + ' chars');
                        }
                    } catch(e) { dbg('initial flight scan error:', e); }
                };
                if (document.readyState === 'loading') {
                    document.addEventListener('DOMContentLoaded', function() { setTimeout(function() { scanInitialFlight('DOMContentLoaded+2s'); }, 2000); });
                } else {
                    setTimeout(function() { scanInitialFlight('late'); }, 2000);
                }
            }
        })();`
        document.documentElement.appendChild(script)
        script.remove()
    })()

    window.addEventListener('__wnWsFrame', e => {
        if (!wsCapturing) return
        wsFrames.push(e.detail)
    })

    function getElementByXpath(path) {
        return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    }

    function createToolsNode() {
        var parentNode = document.createElement('div');
        parentNode.style.position = 'fixed';
        parentNode.style.top = '50%';
        parentNode.style.right = '0';
        parentNode.style.transform = 'translateY(-50%)';
        parentNode.style.backgroundColor = 'lightgray';
        parentNode.style.padding = '10px';
        parentNode.style.fontSize = '2em'; // 2 times bigger font size
        parentNode.style.zIndex = '2147483000'; // Set a high z-index
        document.documentElement.appendChild(parentNode);
        return parentNode
    }

    // Add a dropdown list for selecting tools
    function createToolSelector(parentNode) {
        var toolSelector = document.createElement('select');
        toolSelector.style.marginBottom = '10px'; // Add some margin for spacing
        // Whatnot's reset strips the native select chrome, so draw it ourselves
        toolSelector.style.appearance = 'auto';
        toolSelector.style.webkitAppearance = 'auto';
        toolSelector.style.border = '2px solid #555';
        toolSelector.style.borderRadius = '4px';
        toolSelector.style.padding = '2px 6px';
        toolSelector.style.backgroundColor = 'white';
        parentNode.appendChild(toolSelector);

        var button = document.createElement('button')
        button.textContent = 'X'
        parentNode.appendChild(button);

        button.addEventListener('click', function () {
            parentNode.remove()
        })

        var toolContainer = document.createElement('div')
        parentNode.appendChild(toolContainer);

        // Define tool options
        var toolOptions = [
            { name: 'Username Parser', tool: createUsernameParserTool },
            { name: 'WS Parser', tool: createWSParserTool },
            { name: 'WS Capture', tool: createWSCaptureTool },
            { name: 'Break Parser', tool: createBreakParserTool },
            { name: 'Chat Only', tool: createChatOnlyTool },
            { name: 'Notes', tool: createNotesTool},
            { name: 'Giveaway Alarm', tool: createGiveawayAlarmTool}
        ];

        // Populate dropdown options
        toolOptions.forEach(function(option) {
            var optionElement = document.createElement('option');
            optionElement.value = option.name;
            optionElement.textContent = option.name;
            toolSelector.appendChild(optionElement);
        });

        // Function to hide all tool interfaces
        function hideAllToolInterfaces() {
            // Remove all child nodes from the parent node
            while (toolContainer.firstChild) {
                toolContainer.removeChild(toolContainer.firstChild);
            }
        }

        toolSelector.addEventListener('change', function() {
            var selectedTool = toolOptions.find(option => option.name === toolSelector.value);
            hideAllToolInterfaces()
            selectedTool.tool(toolContainer);
        });
        const defaultTool = currentURL.indexOf('widgets/breaks') !== -1
            ? toolOptions.find(t => t.name === 'Break Parser')
            : toolOptions[0]
        toolSelector.value = defaultTool.name
        defaultTool.tool(toolContainer);
    }

    function initUI() {
        let toolsNode = createToolsNode()
        createToolSelector(toolsNode)
    }
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initUI)
    } else {
        initUI()
    }

    function createUsernameParserTool(parentNode) {
        function start() {
            if (currentURL.indexOf('live') !== -1) {
                var element = null;
                var usernameList = []

                const Teams = [
                    "Arizona Cardinals",
                    "Atlanta Falcons",
                    "Baltimore Ravens",
                    "Buffalo Bills",
                    "Carolina Panthers",
                    "Chicago Bears",
                    "Cincinnati Bengals",
                    "Cleveland Browns",
                    "Dallas Cowboys",
                    "Denver Broncos",
                    "Detroit Lions",
                    "Green Bay Packers",
                    "Houston Texans",
                    "Indianapolis Colts",
                    "Jacksonville Jaguars",
                    "Kansas City Chiefs",
                    "Las Vegas Raiders",
                    "Los Angeles Chargers",
                    "Los Angeles Rams",
                    "Miami Dolphins",
                    "Minnesota Vikings",
                    "New England Patriots",
                    "New Orleans Saints",
                    "New York Giants",
                    "New York Jets",
                    "Philadelphia Eagles",
                    "Pittsburgh Steelers",
                    "San Francisco 49ers",
                    "Seattle Seahawks",
                    "Tampa Bay Buccaneers",
                    "Tennessee Titans",
                    "Washington Commanders"
                ]
                function isATeamGiveaway(value) {
                    return Teams.filter(i => value.indexOf(i) !== -1).length > 0
                }

                let prev = null
                let prevMouseHandler = null
                let id = null

                let teamIds = new Map();
                let giveawayIds = new Map();

                function checkForSoldButton() {
                    let buttons = Array.from(document.querySelectorAll('h5'))
                    let optionsButtons = buttons.filter(i => i.textContent == 'Sold')
                    let soldCategory = optionsButtons.length > 0 ? optionsButtons[0] : null
                    if (soldCategory === null) {
                        console.log("didn't find sold category")
                        return;
                    }
                    if (prev === soldCategory) return;
                    console.log('found sold category', soldCategory)
                    console.log(soldCategory)
                    soldCategory.style.backgroundColor = 'green';
                    console.log("Username parser is init")

                    function mouseHandler() {
                        clearInterval(id)
                        let latestScheduleTime = 0
                        const processedSoldNames = new Set()
                        const perItemObservers = new Map()

                        function setupRandomizingObserver(divItem, divListingItem, log) {
                            if (perItemObservers.has(divItem)) {
                                log('per-item observer already set up for this node')
                                return
                            }

                            const randomizingBadge = document.createElement('div')
                            randomizingBadge.textContent = 'Randomizing...'
                            randomizingBadge.style.backgroundColor = 'orange'
                            randomizingBadge.style.color = 'white'
                            randomizingBadge.style.padding = '5px'
                            randomizingBadge.style.borderRadius = '5px'
                            divListingItem.appendChild(randomizingBadge)

                            function cleanup() {
                                itemObserver.disconnect()
                                clearTimeout(safetyTimeout)
                                perItemObservers.delete(divItem)
                            }

                            const safetyTimeout = setTimeout(() => {
                                log('per-item observer safety timeout reached, disconnecting')
                                cleanup()
                            }, 60000)

                            const itemObserver = new MutationObserver(() => {
                                try {
                                    if (divItem.childNodes.length <= 0) return
                                    const dli = divItem.childNodes[0]
                                    if (dli.childNodes.length <= 0) return
                                    const ddf = dli.childNodes[0]
                                    if (ddf.childNodes.length <= 0) return
                                    const df = ddf.childNodes[1]
                                    if (!df || df.childNodes.length <= 6) return

                                    const content = df.textContent
                                    if (/randomizing/i.test(content) || content.indexOf('Pending') !== -1) return

                                    const contentMatch = content.match(/^(.+?)Qty:\s*\d+Buyer:\s*(.+?)Sold for \$(\d+)/)
                                    if (!contentMatch) return

                                    const soldName = contentMatch[1].trim()
                                    const username = contentMatch[2].trim()
                                    const price = parseInt(contentMatch[3])

                                    cleanup()
                                    randomizingBadge.remove()

                                    if (processedSoldNames.has(soldName)) {
                                        log('per-item observer: duplicate soldName', soldName)
                                        const dupElement = document.createElement('div')
                                        dupElement.textContent = 'Duplicate detected'
                                        dupElement.style.backgroundColor = 'orange'
                                        dupElement.style.color = 'white'
                                        dupElement.style.padding = '5px'
                                        dupElement.style.borderRadius = '5px'
                                        divListingItem.appendChild(dupElement)
                                        return
                                    }
                                    processedSoldNames.add(soldName)

                                    let entity
                                    let wasSent = false
                                    if (soldName.toLowerCase().indexOf('giveaway') !== -1) {
                                        entity = { customer: username, price: 0, name: soldName }
                                        const id = soldName.split('#')[1] || soldName
                                        if (giveawayIds.has(id)) wasSent = true
                                        giveawayIds.set(id, true)
                                        log('per-item observer: parsed giveaway id', id)
                                    } else {
                                        entity = { customer: username, price: price, name: soldName }
                                        const id = soldName.split('#')[1] || soldName
                                        if (teamIds.has(id)) wasSent = true
                                        teamIds.set(id, true)
                                        log('per-item observer: parsed team id', id)
                                    }

                                    const badge = document.createElement('div')
                                    badge.style.color = 'white'
                                    badge.style.padding = '5px'
                                    badge.style.borderRadius = '5px'

                                    if (wasSent) {
                                        badge.textContent = 'Already added'
                                        badge.style.backgroundColor = 'red'
                                        divListingItem.appendChild(badge)
                                        log('per-item observer: skip, already added', entity.name)
                                    } else {
                                        badge.textContent = 'Sent'
                                        badge.style.backgroundColor = 'green'
                                        divListingItem.appendChild(badge)
                                        log('per-item observer: setting entity to', entity)
                                        GM_setValue('newEvent', entity)
                                    }
                                } catch(e) {
                                    log('per-item observer error:', e)
                                }
                            })

                            itemObserver.observe(divItem, { subtree: true, childList: true, characterData: true })
                            perItemObservers.set(divItem, cleanup)
                            log('per-item observer set up for', divItem)
                        }

                        const observer = new MutationObserver(mutationsList => {
                            const sid = Math.random().toString(36).slice(2, 7)
                            const log = (...args) => console.log(`[${sid}]`, ...args)
                            log.sid = sid
                            log('mut list', mutationsList)
                            // Loop through each mutation in the mutationsList
                            for (let mutation of mutationsList) {
                                // Check if nodes were added
                                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                                    log('muts', mutation)
                                    // Process the added nodes
                                    mutation.addedNodes.forEach(addedNode => {
                                        const sid = log.sid + '.' + Math.random().toString(36).slice(2, 7)
                                        try {
                                            const log = (...args) => console.log(`[${sid}]`, ...args)
                                            log.sid = sid
                                            let divItem = addedNode
                                            let attributes = divItem.attributes
                                            let index = attributes.getNamedItem('data-index')
                                            let isParsed = addedNode.getAttribute('data-parsed');
                                            log('new added node', addedNode, index)
                                            if (isParsed) {
                                                log('parsed already')
                                                return
                                            }
                                            addedNode.setAttribute('data-parsed', 'true');
                                            log('parsing', addedNode)
                                            const myTime = Date.now()
                                            latestScheduleTime = myTime
                                            setTimeout(() => {
                                                const sid = log.sid + '.' + Math.random().toString(36).slice(2, 7)
                                                try {
                                                    const log = (...args) => console.log(`[${sid}]`, ...args)
                                                    log.sid = sid
                                                    if (myTime < latestScheduleTime) {
                                                        log('stale timeout blocked, scheduled at', myTime, 'latest is', latestScheduleTime)
                                                        return
                                                    }
                                                    const sentElement = document.createElement('div');
                                                    if (divItem.childNodes.length <= 0) {
                                                        log('wrong node 1', divItem)
                                                        return
                                                    }
                                                    let divListingItem = divItem.childNodes[0]
                                                    if (divListingItem.childNodes.length <= 0) {
                                                        log('wrong node 2', divListingItem)
                                                        return
                                                    }
                                                    let divDisplayFlex = divListingItem.childNodes[0]
                                                    if (divDisplayFlex.childNodes.length <= 0) {
                                                        log('wrong node 3', divDisplayFlex)
                                                        return
                                                    }
                                                    let divFlex = divDisplayFlex.childNodes[1]
                                                    if (divFlex.childNodes.length <= 0) {
                                                        log('wrong node 4', divFlex)
                                                        return
                                                    }

                                                    let entity = {customer: '?', price: 0, name: ''}

                                                    let wasSent = false
                                                    if (divFlex.childNodes.length > 6) {
                                                       let content = divFlex.textContent
                                                       log("content is", content)

                                                       if (/randomizing/i.test(content)) {
                                                           log('content is randomizing, setting up per-item observer')
                                                           setupRandomizingObserver(divItem, divListingItem, log)
                                                           return
                                                       }

                                                       if (content.indexOf("Pending") !== -1) {
                                                           log("content contains Pending, skipping", content)
                                                           return
                                                       }

                                                       let contentMatch = content.match(/^(.+?)Qty:\s*\d+Buyer:\s*(.+?)Sold for \$(\d+)/)
                                                       if (!contentMatch) {
                                                           log("failed to parse content", content)
                                                           return
                                                       }

                                                       let soldName = contentMatch[1].trim()
                                                       let username = contentMatch[2].trim()
                                                       let price = parseInt(contentMatch[3])

                                                       if (processedSoldNames.has(soldName)) {
                                                           log('already processed soldName, skipping', soldName)
                                                           const dupElement = document.createElement('div');
                                                           dupElement.textContent = 'Duplicate detected';
                                                           dupElement.style.backgroundColor = 'blue';
                                                           dupElement.style.color = 'white';
                                                           dupElement.style.padding = '5px';
                                                           dupElement.style.borderRadius = '5px';
                                                           divListingItem.appendChild(dupElement);
                                                           return
                                                       }
                                                       processedSoldNames.add(soldName)

                                                       log("found name", soldName, ", is givy:", soldName.toLowerCase().indexOf("giveaway") != -1)
                                                       if (soldName.toLowerCase().indexOf("giveaway") !== -1) {
                                                           entity = {customer: username, price: 0, name: soldName}
                                                           let id = soldName.split('#')[1] || soldName
                                                           if (giveawayIds.has(id)) {
                                                               wasSent = true
                                                           }
                                                           giveawayIds.set(id, true)
                                                           log("parsed giveaway id is", id)
                                                       } else {
                                                           entity = {customer: username, price: price, name: soldName}
                                                           let id = soldName.split('#')[1] || soldName
                                                           if (teamIds.has(id)) {
                                                               wasSent = true
                                                           }
                                                           teamIds.set(id, true)
                                                           log("parsed team id is", id)
                                                       }
                                                    } else {
                                                        log("skip, invalid node", divFlex)
                                                        return;
                                                    }

                                                    if (wasSent) {
                                                        // Set the text content
                                                        sentElement.textContent = 'Already added';

                                                        // Set the styles
                                                        sentElement.style.backgroundColor = 'red';
                                                        sentElement.style.color = 'white';
                                                        sentElement.style.padding = '5px';
                                                        sentElement.style.borderRadius = '5px';

                                                        // Append the new element to the existing element
                                                        divListingItem.appendChild(sentElement);
                                                        log("skip, already added", entity.name)
                                                        return
                                                    } else {
                                                        // Set the text content
                                                        sentElement.textContent = 'Sent';

                                                        // Set the styles
                                                        sentElement.style.backgroundColor = 'green';
                                                        sentElement.style.color = 'white';
                                                        sentElement.style.padding = '5px';
                                                        sentElement.style.borderRadius = '5px';

                                                        // Append the new element to the existing element
                                                        divListingItem.appendChild(sentElement);
                                                        log('setting entity to', entity)
                                                        log('old value was', GM_getValue('newEvent'))
                                                        GM_setValue('newEvent', entity)
                                                    }
                                                } catch (e) {
                                                    log('element is preparing:', e)
                                                }
                                            }, 2000)
                                        } catch(e) {
                                            log('an error occured:', e)
                                        }
                                    });
                                }
                                if (mutation.type === 'childList' && mutation.removedNodes.length > 0) {
                                    mutation.removedNodes.forEach(removedNode => {
                                        if (perItemObservers.has(removedNode)) {
                                            perItemObservers.get(removedNode)()
                                            log('disconnected per-item observer for removed node', removedNode)
                                        }
                                    })
                                }
                            }
                        });


                        setTimeout(() => {
                            let eventLog = document.querySelector('[data-testid=virtuoso-item-list]');
                            console.log(eventLog)
                            if (!eventLog) {
                                console.log('event log is not found')
                                return
                            }
                            observer.observe(eventLog, {
                                childList: true,
                            });
                            console.log("New event observer is started")
                        }, 5000)
                        soldCategory.style.backgroundColor = 'red';
                        console.log("New event observer is init")
                    }
                    if (prev != null && prevMouseHandler != null) {
                        prev.removeEventListener('click', prevMouseHandler)
                        prev.style.backgroundColor = '';
                    }
                    prev = soldCategory
                    prevMouseHandler = mouseHandler
                    soldCategory.addEventListener('click', mouseHandler)
                }

                checkForSoldButton();
                id = setInterval(checkForSoldButton, 100);
                console.log("Username sender is started")
            } else {
                function setReactInput(node, value) {
                    const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
                        window.HTMLInputElement.prototype,
                        'value').set;
                    nativeInputValueSetter.call(node, value);
                    const event = new Event('input', { bubbles: true });
                    node.dispatchEvent(event);
                }

                const newEventEvent = 'new_event_event'
                let price = 1
                setInterval(() => {
                    let event = GM_getValue('newEvent', null)
                    if (!event) {
                        console.log('new event not found yet')
                        return
                    }
                    console.log('got event', event)
                    GM_setValue("newEvent", null)
                    window.dispatchEvent(new CustomEvent(newEventEvent, { detail: {event: event} }));
                }, 1500)
                const breakOverviewEvent = 'break_overview_event'
                setInterval(() => {
                    let breakOverview = GM_getValue('BreakOverview', null)
                    if (!breakOverview) {
                        console.log('new event not found yet')
                        return
                    }
                    console.log('got break overview', breakOverview)
                    GM_setValue("BreakOverview", null)
                    window.dispatchEvent(new CustomEvent(breakOverviewEvent, {detail: breakOverview}));
                }, 1500)
                console.log("Username receiver is started")
                parentNode.removeChild(parentDiv)
            }
        }

        // Create a new div for the quantity tool
        var parentDiv = document.createElement('div');
        // parentDiv.style.border = '1px solid black'; // Add border
        parentDiv.style.padding = '10px'; // Add padding for spacing

        // Create a button
        const dButton = document.createElement('button');
        dButton.textContent = 'Turn On';
        dButton.addEventListener('mouseenter', () => { !dButton.disabled && (dButton.style.color = 'white') })
        dButton.addEventListener('mouseleave', () => { !dButton.disabled && (dButton.style.color = '') })
        parentDiv.appendChild(dButton);

        dButton.addEventListener('click', async () => {
            dButton.disabled = true
            dButton.textContent = 'Is active';
            dButton.style.backgroundColor = 'red'
            dButton.style.color = 'white'
            start()
        })

        parentNode.appendChild(parentDiv)
    }

    function createWSParserTool(parentNode) {
        const processedListings = new Set()
        let isActive = false
        let eventCount = 0
        let logEl = null  // set once UI is built

        function uiLog(msg) {
            console.log('[WS Parser]', msg)
            if (!logEl) return
            const line = document.createElement('div')
            line.textContent = new Date().toLocaleTimeString() + ' |  ' + msg
            line.style.borderBottom = '1px solid #333'
            line.style.padding = '2px 0'
            logEl.appendChild(line)
            logEl.scrollTop = logEl.scrollHeight
        }

        const sendQueue = []
        let queueInterval = null

        function enqueue(entity) {
            sendQueue.push(entity)
            uiLog('queued "' + entity.name + '" [queue: ' + sendQueue.length + ']')
            if (!queueInterval) {
                queueInterval = setInterval(flushQueue, 300)
            }
        }

        function flushQueue() {
            if (sendQueue.length === 0) {
                clearInterval(queueInterval)
                queueInterval = null
                return
            }
            if (GM_getValue('newEvent', null) !== null) {
                uiLog('queue: slot busy, retrying [' + sendQueue.length + ' pending]')
                return
            }
            const entity = sendQueue.shift()
            GM_setValue('newEvent', entity)
            eventCount++
            uiLog('queue: sent "' + entity.name + '" [' + sendQueue.length + ' remaining]')
        }

        const callbacks = new Map()

        function on(type, cb) {
            if (!callbacks.has(type)) callbacks.set(type, new Set())
            callbacks.get(type).add(cb)
        }

        function off(type, cb) {
            callbacks.get(type)?.delete(cb)
        }

        function onFrame(e) {
            const raw = e.detail.data
            let d
            if (typeof raw === 'string') {
                try { d = JSON.parse(raw) } catch(err) {
                    dbg('WS Parser: JSON.parse failed on string frame:', String(raw).slice(0, 150))
                    return
                }
            } else {
                d = raw
            }
            if (!Array.isArray(d) || d.length < 5) return
            const type = d[3], payload = d[4]
            if (MOB_DEBUG && DBG_INTERESTING.indexOf(type) !== -1) {
                dbg('WS Parser: got', type, '| isActive=' + isActive + ' | listeners=' + (callbacks.get(type)?.size || 0))
            }
            if (!isActive) return
            callbacks.get(type)?.forEach(cb => cb(payload))
        }

        window.addEventListener('__wnWsFrame', onFrame)

        on('auction_ended', payload => {
            const productId = payload?.product?.id
            if (!productId || processedListings.has(productId)) {
                dbg('auction_ended skipped:', !productId ? 'no product.id in payload' : 'duplicate product.id ' + productId)
                return
            }
            const listingId = payload?.product?.listingId
            processedListings.add(productId)
            const isBreakSpot = !!payload.product.isBreakSpot
            const name = payload?.product?.name
            if (isBreakSpot) {
                uiLog('Break auction ' + name + 'just ended with id ' + listingId)
            } else {
                uiLog('Auction ' + name + 'just ended with id ' + listingId)
            }

            function onAuctionPayment(pmPayload) {
                if (pmPayload?.product?.id !== productId) {
                    dbg('payment_succeeded for product', pmPayload?.product?.id, '(listing', pmPayload?.product?.listingId + ') does not match awaited product', productId, '- ignoring')
                    return
                }
                off('payment_succeeded', onAuctionPayment)
                const price = pmPayload.product.soldPrice.amount / 100

                if (!isBreakSpot) {
                    const entity = {
                        customer: payload.product.purchaserUser.username,
                        price,
                        name: payload.product.name
                    }
                    uiLog('Sold auction "' + entity.name + '" to ' + entity.customer + ' for $' + entity.price)
                    enqueue(entity)
                    return
                }

                const paymentListingId = pmPayload.product.listingId
                uiLog('Payment confirmed for id ' + listingId + ', waiting for randomizer result')

                function onRandomizerResult(rrPayload) {
                    if (String(rrPayload.listing_id) !== String(paymentListingId)) {
                        dbg('randomizer_result_event listing', rrPayload.listing_id, 'does not match awaited listing', paymentListingId, '- ignoring')
                        return
                    }
                    off('randomizer_result_event', onRandomizerResult)
                    const entity = {
                        customer: rrPayload.buyer_username,
                        price,
                        name: rrPayload.result
                    }
                    uiLog('Sold break auction "' + entity.name + '" to ' + entity.customer + ' for $' + entity.price)
                    enqueue(entity)
                }
                on('randomizer_result_event', onRandomizerResult)
            }
            on('payment_succeeded', onAuctionPayment)
        })

        on('giveaway_won', payload => {
            const listingId = payload?.product?.listingId
            if (!listingId || processedListings.has(listingId)) {
                dbg('giveaway_won skipped:', !listingId ? 'no product.listingId in payload' : 'duplicate listingId ' + listingId)
                return
            }
            processedListings.add(listingId)
            const entity = {
                customer: payload.product.purchaserUser.username,
                price: 0,
                name: payload.product.name
            }
            uiLog('Given away a giveaway "' + entity.name + '" to ' + entity.customer)
            enqueue(entity)
        })

        // UI
        const parentDiv = document.createElement('div')
        parentDiv.style.padding = '10px'
        parentDiv.style.display = 'flex'
        parentDiv.style.flexDirection = 'column'
        parentDiv.style.gap = '6px'

        const statusLabel = document.createElement('div')
        statusLabel.textContent = 'Status: inactive'
        statusLabel.style.fontSize = '0.75em'
        parentDiv.appendChild(statusLabel)

        const eventCountLabel = document.createElement('div')
        eventCountLabel.textContent = 'Events sent: 0'
        eventCountLabel.style.fontSize = '0.75em'
        parentDiv.appendChild(eventCountLabel)

        const startBtn = document.createElement('button')
        startBtn.textContent = 'Start'
        startBtn.style.backgroundColor = '#2196F3'
        startBtn.style.color = 'white'
        startBtn.style.border = 'none'
        startBtn.style.padding = '4px 8px'
        startBtn.style.borderRadius = '4px'
        startBtn.style.cursor = 'pointer'
        parentDiv.appendChild(startBtn)

        startBtn.addEventListener('click', () => {
            isActive = !isActive
            startBtn.textContent = isActive ? 'Stop' : 'Start'
            startBtn.style.backgroundColor = isActive ? '#f44336' : '#2196F3'
            statusLabel.textContent = 'Status: ' + (isActive ? 'active' : 'inactive')
        })

        setInterval(() => {
            eventCountLabel.textContent = 'Events sent: ' + eventCount
        }, 500)

        logEl = document.createElement('div')
        logEl.style.marginTop = '6px'
        logEl.style.maxHeight = '200px'
        logEl.style.overflowY = 'auto'
        logEl.style.fontSize = '0.65em'
        logEl.style.fontFamily = 'monospace'
        logEl.style.backgroundColor = '#111'
        logEl.style.color = '#ddd'
        logEl.style.padding = '4px'
        logEl.style.borderRadius = '4px'
        logEl.style.minHeight = '40px'
        parentDiv.appendChild(logEl)

        parentNode.appendChild(parentDiv)
    }

    function createWSCaptureTool(parentNode) {
        const parentDiv = document.createElement('div')
        parentDiv.style.padding = '10px'
        parentDiv.style.display = 'flex'
        parentDiv.style.flexDirection = 'column'
        parentDiv.style.gap = '6px'

        const statusLabel = document.createElement('div')
        statusLabel.textContent = 'Status: idle'
        statusLabel.style.fontSize = '0.75em'
        parentDiv.appendChild(statusLabel)

        const frameCountLabel = document.createElement('div')
        frameCountLabel.textContent = 'Frames: 0'
        frameCountLabel.style.fontSize = '0.75em'
        parentDiv.appendChild(frameCountLabel)

        const connLabel = document.createElement('div')
        connLabel.textContent = 'Connections: 0'
        connLabel.style.fontSize = '0.75em'
        parentDiv.appendChild(connLabel)

        function makeButton(text, color) {
            const btn = document.createElement('button')
            btn.textContent = text
            btn.style.backgroundColor = color
            btn.style.color = 'white'
            btn.style.border = 'none'
            btn.style.padding = '4px 8px'
            btn.style.borderRadius = '4px'
            btn.style.cursor = 'pointer'
            return btn
        }

        const startBtn = makeButton('Start Capture', '#2196F3')
        const clearBtn = makeButton('Clear', '#888')
        const exportBtn = makeButton('Export JSON', '#4CAF50')

        parentDiv.appendChild(startBtn)
        parentDiv.appendChild(clearBtn)
        parentDiv.appendChild(exportBtn)

        startBtn.addEventListener('click', () => {
            wsCapturing = !wsCapturing
            startBtn.textContent = wsCapturing ? 'Stop Capture' : 'Start Capture'
            startBtn.style.backgroundColor = wsCapturing ? '#f44336' : '#2196F3'
            statusLabel.textContent = 'Status: ' + (wsCapturing ? 'capturing...' : 'paused')
        })

        clearBtn.addEventListener('click', () => {
            wsFrames.length = 0
        })

        exportBtn.addEventListener('click', () => {
            const blob = new Blob([JSON.stringify(wsFrames, null, 2)], { type: 'application/json' })
            const url = URL.createObjectURL(blob)
            const a = document.createElement('a')
            a.href = url
            a.download = 'ws-frames-' + Date.now() + '.json'
            document.body.appendChild(a)
            a.click()
            document.body.removeChild(a)
            URL.revokeObjectURL(url)
        })

        setInterval(() => {
            frameCountLabel.textContent = 'Frames: ' + wsFrames.length
            const knownConns = new Set(wsFrames.map(f => f.connId))
            connLabel.textContent = 'Connections seen: ' + knownConns.size
        }, 500)

        parentNode.appendChild(parentDiv)
    }

    function createBreakParserTool(parentNode) {
        dbg('Break Parser mounted. Sources: widgets/breaks RSC payload (overview sync) + sale websocket frames (giveaway_won, payment_succeeded, randomizer_result_event)')
        const parentDiv = document.createElement('div')
        let updateCount = 0
        let lastUpdateTs = null
        let lastData = null

        const statusEl = document.createElement('div')
        statusEl.textContent = 'Listening for break data...'
        statusEl.style.cssText = 'color:#aaa;font-size:12px;margin-bottom:6px;'
        parentDiv.appendChild(statusEl)

        const counterEl = document.createElement('div')
        counterEl.textContent = 'Updates sent: 0'
        counterEl.style.cssText = 'color:#fff;font-size:12px;margin-bottom:6px;'
        parentDiv.appendChild(counterEl)

        const syncBtn = document.createElement('button')
        syncBtn.textContent = 'Sync'
        syncBtn.disabled = true
        syncBtn.style.cssText = 'font-size:24px;padding:2px 8px;cursor:pointer;background-color:#fff;color:#000;border:1px solid #ccc;border-radius:4px;'
        syncBtn.addEventListener('click', () => {
            if (!lastData) return
            GM_setValue('BreakOverview', lastData)
            dbg('Break Parser manual sync sent', JSON.stringify(lastData))
        })
        parentDiv.appendChild(syncBtn)

        // Giveaways and spot sales never appear in the break overview (it only
        // carries spot buyers, no prices), so consume the sale websocket frames
        // here directly. payment_succeeded carries buyer + price + isBreakSpot;
        // for break spots the team comes from the randomizer_result_event that
        // follows it. auction_ended is deliberately not required.
        const salesSuffix = SEND_SPOT_EVENTS ? '' : ' (spots disabled)'
        const salesCounterEl = document.createElement('div')
        salesCounterEl.textContent = 'Sales sent: 0' + salesSuffix
        salesCounterEl.style.cssText = 'color:#fff;font-size:12px;margin:6px 0;'
        parentDiv.appendChild(salesCounterEl)

        let salesCount = 0
        const sendQueue = []
        let sendQueueInterval = null

        function flushSendQueue() {
            if (sendQueue.length === 0) {
                clearInterval(sendQueueInterval)
                sendQueueInterval = null
                return
            }
            if (GM_getValue('newEvent', null) !== null) return
            const entity = sendQueue.shift()
            GM_setValue('newEvent', entity)
            salesCount++
            salesCounterEl.textContent = 'Sales sent: ' + salesCount + salesSuffix
            dbg('Break Parser sent:', JSON.stringify(entity))
        }

        function enqueueSale(entity) {
            dbg('Break Parser queueing:', JSON.stringify(entity))
            sendQueue.push(entity)
            if (!sendQueueInterval) sendQueueInterval = setInterval(flushSendQueue, 300)
            flushSendQueue()
        }

        const processedGiveaways = new Set()   // product.listingId
        const processedPayments = new Set()    // product.id
        const pendingRandomizers = new Map()   // listing_id -> price
        const earlyRandomizers = new Map()     // listing_id -> payload (randomizer seen before its payment)
        const processedRandomizers = new Set() // listing_id

        function finishBreakSpot(rrPayload, price) {
            processedRandomizers.add(String(rrPayload.listing_id))
            enqueueSale({ customer: rrPayload.buyer_username, price: price, name: rrPayload.result })
        }

        function onSaleFrame(e) {
            const raw = e.detail.data
            let d = raw
            if (typeof raw === 'string') {
                try { d = JSON.parse(raw) } catch(err) { return }
            }
            if (!Array.isArray(d) || d.length < 5) return
            const type = d[3], payload = d[4]

            if (type === 'giveaway_won') {
                const listingId = payload?.product?.listingId
                if (!listingId || processedGiveaways.has(listingId)) {
                    dbg('giveaway_won skipped:', !listingId ? 'no product.listingId' : 'duplicate listingId ' + listingId)
                    return
                }
                processedGiveaways.add(listingId)
                enqueueSale({ customer: payload.product.purchaserUser.username, price: 0, name: payload.product.name })
                return
            }

            if (type === 'payment_succeeded') {
                if (!SEND_SPOT_EVENTS) {
                    dbg('payment_succeeded ignored (SEND_SPOT_EVENTS=false)')
                    return
                }
                const prod = payload?.product
                const productId = prod?.id
                if (!productId) return
                if (prod.transactionType === 'GIVEAWAY') {
                    dbg('payment_succeeded for giveaway ignored (giveaway_won handles it)')
                    return
                }
                if (processedPayments.has(productId)) {
                    dbg('payment_succeeded duplicate product.id', productId)
                    return
                }
                processedPayments.add(productId)
                const price = (prod.soldPrice?.amount || 0) / 100
                if (!prod.isBreakSpot) {
                    enqueueSale({ customer: prod.purchaserUser?.username, price: price, name: prod.name })
                    return
                }
                const lid = String(prod.listingId)
                if (earlyRandomizers.has(lid)) {
                    dbg('payment for spot listing ' + lid + ' ($' + price + '), randomizer already arrived')
                    const rr = earlyRandomizers.get(lid)
                    earlyRandomizers.delete(lid)
                    finishBreakSpot(rr, price)
                } else {
                    pendingRandomizers.set(lid, price)
                    dbg('payment for spot listing ' + lid + ' ($' + price + '), waiting for randomizer result')
                }
                return
            }

            if (type === 'randomizer_result_event') {
                if (!SEND_SPOT_EVENTS) return
                const lid = String(payload.listing_id)
                if (processedRandomizers.has(lid)) return
                if (pendingRandomizers.has(lid)) {
                    const price = pendingRandomizers.get(lid)
                    pendingRandomizers.delete(lid)
                    finishBreakSpot(payload, price)
                } else {
                    earlyRandomizers.set(lid, payload)
                    dbg('randomizer result for listing ' + lid + ' arrived before its payment, holding')
                }
            }
        }
        window.addEventListener('__wnWsFrame', onSaleFrame)

        function formatAgo(ms) {
            const s = Math.floor(ms / 1000)
            if (s < 60) return s + 's ago'
            const m = Math.floor(s / 60)
            if (m < 60) return m + 'm ' + (s % 60) + 's ago'
            return Math.floor(m / 60) + 'h ' + (m % 60) + 'm ago'
        }

        setInterval(() => {
            if (lastUpdateTs !== null) {
                statusEl.textContent = 'Last update: ' + formatAgo(Date.now() - lastUpdateTs)
            }
        }, 1000)

        function onBreakDetails(e) {
            dbg('Break Parser overview received:', JSON.stringify(e.detail))
            lastData = e.detail
            syncBtn.disabled = false
            GM_setValue('BreakOverview', e.detail)
            updateCount++
            lastUpdateTs = Date.now()
            statusEl.textContent = 'Last update: 0s ago'
            counterEl.textContent = 'Updates sent: ' + updateCount
        }
        window.addEventListener('__wnBreakDetails', onBreakDetails)

        if (globalLastBreakData) {
            onBreakDetails({ detail: globalLastBreakData })
        }

        parentNode.appendChild(parentDiv)
    }

    function createChatOnlyTool(parentNode) {
        function removeNonRelatedNodes(rootElement, targetElements) {
            const queue = [rootElement]; // Queue to traverse the DOM tree
            let targetHit = false;

            while (queue.length > 0) {
                const currentElement = queue.shift(); // Dequeue current element
                if (!targetHit) {
                    currentElement.style.height = '100%';
                    currentElement.style.width = '100%';
                }
                if (targetElements.includes(currentElement)) {
                    targetHit = true;
                }
                // Check if the current element is not related to any target elements
                if (!targetElements.some(targetElement => currentElement && targetElement && (currentElement === targetElement || currentElement.contains(targetElement) || targetElement.contains(currentElement)))) {
                    // Remove the current element
                    currentElement.style.display = "none"
                    //currentElement.parentNode.removeChild(currentElement);
                } else {
                    // Add the children of the current element to the queue for further traversal
                    Array.from(currentElement.children).forEach(child => queue.push(child));
                }
            }
        }

        // Create a new div for the quantity tool
        var parentDiv = document.createElement('div');
        parentDiv.style.border = '1px solid black'; // Add border
        parentDiv.style.padding = '10px'; // Add padding for spacing

        // Create a button
        const dButton = document.createElement('button');
        dButton.textContent = 'Clean page';
        parentDiv.appendChild(dButton);

        dButton.addEventListener('click', async () => {
            const rootElement = document.body;
            const chatContainer = document.querySelector('#bottom-section-stream-container > div > div > div:nth-child(1)')
            chatContainer.classList.add('mob-chat')
            let chatContainerParent = chatContainer.parentNode
            chatContainerParent.classList.add('mob-chat-parent')

            const chatWindow = document.querySelector('#bottom-section-stream-container > div > div > div.mob-chat > div:nth-child(1) > div:nth-child(3)')

            function autoScrollToBottom(element) {
                // Create a mutation observer to watch for changes
                const observer = new MutationObserver(() => {
                    // Scroll to the bottom
                    element.scrollTop = element.scrollHeight;
                });

                // Start observing the element for DOM changes
                observer.observe(element, {
                    childList: true,  // Watch for child additions/removals
                    subtree: true,    // Watch all descendants, not just direct children
                    characterData: true // Watch for text content changes
                });

                // Initial scroll to bottom
                element.scrollTop = element.scrollHeight;

                return observer; // Return observer so it can be disconnected later if needed
            }

            const scrollViewport = chatWindow.querySelector('[data-overlayscrollbars-viewport]')
            if (scrollViewport) {
                const currentValue = scrollViewport.getAttribute('data-overlayscrollbars-viewport');
                if (currentValue) {
                    const updatedValue = currentValue.replace('scrollbarHidden', '').trim();
                    scrollViewport.setAttribute('data-overlayscrollbars-viewport', updatedValue);
                }
                autoScrollToBottom(scrollViewport);
            }

            const videoElement = document.querySelector('video')

            const targetElements = [
                chatWindow,
                videoElement,
            ];
            console.log(targetElements);

            const priceDiv = document.querySelector('#bottom-section-stream-container > div > div.mob-chat-parent > div:nth-child(2) > div:nth-child(2)')
            if (priceDiv) {
                targetElements.push(priceDiv)
                Array.from(priceDiv.parentNode.children).forEach(sibling => {
                    if (sibling !== priceDiv) sibling.style.display = 'none'
                })
            }

            const online = document.querySelector('#top-section-stream-container > div:nth-child(1) > div:nth-child(2) > div > div > div:nth-child(1)')
            if (online) targetElements.push(online)

            removeNonRelatedNodes(rootElement, targetElements); // Call the function to remove non-related nodes

            document.querySelector('#bottom-section-stream-container').style.height = '100%';

            var styleElement = document.createElement('style');
            styleElement.type = 'text/css';
            styleElement.innerHTML = '#bottom-section-stream-container > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) * { background-color: rgba(0, 0, 0, 0.1) !important; }';
            document.head.appendChild(styleElement);

            function updateNestedStyles(element, property, value) {
                // Apply the style to the current element
                element.style[property] = value

                // Recursively apply the style to all child elements
                Array.from(element.children).forEach(child => {
                    updateNestedStyles(child, property, value);
                });
            }

            if (online) {
                online.classList.add('mob-online')
                const onlineNumber = online.querySelector('div > div:nth-child(4)')
                if (onlineNumber) onlineNumber.style.fontSize = '45px'
                const onlineIcon = online.querySelector('div > div:nth-child(1)')
                if (onlineIcon) {
                    const iconFirstDiv = onlineIcon.querySelector('div')
                    if (iconFirstDiv) iconFirstDiv.classList.add('mob-online-icon')
                }
            }
            if (priceDiv) {
                priceDiv.classList.add('mob-price')
                priceDiv.style.width = ''
                priceDiv.style.position = 'static'
                priceDiv.style.right = ''
                priceDiv.style.left = ''
                priceDiv.style.top = ''
                priceDiv.childNodes.forEach(i => {
                    i.classList.add('mob-price-child')
                })
                if (online) online.appendChild(priceDiv)
            }

            const lastBuyerDiv = document.querySelector('#bottom-section-stream-container > div > div:nth-child(1) > div:nth-child(1) > div:nth-child(3) > div:nth-child(1)')
            if (lastBuyerDiv) {
                lastBuyerDiv.style.position = 'fixed'
                lastBuyerDiv.style.right = '2%'
                lastBuyerDiv.style.zIndex = '999'
                let firstDiv = lastBuyerDiv.childNodes[0]
                firstDiv.style.flexDirection = 'row-reverse'
                let firstDivChild = firstDiv.childNodes[0]
                let logo = firstDivChild.childNodes[0]
                logo.style.display = 'none'
                let buyerName = firstDivChild.childNodes[1]
                buyerName.style.color = 'white'
                let lastBuyerStatus = firstDivChild.childNodes[2]
                lastBuyerStatus.style.display = 'none'
                updateNestedStyles(lastBuyerDiv, 'font-size', '45px')
                document.body.appendChild(lastBuyerDiv)

                function updateLastBuyerPosition() {
                    const onlineRect = online ? online.getBoundingClientRect() : null
                    if (onlineRect) {
                        lastBuyerDiv.style.top = (onlineRect.bottom + 5) + 'px'
                    }
                }
                updateLastBuyerPosition()
                const lastBuyerPositionObserver = new MutationObserver(updateLastBuyerPosition)
                if (online) {
                    lastBuyerPositionObserver.observe(online, { childList: true, subtree: true, characterData: true })
                }

                const lastBuyerStyleObserver = new MutationObserver((mutationsList) => {
                    let firstDiv = mutationsList[0].addedNodes[0]
                    firstDiv.style.flexDirection = 'row-reverse'
                    let firstDivChild = firstDiv.childNodes[0]
                    let logo = firstDivChild.childNodes[0]
                    logo.style.display = 'none'
                    let buyerName = firstDivChild.childNodes[1]
                    buyerName.style.color = 'white'
                    let lastBuyerStatus = firstDivChild.childNodes[2]
                    lastBuyerStatus.style.display = 'none'
                    updateNestedStyles(lastBuyerDiv, 'font-size', '45px')
                })
                lastBuyerStyleObserver.observe(lastBuyerDiv, { childList: true, subtree: true })
            }

            parentNode.removeChild(parentDiv);
        });


        parentNode.appendChild(parentDiv);
    }

    // Whatnot's notes editor only exists while its "Show Notes" dialog is open,
    // and the dialog element sticks around (closed) after it has been used once.
    function findNotesTextarea() {
        const textAreaToUpdate = document.querySelector('textarea[placeholder*="notes"]');
        if (!textAreaToUpdate) return null;
        const dialog = textAreaToUpdate.closest('dialog');
        if (dialog && !dialog.open) return null;
        return textAreaToUpdate;
    }

    function openShowNotes() {
        if (findNotesTextarea()) return;
        const notesButton = getElementByXpath("//button[normalize-space(.)='Notes']");
        if (notesButton) notesButton.click();
    }

    function waitForNotesTextarea(timeoutMs, callback) {
        const deadline = Date.now() + timeoutMs;
        (function poll() {
            const textAreaToUpdate = findNotesTextarea();
            if (textAreaToUpdate) return callback(textAreaToUpdate);
            if (Date.now() > deadline) return callback(null);
            setTimeout(poll, 50);
        })();
    }

    function createNotesTool(parentNode) {
        // Create a new div for the quantity tool
        const parentDiv = document.createElement('div');
        parentDiv.style.border = '1px solid black'; // Add border
        parentDiv.style.padding = '10px'; // Add padding for spacing

        // Create a textarea
        const textarea = document.createElement('textarea');
        textarea.placeholder = 'Notes';
        textarea.style.width = '150px'; // Adjust width as necessary
        textarea.rows = 4; // Set number of visible rows
        // Whatnot's reset strips the native input chrome, so draw it ourselves
        textarea.style.border = '2px solid #555';
        textarea.style.borderRadius = '4px';
        textarea.style.padding = '2px 6px';
        textarea.style.backgroundColor = 'white';
        parentDiv.appendChild(textarea);

        // Create a button
        const setNotesButton = document.createElement('button');
        setNotesButton.textContent = 'Set notes';
        parentDiv.appendChild(setNotesButton);

        setNotesButton.addEventListener('click', () => {
            const text = textarea.value

            // Open Whatnot's "Show Notes" panel ourselves, then fill it in, so
            // the only step left for the user is pressing Post.
            openShowNotes();
            waitForNotesTextarea(3000, (textAreaToUpdate) => {
                if (!textAreaToUpdate) {
                    console.log('Could not open the Show Notes panel.');
                    setNotesButton.textContent = 'Notes panel not found';
                    setTimeout(() => { setNotesButton.textContent = 'Set notes'; }, 3000);
                    return;
                }

                // Set the value of the specific textarea
                const nativeTextAreaValueSetter = Object.getOwnPropertyDescriptor(
                    window.HTMLTextAreaElement.prototype,
                    'value'
                ).set;
                nativeTextAreaValueSetter.call(textAreaToUpdate, text);

                // Dispatch input event to notify any listeners
                const event = new Event('input', { bubbles: true });
                textAreaToUpdate.dispatchEvent(event);
            });
        });

        parentNode.appendChild(parentDiv);
    }

    let isGiveawayAlarmToolRunning = false;

    function createGiveawayAlarmTool(parentNode) {
        if (isGiveawayAlarmToolRunning) {
            console.log('Giveaway alarm tool is already running.');
            return;
        }

        isGiveawayAlarmToolRunning = true;
        let foundEntries = false;
        let isCheckingEntries = false;
        let rememberedEntriesDiv = null;

        function checkEntries() {
            if (isCheckingEntries) {
                console.log('checkEntries is already running.');
                return;
            }

            isCheckingEntries = true;

            // Check if rememberedEntriesDiv is still in the document
            if (rememberedEntriesDiv && !document.body.contains(rememberedEntriesDiv)) {
                rememberedEntriesDiv = null;
            }

            const entriesDiv = rememberedEntriesDiv || Array.from(document.querySelectorAll('div')).find(div => /^\d+Entries$/.test(div.textContent));
            // console.log('Checking for entries div:', entriesDiv);

            if (entriesDiv) {
                if (!foundEntries) {
                    console.log('Entries div found for the first time.', entriesDiv);
                    rememberedEntriesDiv = entriesDiv;
                }
                foundEntries = true;
            } else if (foundEntries) {
                console.log('Entries div was found before but is now missing.');
                showAlarm();
                foundEntries = false;
                rememberedEntriesDiv = null;
            } else {
                // console.log('Entries div not found.');
            }

            isCheckingEntries = false;
        }

        function showAlarm() {
            console.log('Showing alarm.');
            const alarmDiv = document.createElement('div');
            alarmDiv.textContent = 'Start the giveaway';
            alarmDiv.style.position = 'fixed';
            alarmDiv.style.top = '10%';
            alarmDiv.style.left = '25%'; // Center the div horizontally
            alarmDiv.style.width = '50%';
            alarmDiv.style.backgroundColor = 'red';
            alarmDiv.style.color = 'white';
            alarmDiv.style.textAlign = 'center';
            alarmDiv.style.padding = '10px';
            alarmDiv.style.zIndex = '10000';
            alarmDiv.style.fontSize = '2em'; // Increase text size
            alarmDiv.style.border = '5px solid white'; // Restore white border

            // Create a close button
            const closeButton = document.createElement('button');
            closeButton.textContent = 'Close';
            closeButton.style.position = 'absolute';
            closeButton.style.top = '50%';
            closeButton.style.right = '10px'; // Slight distance from the right border
            closeButton.style.transform = 'translateY(-50%)'; // Center vertically
            closeButton.style.fontSize = '1em'; // Set text size to 1em
            closeButton.style.padding = '5px'; // Add padding for spacing around text
            closeButton.style.backgroundColor = 'grey'; // Add grey background
            closeButton.style.border = '2px solid darkgrey'; // Add darkgrey border
            closeButton.addEventListener('click', () => {
                document.body.removeChild(alarmDiv);
            });

            alarmDiv.appendChild(closeButton);
            document.body.appendChild(alarmDiv);

            // Auto-close the alarm after 30 seconds
            setTimeout(() => {
                if (document.body.contains(alarmDiv)) {
                    document.body.removeChild(alarmDiv);
                }
            }, 30000);
        }

        console.log('Starting giveaway alarm tool.');
        setInterval(checkEntries, 1000);
    }

})();