Greasy Fork is available in English.

Click buttons across tabs

Clicks specified buttons across tabs using the Broadcast Channel API and closes tabs after successful submission.

As of 14. 07. 2025. See the latest version.

// ==UserScript==
// @name        Click buttons across tabs
// @namespace   https://musicbrainz.org/user/chaban
// @version     2.6
// @tag         ai-created
// @description Clicks specified buttons across tabs using the Broadcast Channel API and closes tabs after successful submission.
// @author      chaban
// @license     MIT
// @match       *://*.musicbrainz.org/*
// @match       *://magicisrc.kepstin.ca/*
// @match       *://magicisrc-beta.kepstin.ca/*
// @run-at      document-start
// @grant       GM.info
// @grant       GM_registerMenuCommand
// @grant       GM_unregisterMenuCommand
// @grant       GM.getValue
// @grant       GM.setValue
// @grant       window.close
// ==/UserScript==

(async function () {
    'use strict';

    const scriptName = GM.info.script.name;
    console.log(`%c[${scriptName}] Script initialization started on ${location.href}`, 'font-weight: bold;');

    /**
     * @typedef {Object} SiteConfig
     * @property {string|string[]} hostnames - A single hostname string or an array of hostname strings.
     * @property {string|string[]} paths - A single path string or an array of path strings (must match ending of pathname).
     * @property {string} buttonSelector - The CSS selector string for the button to be clicked.
     * @property {string} [channelName] - The name of the BroadcastChannel to use. Omit if autoClick is true.
     * @property {string} [messageTrigger] - The message data that triggers the button click. Omit if autoClick is true.
     * @property {string} [menuCommandName] - The name to display in the Tampermonkey/Greasemonkey menu. Omit if autoClick is true.
     * @property {(RegExp|string)[]} [successUrlPatterns] - An array of RegExp or string patterns to match against the URL to indicate a successful submission.
     * @property {boolean} [shouldCloseAfterSuccess=false] - Whether to attempt closing the tab after a successful submission. Note: Browser security heavily restricts `window.close()`.
     * @property {boolean} [autoClick=false] - If true, the button will be clicked automatically when found, without needing a broadcast message.
     * @property {boolean} [disableDelay=false] - If true, this button click will happen immediately (0 delay).
     */

    /**
     * Configuration for different websites and their button click settings.
     * Order matters for `autoClick` rules and how `configForSuccessCheck` is found.
     * @type {SiteConfig[]}
     */
    const siteConfigurations = [
        {
            hostnames: ['musicbrainz.org'],
            paths: ['/edit-relationships'],
            buttonSelector: '.rel-editor > button',
            autoClick: true,
            disableDelay: true,
            successUrlPatterns: [],
            shouldCloseAfterSuccess: false
        },
        {
            hostnames: ['musicbrainz.org'],
            paths: [
                '/edit',
                '/edit-relationships',
                '/add-cover-art'
            ],
            channelName: 'mb_edit_channel',
            messageTrigger: 'submit-edit',
            buttonSelector: 'button.submit.positive[type="submit"]',
            menuCommandName: 'MusicBrainz: Submit Edit (All Tabs)',
            successUrlPatterns: [
                /^https?:\/\/(?:beta\.)?musicbrainz\.org\/(?!collection\/)[^/]+\/[a-f0-9\-]{36}(?:\/cover-art)?\/?$/,
            ],
            shouldCloseAfterSuccess: true
        },
        {
            hostnames: ['magicisrc.kepstin.ca', 'magicisrc-beta.kepstin.ca'],
            paths: ['/'],
            channelName: 'magicisrc_submit_channel',
            messageTrigger: 'submit-isrcs',
            buttonSelector: '[onclick^="doSubmitISRCs"]',
            menuCommandName: 'MagicISRC: Submit ISRCs (All Tabs)',
            successUrlPatterns: [
                /\?.*submit=1/
            ],
            shouldCloseAfterSuccess: true
        }
    ];

    const SUBMISSION_TRIGGERED_FLAG = 'broadcastChannelSubmissionState';

    const GLOBAL_CLOSE_TAB_CHANNEL_NAME = 'global_close_tab_channel';
    const GLOBAL_CLOSE_TAB_MESSAGE_TRIGGER = 'close-this-tab';
    const GLOBAL_CLOSE_TAB_MENU_COMMAND_NAME = 'Global: Close This Tab (All Tabs)';

    const MIN_CLICK_DELAY_SECONDS = 0;
    const DELAY_SCALING_FACTOR = 120;
    const DELAY_MODE_SETTING = 'mb_button_clicker_delayMode';
    const STATIC_MAX_DELAY_SETTING = 'mb_button_clicker_staticMaxDelay';
    const DISABLE_AUTO_CLOSE_SETTING = 'mb_button_clicker_disableAutoClose';

    const MAGICISRC_ENABLE_AUTO_RELOAD = 'magicisrc_enableAutoReload';

    let registeredMenuCommandIDs = [];

    /**
     * Sends a message to the specified BroadcastChannel.
     * @param {string} channelName
     * @param {string} message
     */
    function sendMessageToChannel(channelName, message) {
        try {
            new BroadcastChannel(channelName).postMessage(message);
            console.log(`[${scriptName}] Sent message "${message}" to channel "${channelName}".`);
        } catch (error) {
            console.error(`[${scriptName}] Error sending message to channel "${channelName}":`, error);
        }
    }

    /**
     * Checks if the current page indicates a successful submission based on the given config's URL patterns.
     * @param {SiteConfig} config - The site configuration.
     * @param {boolean} [quiet=false] - If true, suppresses console logs for matches/no matches.
     * @returns {boolean}
     */
    function isSubmissionSuccessful(config, quiet = false) {
        if (!config.successUrlPatterns || config.successUrlPatterns.length === 0) {
            return false;
        }

        for (const pattern of config.successUrlPatterns) {
            const matchResult = (typeof pattern === 'string') ? location.href.includes(pattern) : pattern.test(location.href);
            if (matchResult) {
                if (!quiet) {
                    console.log(`[${scriptName}] URL "${location.href}" matches success pattern "${pattern}".`);
                }
                return true;
            }
        }
        if (!quiet) {
            console.log(`[${scriptName}] URL "${location.href}" does not match any success pattern.`);
        }
        return false;
    }

    /**
     * Attempts to close the current tab.
     * @param {number} delayMs - Optional delay in milliseconds before attempting to close.
     * @returns {void}
     */
    function attemptCloseTab(delayMs = 200) {
        console.log(`[${scriptName}] Attempting to close tab in ${delayMs}ms.`);
        setTimeout(() => {
            try {
                window.close();
                console.log(`[${scriptName}] Successfully called window.close() after delay.`);
            } catch (e) {
                console.warn(`[${scriptName}] Failed to close tab automatically via window.close(). This is expected due to browser security restrictions.`, e);
            }
        }, delayMs);
    }

    /**
     * Checks the current page against the success criteria for the relevant site,
     * and closes the tab if successful, unless auto-close is disabled.
     * @param {SiteConfig} currentConfigForSuccessCheck - The site config relevant to the current hostname.
     * @returns {Promise<void>}
     */
    async function checkAndCloseIfSuccessful(currentConfigForSuccessCheck) {
        const storedSubmissionState = sessionStorage.getItem(SUBMISSION_TRIGGERED_FLAG);

        if (!storedSubmissionState) {
            console.log(`[${scriptName}] No stored submission state. Skipping close check.`);
            return;
        }

        const triggered = storedSubmissionState === 'true';

        const disableAutoClose = await GM.getValue(DISABLE_AUTO_CLOSE_SETTING, false);
        const isCurrentlySuccessful = isSubmissionSuccessful(currentConfigForSuccessCheck);


        if (triggered && currentConfigForSuccessCheck.shouldCloseAfterSuccess) {
            console.log(`[${scriptName}] Checking for submission success on "${location.href}".`);
            if (isCurrentlySuccessful) {
                console.log(`[${scriptName}] Submission successful. Clearing SUBMISSION_TRIGGERED_FLAG.`);
                sessionStorage.removeItem(SUBMISSION_TRIGGERED_FLAG);

                if (disableAutoClose) {
                    console.info(`%c[${scriptName}] Submission successful, but automatic tab closing is DISABLED by user setting.`, 'color: orange; font-weight: bold;');
                } else {
                    console.log(`%c[${scriptName}] Submission successful. Closing tab.`, 'color: green; font-weight: bold;');
                    attemptCloseTab();
                }
            } else {
                console.info(`%c[${scriptName}] Submission was triggered, but URL does not match success criteria.`, 'color: orange;');
            }
        } else {
            console.log(`[${scriptName}] Not checking for success: triggered is false, or closing is not configured for this site.`);
        }
    }

    /**
     * Retrieves the current delay mode from storage.
     * @returns {Promise<string>} 'dynamic' or 'static'. Defaults to 'dynamic'.
     */
    async function getDelayMode() {
        return await GM.getValue(DELAY_MODE_SETTING, 'dynamic');
    }

    /**
     * Determines and returns the current minimum and maximum click delays based on settings.
     * @returns {Promise<{min: number, max: number}>}
     */
    async function getCurrentClickDelayRange() {
        const delayMode = await getDelayMode();

        let currentMinDelay;
        let currentMaxDelay;
        let delayModeLog;

        if (delayMode === 'static') {
            currentMinDelay = 0;
            currentMaxDelay = await GM.getValue(STATIC_MAX_DELAY_SETTING, 6);
            delayModeLog = `Delays set to STATIC (random 0 to ${currentMaxDelay}s).`;
        } else {
            const logicalCores = Math.max(1, navigator.hardwareConcurrency || 1);
            currentMinDelay = MIN_CLICK_DELAY_SECONDS;
            currentMaxDelay = Math.max(currentMinDelay, Math.round(DELAY_SCALING_FACTOR / logicalCores));
            delayModeLog = `Delays set to DYNAMIC (based on ${logicalCores} cores): ${currentMinDelay} to ${currentMaxDelay} seconds.`;
        }

        console.log(`[${scriptName}] ${delayModeLog}`);
        return { min: currentMinDelay, max: currentMaxDelay };
    }

    /**
     * Calculates a random delay based on the current delay mode settings.
     * @param {boolean} [disableDelayOverride=false] - If true, forces a 0ms delay.
     * @returns {Promise<number>} Delay in milliseconds.
     */
    async function getCalculatedDelay(disableDelayOverride = false) {
        if (disableDelayOverride) {
            console.log(`[${scriptName}] Delay disabled for this action as per config.`);
            return 0;
        }
        const { min: currentMinDelay, max: currentMaxDelay } = await getCurrentClickDelayRange();
        const delaySeconds = Math.floor(Math.random() * (currentMaxDelay - currentMinDelay + 1)) + currentMinDelay;
        return delaySeconds * 1000;
    }


    /**
     * Sets up menu commands for the userscript.
     * This function is responsible for clearing and re-registering ALL commands
     * to ensure dynamic display names and conditional commands work correctly.
     */
    async function setupMenuCommands() {
        registeredMenuCommandIDs.forEach(id => {
            try {
                GM_unregisterMenuCommand(id);
            } catch (e) {
            }
        });
        registeredMenuCommandIDs = [];

        const registerCommand = (name, func) => {
            const id = GM_registerMenuCommand(name, func);
            registeredMenuCommandIDs.push(id);
        };

        const delayMode = await getDelayMode();
        const disableAutoClose = await GM.getValue(DISABLE_AUTO_CLOSE_SETTING, false);
        const enableMagicISRCReload = await GM.getValue(MAGICISRC_ENABLE_AUTO_RELOAD, true);

        if (delayMode === 'dynamic') {
            registerCommand('Delay Mode: DYNAMIC', async () => {
                await GM.setValue(DELAY_MODE_SETTING, 'static');
                console.log(`[${scriptName}] Delay mode switched to: STATIC.`);
                await getCurrentClickDelayRange();
                await setupMenuCommands();
            });
        } else {
            registerCommand('Delay Mode: STATIC', async () => {
                await GM.setValue(DELAY_MODE_SETTING, 'dynamic');
                console.log(`[${scriptName}] Delay mode switched to: DYNAMIC.`);
                await getCurrentClickDelayRange();
                await setupMenuCommands();
            });

            const currentStaticMaxValue = await GM.getValue(STATIC_MAX_DELAY_SETTING, 6);

            registerCommand(`Set Static Max Delay (Current: ${currentStaticMaxValue}s)`, async () => {
                let newValue = prompt(`Enter maximum STATIC delay in seconds (Current: ${currentStaticMaxValue}). A random delay between 0 and this value (inclusive) will be used.`);
                if (newValue === null) return;

                newValue = parseFloat(newValue);
                if (isNaN(newValue) || newValue < 0) {
                    console.warn(`[${scriptName}] Invalid input for static max delay: "${newValue}". Please enter a non-negative number.`);
                    alert("Invalid input. Please enter a non-negative number.");
                    return;
                }
                const actualNewValue = Math.floor(newValue);
                await GM.setValue(STATIC_MAX_DELAY_SETTING, actualNewValue);
                console.log(`[${scriptName}] Static maximum delay set to ${actualNewValue} seconds.`);
                await getCurrentClickDelayRange();
                await setupMenuCommands();
            });
        }

        registerCommand(`Auto Close Tabs: ${disableAutoClose ? 'DISABLED' : 'ENABLED'}`, async () => {
            const currentState = await GM.getValue(DISABLE_AUTO_CLOSE_SETTING, false);
            const newState = !currentState;
            await GM.setValue(DISABLE_AUTO_CLOSE_SETTING, newState);
            console.log(`[${scriptName}] Automatic tab closing is now ${newState ? 'DISABLED' : 'ENABLED'}.`);
            await setupMenuCommands();
        });

        registerCommand(`MagicISRC Auto Reload: ${enableMagicISRCReload ? 'ENABLED' : 'DISABLED'}`, async () => {
            const currentState = await GM.getValue(MAGICISRC_ENABLE_AUTO_RELOAD, true);
            const newState = !currentState;
            await GM.setValue(MAGICISRC_ENABLE_AUTO_RELOAD, newState);
            console.log(`[${scriptName}] MagicISRC automatic reload is now ${newState ? 'DISABLED' : 'ENABLED'}.`);
            await setupMenuCommands();
        });


        const currentHostname = location.hostname;
        const currentPathname = location.pathname;

        const configsForMenuCommands = siteConfigurations.filter(config => {
            if (config.autoClick) return false;
            const hostnames = Array.isArray(config.hostnames) ? config.hostnames : [config.hostnames];
            const hostnameMatches = hostnames.some(hostname => currentHostname.endsWith(hostname));
            if (hostnameMatches) {
                const paths = Array.isArray(config.paths) ? config.paths : [config.paths];
                return paths.some(pathPattern => currentPathname.endsWith(pathPattern));
            }
            return false;
        });

        for (const config of configsForMenuCommands) {
            if (config.menuCommandName) {
                registerCommand(config.menuCommandName, () => {
                    sessionStorage.setItem(SUBMISSION_TRIGGERED_FLAG, 'true');
                    console.log(`[${scriptName}] Menu command triggered. Stored submission state: true.`);
                    sendMessageToChannel(config.channelName, config.messageTrigger);
                });
                console.log(`[${scriptName}] Registered menu command "${config.menuCommandName}".`);
            }
        }

        const configForSuccessCheck = siteConfigurations.find(config => {
            const hostnames = Array.isArray(config.hostnames) ? config.hostnames : [config.hostnames];
            const hostnameMatches = hostnames.some(hostname => currentHostname.endsWith(hostname));
            return hostnameMatches && config.shouldCloseAfterSuccess;
        });

        if (configForSuccessCheck && isSubmissionSuccessful(configForSuccessCheck, true)) {
            registerCommand(GLOBAL_CLOSE_TAB_MENU_COMMAND_NAME, () => {
                console.log(`[${scriptName}] Global close menu command triggered.`);
                sendMessageToChannel(GLOBAL_CLOSE_TAB_CHANNEL_NAME, GLOBAL_CLOSE_TAB_MESSAGE_TRIGGER);
            });
            console.log(`[${scriptName}] Registered global close menu command.`);
        }

        console.log(`[${scriptName}] Menu commands setup finished. Total commands: ${registeredMenuCommandIDs.length}`);
    }

    /**
     * Attempts to perform a MagicISRC button click if a submission is pending.
     * This uses a MutationObserver to wait for the button to appear.
     * @returns {Promise<void>}
     */
    async function handleMagicISRCButtonInteraction() {
        const currentHostname = location.hostname;
        const currentPathname = location.pathname;

        const submissionFlagValue = sessionStorage.getItem(SUBMISSION_TRIGGERED_FLAG);
        const submissionTriggered = submissionFlagValue === 'true';

        console.log(`%c[${scriptName}] handleMagicISRCButtonInteraction called. SUBMISSION_TRIGGERED_FLAG: ${submissionTriggered}`, 'color: blue; font-weight: bold;');

        if (submissionTriggered) {
            console.log(`%c[${scriptName}] SUBMISSION_TRIGGERED_FLAG is true. Proceeding with button interaction logic.`, 'color: blue; font-weight: bold;');

            const magicisrcConfig = siteConfigurations.find(config =>
                Array.isArray(config.hostnames) ? config.hostnames.some(h => currentHostname.includes(h)) : currentHostname.includes(h)
                && (Array.isArray(config.paths) ? config.paths.some(p => currentPathname.endsWith(p)) : currentPathname.endsWith(p))
                && config.channelName === 'magicisrc_submit_channel'
            );

            if (magicisrcConfig) {
                console.log(`[${scriptName}] MagicISRC config found for button interaction. Button selector: "${magicisrcConfig.buttonSelector}"`);
                let buttonFoundAndClicked = false;

                const checkAndClickButton = async () => {
                    const btn = document.querySelector(magicisrcConfig.buttonSelector);
                    if (btn) {
                        console.log(`[${scriptName}] Button found:`, btn, `Disabled state: ${btn.disabled}`);
                        if (!btn.disabled) {
                            console.log(`%c[${scriptName}] Button found and ENABLED. Clicking now.`, 'color: green; font-weight: bold;');
                            const delayMs = await getCalculatedDelay(magicisrcConfig.disableDelay);
                            console.log(`[${scriptName}] Attempting to click MagicISRC button "${btn.textContent.trim()}" in ${delayMs / 1000} seconds.`);
                            setTimeout(() => {
                                btn.click();
                            }, delayMs);
                            return true;
                        } else {
                            console.log(`[${scriptName}] Button found but is DISABLED.`);
                        }
                    } else {
                        console.log(`[${scriptName}] Button not yet found.`);
                    }
                    return false;
                };

                if (await checkAndClickButton()) {
                    buttonFoundAndClicked = true;
                    return;
                }

                console.log(`[${scriptName}] Button not immediately available/enabled. Setting up MutationObserver for button interaction.`);
                const observer = new MutationObserver(async (mutations, obs) => {
                    console.log(`[${scriptName}] MutationObserver (button interaction) triggered.`);
                    if (await checkAndClickButton()) {
                        obs.disconnect();
                        buttonFoundAndClicked = true;
                    }
                });

                if (document.body) {
                    observer.observe(document.body, { childList: true, subtree: true, attributes: true });
                    console.log(`[${scriptName}] MutationObserver active for MagicISRC button interaction.`);
                } else {
                    console.warn(`[${scriptName}] document.body not available for MagicISRC button interaction observer.`);
                    sessionStorage.removeItem(SUBMISSION_TRIGGERED_FLAG);
                    console.log(`[${scriptName}] Cleared SUBMISSION_TRIGGERED_FLAG: body not available for observer.`);
                    return;
                }

            } else {
                console.warn(`[${scriptName}] MagicISRC config not found for button interaction.`);
                sessionStorage.removeItem(SUBMISSION_TRIGGERED_FLAG);
                console.log(`[${scriptName}] Cleared SUBMISSION_TRIGGERED_FLAG: MagicISRC config not found.`);
            }
        } else {
            console.log(`[${scriptName}] Skipping button interaction logic: SUBMISSION_TRIGGERED_FLAG is ${submissionTriggered}.`);
        }
    }

    /**
     * Handles the reload logic when a MagicISRC error is detected.
     * This function is now primarily called by the intercepted `renderError` or `fetch`.
     * @returns {Promise<void>}
     */
    async function handleMagicISRCErrorReload() {
        console.log(`%c[${scriptName}] handleMagicISRCErrorReload called.`, 'color: red; font-weight: bold;');

        const enableReload = await GM.getValue(MAGICISRC_ENABLE_AUTO_RELOAD, true);
        const reloadDelayMs = await getCalculatedDelay();

        if (!enableReload) {
            console.warn(`%c[${scriptName}] MagicISRC automatic reload is DISABLED by user setting. Skipping reload.`, 'color: orange; font-weight: bold;');
            return;
        }

        console.warn(`%c[${scriptName}] Detected MagicISRC error. Reloading page in ${reloadDelayMs / 1000} seconds.`, 'color: red; font-weight: bold;');


        setTimeout(() => {
            console.log(`[${scriptName}] Attempting location.reload() now.`);
            location.reload();
        }, reloadDelayMs);
    }

    /**
     * Injects a script into the page's context to intercept window.renderError and window.fetch.
     */
    function injectMagicISRCErrorInterceptor() {
        const script = document.createElement('script');
        script.textContent = `
            (function() {
                const originalRenderError = window.renderError;
                window.renderError = function(message, error) {
                    if (originalRenderError) {
                        originalRenderError.apply(this, arguments);
                    } else {
                        console.error('MagicISRC original renderError was not available. Message:', message, 'Error:', error);
                        const container = document.getElementById("container");
                        if (container) {
                            container.innerHTML = '<h1 class="h3">An error occurred</h1><p>' + message + '</p>';
                        }
                    }

                    console.log('Injected script: Posting MAGICISRC_ERROR_DETECTED message.');
                    window.postMessage({
                        type: 'MAGICISRC_ERROR_DETECTED',
                        message: message,
                        error: error ? error.toString() : 'N/A'
                    }, window.location.origin);
                };
                console.log('MagicISRC renderError function successfully intercepted by injected script.');

                const originalFetch = window.fetch;
                window.fetch = function(...args) {
                    return originalFetch.apply(this, args)
                        .then(response => {
                            if (!response.ok && response.status >= 500 && response.status < 600) {
                                console.error('MagicISRC fetch intercepted: Server error detected!', response.status, response.url);
                                console.log('Injected script: Posting MAGICISRC_NETWORK_ERROR message (server error).');
                                window.postMessage({
                                    type: 'MAGICISRC_NETWORK_ERROR',
                                    url: response.url,
                                    status: response.status,
                                    statusText: response.statusText
                                }, window.location.origin);
                            }
                            return response;
                        })
                        .catch(error => {
                            console.error('MagicISRC fetch intercepted: Network error detected!', error);
                            console.log('Injected script: Posting MAGICISRC_NETWORK_ERROR message (network error).');
                            window.postMessage({
                                type: 'MAGICISRC_NETWORK_ERROR',
                                message: error.message,
                                error: error.toString()
                            }, window.location.origin);
                            throw error;
                        });
                };
                console.log('MagicISRC fetch function successfully intercepted by injected script.');
            })();
        `;
        document.documentElement.appendChild(script);
        script.remove();
    }

    /**
     * Main initialization function for the userscript.
     * This function focuses on setting up event listeners and core logic.
     */
    async function initializeScript() {
        const currentHostname = location.hostname;
        const currentPathname = location.pathname;

        let isButtonClickInProgress = false;

        const isMagicISRC = currentHostname.includes('magicisrc');
        if (isMagicISRC) {
            console.log(`[${scriptName}] MagicISRC page detected. Setting up injected script for error interception.`);

            injectMagicISRCErrorInterceptor();

            window.addEventListener('message', async (event) => {
                console.log(`[${scriptName}] Received message in userscript sandbox:`, event);

                if (event.data && event.origin === window.location.origin) {
                    if (event.data.type === 'MAGICISRC_ERROR_DETECTED' || event.data.type === 'MAGICISRC_NETWORK_ERROR') {
                        console.log(`[${scriptName}] Received ${event.data.type} message from page context. Calling handleMagicISRCErrorReload.`);
                        handleMagicISRCErrorReload();
                    }
                }
            });

            await handleMagicISRCButtonInteraction();

        }


        let configForSuccessCheck = null;

        const applicableConfigs = siteConfigurations.filter(config => {
            const hostnames = Array.isArray(config.hostnames) ? config.hostnames : [config.hostnames];
            const hostnameMatches = hostnames.some(hostname => currentHostname.endsWith(hostname));
            if (hostnameMatches) {
                const paths = Array.isArray(config.paths) ? config.paths : [config.paths];
                return paths.some(pathPattern => currentPathname.endsWith(pathPattern));
            }
            return false;
        });

        configForSuccessCheck = siteConfigurations.find(config => {
            const hostnames = Array.isArray(config.hostnames) ? config.hostnames : [config.hostnames];
            const hostnameMatches = hostnames.some(hostname => currentHostname.endsWith(hostname));
            return hostnameMatches && config.shouldCloseAfterSuccess;
        });


        await getCurrentClickDelayRange();

        const disableAutoClose = await GM.getValue(DISABLE_AUTO_CLOSE_SETTING, false);
        console.log(`[${scriptName}] Automatic tab closing is currently ${disableAutoClose ? 'DISABLED' : 'ENABLED'}.`);


        for (const config of applicableConfigs) {
            if (config.autoClick) {
                console.log(`[${scriptName}] Setting up AUTOMATIC button-clicking logic for a rule.`);
                const findAndClickButton = async () => {
                    const btn = document.querySelector(config.buttonSelector);

                    if (btn && !btn.disabled) {
                        const delayMs = await getCalculatedDelay(config.disableDelay);

                        console.log(`[${scriptName}] Automatically clicking button "${btn.textContent.trim()}" in ${delayMs / 1000} seconds.`);
                        setTimeout(() => {
                            btn.click();
                        }, delayMs);
                        return true;
                    }
                    return false;
                };

                document.addEventListener('DOMContentLoaded', () => {
                    console.log(`[${scriptName}] DOMContentLoaded listener for autoClick button setup.`);
                    const observer = new MutationObserver(async (mutations, obs) => {
                        console.log(`[${scriptName}] MutationObserver (autoClick button) triggered.`);
                        if (await findAndClickButton()) {
                            obs.disconnect();
                            console.log(`[${scriptName}] Automatic button found and clicked via observer.`);
                        }
                    });
                    if (document.body) {
                        observer.observe(document.body, { childList: true, subtree: true, attributes: true });
                        console.log(`[${scriptName}] MutationObserver active for automatic button click on DOMContentLoaded.`);
                    }
                });

            } else {
                if (!config.channelName || !config.messageTrigger) {
                    console.warn(`[${scriptName}] Skipping manual button-clicking setup for a config without channelName or messageTrigger.`, config);
                    continue;
                }
                console.log(`[${scriptName}] Setting up BROADCAST CHANNEL button-clicking logic for "${config.channelName}".`);
                try {
                    const channel = new BroadcastChannel(config.channelName);
                    channel.addEventListener('message', async (event) => {
                        if (event.data === config.messageTrigger) {
                            if (isSubmissionSuccessful(config)) {
                                console.log(`[${scriptName}] Received trigger message "${event.data}" but already on a success page. Skipping button click.`);
                                return;                            }

                            sessionStorage.setItem(SUBMISSION_TRIGGERED_FLAG, 'true');
                            console.log(`[${scriptName}] Stored submission state: true (due to received command).`);

                            console.log(`[${scriptName}] Received trigger message "${event.data}".`);

                            if (isButtonClickInProgress) {
                                console.log(`[${scriptName}] Button click already in progress. Ignoring duplicate message.`);
                                return;
                            }
                            isButtonClickInProgress = true;

                            const btn = document.querySelector(config.buttonSelector);

                            if (btn) {
                                const delayMs = await getCalculatedDelay(config.disableDelay);

                                console.log(`[${scriptName}] Attempting to click button "${btn.textContent.trim()}" in ${delayMs / 1000} seconds.`);
                                setTimeout(() => {
                                    btn.click();
                                    isButtonClickInProgress = false;
                                }, delayMs);
                            } else {
                                console.warn(`[${scriptName}] Button for selector "${JSON.stringify(config.buttonSelector)}" not found.`);
                                isButtonClickInProgress = false;
                            }
                        }
                    });
                    console.log(`[${scriptName}] Listener active for button clicks on channel "${config.channelName}".`);
                } catch (error) {
                    console.error(`[${scriptName}] Error initializing BroadcastChannel:`, error);
                }
            }
        }

        if (configForSuccessCheck) {
            await checkAndCloseIfSuccessful(configForSuccessCheck);

            let lastUrl = location.href;
            const urlChangedHandler = async () => {
                if (location.href !== lastUrl) {
                    console.log(`%c[${scriptName}] URL changed from "${lastUrl}" to "${location.href}". Re-checking for success.`, 'color: purple; font-weight: bold;');
                    lastUrl = location.href;
                    await checkAndCloseIfSuccessful(configForSuccessCheck);

                    if (isMagicISRC) {
                        console.log(`[${scriptName}] Calling handleMagicISRCButtonInteraction from urlChangedHandler.`);
                        await handleMagicISRCButtonInteraction();
                    }
                }
            };

            window.addEventListener('popstate', urlChangedHandler);
            const originalPushState = history.pushState;
            history.pushState = function() {
                originalPushState.apply(this, arguments);
                urlChangedHandler();
            };
            const originalReplaceState = history.replaceState;
            history.replaceState = function() {
                originalReplaceState.apply(this, arguments);
                urlChangedHandler();
            };
            console.log(`[${scriptName}] URL change listeners activated.`);

            if (isSubmissionSuccessful(configForSuccessCheck)) {
                console.log(`[${scriptName}] Current URL matches a success pattern. Setting up global close listener.`);
                try {
                    const globalCloseChannel = new BroadcastChannel(GLOBAL_CLOSE_TAB_CHANNEL_NAME);
                    globalCloseChannel.addEventListener('message', (event) => {
                        if (event.data === GLOBAL_CLOSE_TAB_MESSAGE_TRIGGER) {
                            console.log(`[${scriptName}] Received global close request.`);
                            attemptCloseTab(50);
                        }
                    });
                    console.log(`[${scriptName}] Global close channel listener active.`);
                } catch (error) {
                    console.error(`[${scriptName}] Error initializing global BroadcastChannel:`, error);
                }
            } else {
                console.log(`[${scriptName}] Current URL does NOT match a success pattern. Global close listener skipped.`);
            }

        } else {
            console.log(`[${scriptName}] No success-check config found for this hostname. Not monitoring for closure.`);
            sessionStorage.removeItem(SUBMISSION_TRIGGERED_FLAG);
        }

        console.log(`%c[${scriptName}] Script initialization finished.`, 'font-weight: bold;');
    }

    await setupMenuCommands();
    await initializeScript();

})();