DataAnnotation Power-Up v1

Includes multi-currency support, togglable filters, a project grabber, and a horrific notification system.

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

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

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

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

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

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

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

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         DataAnnotation Power-Up v1
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Includes multi-currency support, togglable filters, a project grabber, and a horrific notification system.
// @author       adonno55
// @match        https://app.dataannotation.tech/workers/payments*
// @match        https://app.dataannotation.tech/workers/projects*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_notification
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_openInTab
// @connect      api.frankfurter.app
// @connect      self
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const SCRIPT_PREFIX = "[DataAnnotation Power-Up v1]";
    const ENABLE_DEBUG_LOGGING = true;
    const CURRENCY_SYMBOLS = { USD: '$', EUR: '€', GBP: '£', JPY: '¥', CAD: 'C$', AUD: 'A$', CHF: 'CHF', CNY: '¥', INR: '₹' };

    const Config = {
        defaults: {
            payThreshold: 25,
            nameFilters: ['chatbot', 'ai', 'model', 'llm'],
            checkInterval: 1,
            enableSound: true,
            enableCurrencyConversion: true,
            targetCurrency: 'GBP',
            includePayPalFee: true,
            paypalFeePercentage: 3.0,
            enablePayFilter: true,
            enableNameFilter: true,
            notifyForHighPay: true,
            notifyForNameMatch: true,
            notifyForOtherNew: true,
            // Project Grabber Settings
            enableProjectGrabber: false, // Defaulted to off for user safety
            grabHighPay: true,
            grabNameMatch: true,
            grabberDelay: 2, // Delay in seconds between opening tabs
            grabberMaxTabs: 1 // Safety limit: max tabs to open in one check cycle
        },
        settings: {},
        async load() {
            const loadedSettings = await GM_getValue('powerUpConfig_v10', {});
            if (typeof loadedSettings.enableUnseenAlerts !== 'undefined') {
                loadedSettings.notifyForOtherNew = loadedSettings.enableUnseenAlerts;
                delete loadedSettings.enableUnseenAlerts;
            }
            this.settings = { ...this.defaults, ...loadedSettings };
            if (ENABLE_DEBUG_LOGGING) console.info(`${SCRIPT_PREFIX} [Config] Settings loaded:`, this.settings);
        },
        async save() {
            await GM_setValue('powerUpConfig_v10', this.settings);
            if (ENABLE_DEBUG_LOGGING) console.info(`${SCRIPT_PREFIX} [Config] Settings saved:`, this.settings);
        }
    };

    const SettingsGUI = {
        init() { this._addStyles(); this._createSettingsButton(); },
        _createSettingsButton() { const btn = document.createElement('button'); btn.id = 'powerup-settings-btn'; btn.textContent = '⚙️'; btn.title = 'Open Power-Up Settings'; btn.onclick = () => this._togglePanel(); document.body.appendChild(btn); },
        _togglePanel() { let panel = document.getElementById('powerup-settings-panel'); if (panel) { panel.remove(); } else { this._createPanel(); } },
        _createPanel() {
            const panel = document.createElement('div'); panel.id = 'powerup-settings-panel';
            panel.innerHTML = `
                <div class="powerup-header"><h3>DA Power-Up Settings</h3><button class="powerup-close-btn">&times;</button></div>
                <div class="powerup-content">
                    <h4>🔔 Notification Filters</h4>
                    <div class="powerup-setting-checkbox"><input type="checkbox" id="enablePayFilter" ${Config.settings.enablePayFilter ? 'checked' : ''}><label for="enablePayFilter">Enable High-Pay Filter</label></div>
                    <div class="powerup-setting ${!Config.settings.enablePayFilter ? 'disabled' : ''}"><label for="payThreshold">High-Pay Threshold ($/hr)</label><input type="number" id="payThreshold" min="0" value="${Config.settings.payThreshold}" ${!Config.settings.enablePayFilter ? 'disabled' : ''}></div>
                    <div class="powerup-setting-checkbox"><input type="checkbox" id="enableNameFilter" ${Config.settings.enableNameFilter ? 'checked' : ''}><label for="enableNameFilter">Enable Name Filter</label></div>
                    <div class="powerup-setting ${!Config.settings.enableNameFilter ? 'disabled' : ''}"><label for="nameFilters">Name Filters (one per line)</label><textarea id="nameFilters" rows="4" ${!Config.settings.enableNameFilter ? 'disabled' : ''}>${Config.settings.nameFilters.join('\n')}</textarea></div>
                    <h4>🎯 Notification Triggers</h4>
                    <p style="margin-top: -10px; font-size: 0.9em; color: #ccc;">Choose which types of new projects should trigger a notification.</p>
                    <div class="powerup-setting-checkbox"><input type="checkbox" id="notifyForHighPay" ${Config.settings.notifyForHighPay ? 'checked' : ''}><label for="notifyForHighPay">Notify for High-Pay projects</label></div>
                    <div class="powerup-setting-checkbox"><input type="checkbox" id="notifyForNameMatch" ${Config.settings.notifyForNameMatch ? 'checked' : ''}><label for="notifyForNameMatch">Notify for Name-Filter matches</label></div>
                    <div class="powerup-setting-checkbox"><input type="checkbox" id="notifyForOtherNew" ${Config.settings.notifyForOtherNew ? 'checked' : ''}><label for="notifyForOtherNew">Notify for other new projects (general)</label></div>

                    <h4>🤖 Project Grabber</h4>
                    <p style="margin-top: -10px; font-size: 0.9em; color: #ccc;">Automatically open matching new projects in a new tab. Use with caution.</p>
                    <div class="powerup-setting-checkbox"><input type="checkbox" id="enableProjectGrabber" ${Config.settings.enableProjectGrabber ? 'checked' : ''}><label for="enableProjectGrabber">Enable Project Grabber</label></div>
                    <div id="grabber-options" class="${!Config.settings.enableProjectGrabber ? 'disabled' : ''}">
                        <p style="margin-bottom: 5px; margin-top: 10px; font-size: 0.9em; color: #ccc;">Grab projects that match:</p>
                        <div class="powerup-setting-checkbox"><input type="checkbox" id="grabHighPay" ${Config.settings.grabHighPay ? 'checked' : ''} ${!Config.settings.enableProjectGrabber ? 'disabled' : ''}><label for="grabHighPay">High-Pay Filter</label></div>
                        <div class="powerup-setting-checkbox"><input type="checkbox" id="grabNameMatch" ${Config.settings.grabNameMatch ? 'checked' : ''} ${!Config.settings.enableProjectGrabber ? 'disabled' : ''}><label for="grabNameMatch">Name Filter</label></div>
                        <div class="powerup-setting"><label for="grabberMaxTabs">Max Tabs per Check</label><input type="number" id="grabberMaxTabs" min="1" max="5" value="${Config.settings.grabberMaxTabs}" ${!Config.settings.enableProjectGrabber ? 'disabled' : ''}></div>
                        <div class="powerup-setting"><label for="grabberDelay">Delay Between Grabs (seconds)</label><input type="number" id="grabberDelay" min="0" value="${Config.settings.grabberDelay}" ${!Config.settings.enableProjectGrabber ? 'disabled' : ''}></div>
                    </div>

                    <h4>⚙️ General Settings</h4>
                    <div class="powerup-setting"><label for="checkInterval">Check Interval (minutes)</label><input type="number" id="checkInterval" min="1" value="${Config.settings.checkInterval}"></div>
                    <div class="powerup-setting-checkbox"><input type="checkbox" id="enableSound" ${Config.settings.enableSound ? 'checked' : ''}><label for="enableSound">Enable Sound Notifications</label></div>
                    <h4>💱 Currency Converter</h4>
                    <div class="powerup-setting-checkbox"><input type="checkbox" id="enableCurrencyConversion" ${Config.settings.enableCurrencyConversion ? 'checked' : ''}><label for="enableCurrencyConversion">Enable Currency Conversion</label></div>
                    <div class="powerup-setting ${!Config.settings.enableCurrencyConversion ? 'disabled' : ''}"><label for="targetCurrency">Target Currency</label><select id="targetCurrency" ${!Config.settings.enableCurrencyConversion ? 'disabled' : ''}>
                        <option value="GBP" ${Config.settings.targetCurrency === 'GBP' ? 'selected' : ''}>🇬🇧 British Pound (GBP)</option>
                        <option value="EUR" ${Config.settings.targetCurrency === 'EUR' ? 'selected' : ''}>🇪🇺 Euro (EUR)</option>
                        <option value="JPY" ${Config.settings.targetCurrency === 'JPY' ? 'selected' : ''}>🇯🇵 Japanese Yen (JPY)</option>
                        <option value="CAD" ${Config.settings.targetCurrency === 'CAD' ? 'selected' : ''}>🇨🇦 Canadian Dollar (CAD)</option>
                        <option value="AUD" ${Config.settings.targetCurrency === 'AUD' ? 'selected' : ''}>🇦🇺 Australian Dollar (AUD)</option>
                        <option value="INR" ${Config.settings.targetCurrency === 'INR' ? 'selected' : ''}>🇮🇳 Indian Rupee (INR)</option>
                    </select></div>
                    <div class="powerup-setting-checkbox ${!Config.settings.enableCurrencyConversion ? 'disabled' : ''}"><input type="checkbox" id="includePayPalFee" ${Config.settings.includePayPalFee ? 'checked' : ''} ${!Config.settings.enableCurrencyConversion ? 'disabled' : ''}><label for="includePayPalFee">Deduct estimated PayPal Fee (${Config.settings.paypalFeePercentage}%)</label></div>
                </div>
                <div class="powerup-footer"><button id="powerup-save-btn">Save & Apply</button><span id="powerup-save-confirm" style="display:none;">✅ Saved!</span></div>`;
            document.body.appendChild(panel);
            panel.querySelector('.powerup-close-btn').onclick = () => this._togglePanel();
            document.getElementById('powerup-save-btn').onclick = () => this._saveSettings();
            const setupToggle = (checkId, fieldIds, containerSelector) => {
                const checkbox = document.getElementById(checkId);
                const updateState = () => {
                    const isDisabled = !checkbox.checked;
                    fieldIds.forEach(id => document.getElementById(id).disabled = isDisabled);
                    document.getElementById(fieldIds[0]).closest(containerSelector).classList.toggle('disabled', isDisabled);
                };
                checkbox.onchange = updateState;
            };
            setupToggle('enablePayFilter', ['payThreshold'], '.powerup-setting');
            setupToggle('enableNameFilter', ['nameFilters'], '.powerup-setting');
            setupToggle('enableCurrencyConversion', ['targetCurrency', 'includePayPalFee'], '.powerup-setting');

            const grabberCheckbox = document.getElementById('enableProjectGrabber');
            grabberCheckbox.onchange = () => {
                const isDisabled = !grabberCheckbox.checked;
                document.getElementById('grabber-options').classList.toggle('disabled', isDisabled);
                document.querySelectorAll('#grabber-options input, #grabber-options select').forEach(input => input.disabled = isDisabled);
            };
        },
        async _saveSettings() {
            Config.settings.payThreshold = parseFloat(document.getElementById('payThreshold').value);
            Config.settings.nameFilters = document.getElementById('nameFilters').value.split('\n').map(f => f.trim()).filter(Boolean);
            Config.settings.enablePayFilter = document.getElementById('enablePayFilter').checked;
            Config.settings.enableNameFilter = document.getElementById('enableNameFilter').checked;
            Config.settings.notifyForHighPay = document.getElementById('notifyForHighPay').checked;
            Config.settings.notifyForNameMatch = document.getElementById('notifyForNameMatch').checked;
            Config.settings.notifyForOtherNew = document.getElementById('notifyForOtherNew').checked;

            Config.settings.enableProjectGrabber = document.getElementById('enableProjectGrabber').checked;
            Config.settings.grabHighPay = document.getElementById('grabHighPay').checked;
            Config.settings.grabNameMatch = document.getElementById('grabNameMatch').checked;
            Config.settings.grabberMaxTabs = parseInt(document.getElementById('grabberMaxTabs').value, 10);
            Config.settings.grabberDelay = parseFloat(document.getElementById('grabberDelay').value);

            Config.settings.checkInterval = parseInt(document.getElementById('checkInterval').value, 10);
            Config.settings.enableSound = document.getElementById('enableSound').checked;
            Config.settings.enableCurrencyConversion = document.getElementById('enableCurrencyConversion').checked;
            Config.settings.targetCurrency = document.getElementById('targetCurrency').value;
            Config.settings.includePayPalFee = document.getElementById('includePayPalFee').checked;
            await Config.save();
            const confirmMsg = document.getElementById('powerup-save-confirm'); confirmMsg.style.display = 'inline';
            setTimeout(() => { this._togglePanel(); alert('Settings saved. Page will reload to apply changes.'); window.location.reload(); }, 1000);
        },
        _addStyles() { GM_addStyle(`#powerup-settings-btn { position: fixed; bottom: 20px; left: 20px; width: 50px; height: 50px; background: linear-gradient(135deg, #6a0dad, #a020f0); color: white; border: none; border-radius: 50%; font-size: 24px; cursor: pointer; z-index: 10000; box-shadow: 0 4px 12px rgba(0,0,0,0.4); } #powerup-settings-panel { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 500px; max-height: 80vh; overflow-y: auto; background: #2d2d2d; color: #f0f0f0; border: 1px solid #6a0dad; border-radius: 12px; z-index: 10001; box-shadow: 0 10px 40px rgba(0,0,0,0.8); font-family: -apple-system, BlinkMacSystemFont, sans-serif; } .powerup-header { padding: 15px 20px; background: #444; border-bottom: 1px solid #555; display: flex; justify-content: space-between; align-items: center; border-radius: 10px 10px 0 0; position: sticky; top: 0; z-index: 1; } .powerup-header h3 { margin: 0; } .powerup-close-btn { background: none; border: none; color: white; font-size: 24px; cursor: pointer; } .powerup-content { padding: 20px; display: flex; flex-direction: column; gap: 15px; } .powerup-content h4 { margin: 10px 0 5px 0; color: #a020f0; border-bottom: 1px solid #555; padding-bottom: 5px; } .powerup-setting, .powerup-setting-checkbox, #grabber-options { transition: opacity 0.3s; } .powerup-setting.disabled, .powerup-setting-checkbox.disabled, #grabber-options.disabled { opacity: 0.5; } .powerup-setting label, .powerup-setting-checkbox label { margin-bottom: 5px; font-weight: bold; } .powerup-setting input, .powerup-setting textarea, .powerup-setting select { background: #222; border: 1px solid #666; color: white; padding: 10px; border-radius: 4px; width: 100%; box-sizing: border-box; } .powerup-setting-checkbox { display: flex; align-items: center; gap: 10px; } .powerup-footer { padding: 15px 20px; background: #444; border-top: 1px solid #555; text-align: right; border-radius: 0 0 10px 10px; position: sticky; bottom: 0; z-index: 1;} #powerup-save-btn { background: #6a0dad; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; } #powerup-save-confirm { margin-left: 10px; color: lightgreen; }`); }
    };

    const Notifier = {
        NOTIFICATION_SOUND_URL: 'https://www.myinstants.com/media/sounds/hell_AJWSn3e.mp3', _originalTitle: document.title,
        send(project, reasons) {
            if (ENABLE_DEBUG_LOGGING) console.info(`${SCRIPT_PREFIX} [Notifier] Sending notification for '${project.name}' for reasons:`, reasons);
            const { title, text, highlight } = this._formatMessage(project, reasons);
            GM_notification({ title, text, highlight, silent: false, timeout: 30000, onclick: () => window.focus() });
            this._showBanner(title);
            if (Config.settings.enableSound) { const audio = new Audio(this.NOTIFICATION_SOUND_URL); audio.play().catch(e => console.warn(`${SCRIPT_PREFIX} Audio notification was blocked.`)); }
        },
        _formatMessage(project, reasons) {
            let title = "✨ New Project Available!"; let text = `${project.name} | $${project.pay.toFixed(2)}/hr | ${project.tasks} tasks`; let highlight = true;
            const isHighPay = reasons.includes('highPay'); const isNameMatch = reasons.includes('nameMatch');
            if (isHighPay && isNameMatch) { title = `💸🎯 High-Pay & Filter Match!`; }
            else if (isHighPay) { title = `💸 High-Pay Project Found!`; }
            else if (isNameMatch) { title = `🎯 Filter Match Project Found!`; }
            return { title, text, highlight };
        },
        _showBanner(title) {
            document.title = `(${title.slice(0, 1)}) New Project! | ${this._originalTitle}`;
            let banner = document.getElementById('new-project-notifier-banner');
            if (!banner) { banner = document.createElement('div'); banner.id = 'new-project-notifier-banner'; document.documentElement.appendChild(banner); }
            banner.innerHTML = `<span>${title} Refresh the page to see the new project.</span><button title="Dismiss">&times;</button>`;
            banner.querySelector('button').onclick = () => { banner.remove(); document.title = this._originalTitle; };
        }
    };

    const ProjectTracker = {
        _tabId: Date.now() + Math.random(), _isLeader: false, _isChecking: false, _checkTimeoutId: null,
        async _updateLeaderTimestamp() {
            if (this._isLeader) { const now = Date.now(); await GM_setValue('leaderInfo', { id: this._tabId, timestamp: now }); if (ENABLE_DEBUG_LOGGING) console.log(`%c[LEADERSHIP] Heartbeat updated at ${new Date(now).toLocaleTimeString()}`, 'color: cyan;'); }
        },
        _getProjectsFromDoc(doc) {
            const projectsMap = new Map(); doc.querySelectorAll('tbody tr').forEach(row => { try { const cells = row.querySelectorAll('td'); if (cells.length < 4) return; const linkElement = cells[0].querySelector('a'); if (!linkElement) return; const projectId = new URL(linkElement.href, doc.baseURI).searchParams.get('project_id'); const name = linkElement.textContent.trim(); const pay = parseFloat(cells[1].textContent.trim().replace(/[^0-9.-]+/g, "")); const tasks = parseInt(cells[2].textContent.trim().replace(/,/g, ''), 10); const url = linkElement.href; if (projectId && name) { projectsMap.set(projectId, { id: projectId, name, pay: isNaN(pay) ? 0 : pay, tasks: isNaN(tasks) ? 0 : tasks, url }); } } catch (e) { console.error(`${SCRIPT_PREFIX} Error parsing a project row:`, e, row); } }); return projectsMap;
        },
        _fetchProjectsWithIframe() {
            return new Promise((resolve, reject) => { const iframe = document.createElement('iframe'); iframe.id = 'powerup-fetch-iframe'; iframe.style.display = 'none'; const timeout = setTimeout(() => { cleanup(); reject(new Error('Iframe load timed out.')); }, 30000); const cleanup = () => { clearTimeout(timeout); const el = document.getElementById('powerup-fetch-iframe'); if (el) el.remove(); }; iframe.onload = () => { try { const projects = this._getProjectsFromDoc(iframe.contentDocument); resolve(projects); } catch (e) { reject(e); } finally { cleanup(); } }; iframe.onerror = () => { cleanup(); reject(new Error('Iframe failed to load.')); }; iframe.src = window.location.href; document.body.appendChild(iframe); });
        },
        async _checkForNewProjects() {
            if (this._isChecking) return;
            this._isChecking = true;
            if (ENABLE_DEBUG_LOGGING) console.groupCollapsed(`${SCRIPT_PREFIX} [Tracker] Starting Project Check @ ${new Date().toLocaleTimeString()}`);
            try {
                await this._updateLeaderTimestamp();
                const currentProjectsMap = await this._fetchProjectsWithIframe();
                const currentProjectIds = Array.from(currentProjectsMap.keys());
                const previousProjectIds = await GM_getValue('powerUpLastSeenProjects_v10', []);
                const historicalDB = await GM_getValue('slimProjectDatabase_v10', {});
                const previousProjectIdsSet = new Set(previousProjectIds);
                let historicalDbWasModified = false;
                const projectsToGrab = [];

                if (ENABLE_DEBUG_LOGGING) {
                    console.log("Projects now:", currentProjectIds);
                    console.log("Projects last check:", previousProjectIds);
                }
                for (const [projectId, projectData] of currentProjectsMap.entries()) {
                    if (!previousProjectIdsSet.has(projectId)) {
                        if (ENABLE_DEBUG_LOGGING) console.log(`%c[NEW THIS CYCLE]`, 'color: lightgreen; font-weight: bold;', projectData);
                        const triggeringReasons = [];
                        const lowerCaseName = projectData.name.toLowerCase();
                        const isHighPay = Config.settings.enablePayFilter && projectData.pay >= Config.settings.payThreshold;
                        const isNameMatch = Config.settings.enableNameFilter && Config.settings.nameFilters.some(filter => lowerCaseName.includes(filter.toLowerCase()));

                        if (isHighPay && Config.settings.notifyForHighPay) { triggeringReasons.push('highPay'); }
                        if (isNameMatch && Config.settings.notifyForNameMatch) { triggeringReasons.push('nameMatch'); }
                        if (triggeringReasons.length > 0) { Notifier.send(projectData, triggeringReasons); }
                        else if (Config.settings.notifyForOtherNew) { Notifier.send(projectData, ['unseen']); }

                        if (Config.settings.enableProjectGrabber) {
                            const shouldGrabHighPay = isHighPay && Config.settings.grabHighPay;
                            const shouldGrabNameMatch = isNameMatch && Config.settings.grabNameMatch;
                            if (shouldGrabHighPay || shouldGrabNameMatch) {
                                projectsToGrab.push(projectData);
                            }
                        }
                    }
                    if (!historicalDB[projectId]) {
                        historicalDB[projectId] = { id: projectData.id, name: projectData.name, pay: projectData.pay };
                        historicalDbWasModified = true;
                        if (ENABLE_DEBUG_LOGGING) console.log(`%c[NEW TO HISTORY] Added '${projectData.name}' to the historical database.`, 'color: cyan;');
                    }
                }

                if (projectsToGrab.length > 0) {
                    if (ENABLE_DEBUG_LOGGING) console.info(`${SCRIPT_PREFIX} [Grabber] Found ${projectsToGrab.length} project(s) matching grab criteria.`);
                    const projectsToActuallyOpen = projectsToGrab.slice(0, Config.settings.grabberMaxTabs);
                    if (projectsToActuallyOpen.length < projectsToGrab.length) {
                        console.warn(`${SCRIPT_PREFIX} [Grabber] Max tabs limit (${Config.settings.grabberMaxTabs}) reached. Not opening all ${projectsToGrab.length} matched projects.`);
                    }
                    projectsToActuallyOpen.forEach((project, index) => {
                        const delay = index * Config.settings.grabberDelay * 1000;
                        setTimeout(() => {
                            console.log(`%c[GRABBING] Opening '${project.name}' in new tab...`, 'color: magenta; font-weight: bold;');
                            GM_openInTab(project.url, { active: true, setParent: true });
                        }, delay);
                    });
                }

                if (historicalDbWasModified) {
                    await GM_setValue('slimProjectDatabase_v10', historicalDB);
                }
                await GM_setValue('powerUpLastSeenProjects_v10', currentProjectIds);
            } catch (error) {
                console.error(`${SCRIPT_PREFIX} [Tracker] A critical error occurred during the project check:`, error);
            } finally {
                this._isChecking = false;
                if (ENABLE_DEBUG_LOGGING) console.groupEnd();
            }
        },
        async _electLeader() {
            const leaderInfo = await GM_getValue('leaderInfo', {}); const now = Date.now(); let becameLeader = false; if (!leaderInfo.id || (now - leaderInfo.timestamp) > Config.settings.checkInterval * 60 * 1000 * 1.5) { this._isLeader = true; becameLeader = true; await this._updateLeaderTimestamp(); } else { this._isLeader = (leaderInfo.id === this._tabId); } if (ENABLE_DEBUG_LOGGING) { console.log(`%c[LEADERSHIP CHECK] This tab is ${this._isLeader ? 'the leader' : 'a follower'}. ${becameLeader ? '(Newly Elected)' : ''}`, 'color: orange;'); }
        },
        async init() {
            if (ENABLE_DEBUG_LOGGING) console.info(`${SCRIPT_PREFIX} [Tracker] Initializing...`);
            GM_addStyle(`#new-project-notifier-banner { position: fixed; top: 0; left: 0; width: 100%; background: linear-gradient(45deg, #6a0dad, #a020f0); color: white; text-align: center; padding: 15px; font-size: 1.2em; z-index: 9999; display: flex; justify-content: center; align-items: center; box-shadow: 0 4px 8px rgba(0,0,0,0.3); } #new-project-notifier-banner button { background:none; border:none; color:white; font-size:2em; cursor:pointer; margin-left:20px; } }`);
            window.addEventListener('unload', async () => { if (this._isLeader) { if (ENABLE_DEBUG_LOGGING) console.log(`%c[LEADERSHIP] Leader tab closing. Abdicating leadership.`, 'color: red;'); await GM_setValue('leaderInfo', {}); } });
            await this._electLeader();
            if (this._isLeader) { if (ENABLE_DEBUG_LOGGING) console.info(`${SCRIPT_PREFIX} [Tracker] This is the leader tab. Performing an immediate initial check.`); this._checkForNewProjects(); }
            const scheduleNextCheck = () => { clearTimeout(this._checkTimeoutId); this._checkTimeoutId = setTimeout(() => { if (this._isLeader) { this._checkForNewProjects(); } scheduleNextCheck(); }, Config.settings.checkInterval * 60 * 1000); };
            setInterval(() => this._electLeader(), 5000);
            scheduleNextCheck();
            if (ENABLE_DEBUG_LOGGING) console.info(`${SCRIPT_PREFIX} [Tracker] Project tracker active. Checks every ${Config.settings.checkInterval} minutes.`);
        }
    };

    const CurrencyConverter = {
        FALLBACK_RATES: { GBP: 0.79, EUR: 0.92, JPY: 150.0, CAD: 1.35, AUD: 1.50, INR: 83.0 },
        _fetchRate(callback) {
            const target = Config.settings.targetCurrency;
            const fallback = this.FALLBACK_RATES[target] || 1.0;
            GM_xmlhttpRequest({
                method: "GET", url: `https://api.frankfurter.app/latest?from=USD&to=${target}`,
                onload: (res) => { try { callback(JSON.parse(res.responseText)?.rates?.[target] || fallback); } catch (e) { callback(fallback); } },
                onerror: () => callback(fallback)
            });
        },
        _convertPrices(rootNode, effectiveRate) {
            const usdRegex = /\$(-?[\d,]+(?:\.\d{2})?)/g;
            const walker = document.createTreeWalker(rootNode, NodeFilter.SHOW_TEXT);
            let node;
            while ((node = walker.nextNode())) {
                const p = node.parentElement;
                if (!p || p.closest('[data-usd-converted]') || ['SCRIPT', 'STYLE'].includes(p.tagName)) continue;
                if (node.nodeValue.includes('$')) {
                    const newText = node.nodeValue.replace(usdRegex, (_, numStr) => {
                        const symbol = CURRENCY_SYMBOLS[Config.settings.targetCurrency] || Config.settings.targetCurrency;
                        const value = parseFloat(numStr.replace(/,/g, '')) * effectiveRate;
                        const decimalPlaces = ['JPY'].includes(Config.settings.targetCurrency) ? 0 : 2;
                        return `${symbol}${value.toFixed(decimalPlaces)}`;
                    });
                    if (newText !== node.nodeValue) {
                        node.nodeValue = newText;
                        p.dataset.usdConverted = 'true';
                    }
                }
            }
        },
        init() {
            if (!Config.settings.enableCurrencyConversion) return;
            this._fetchRate(marketRate => {
                const feeMultiplier = Config.settings.includePayPalFee ? (1 - (Config.settings.paypalFeePercentage / 100)) : 1;
                const effectiveRate = marketRate * feeMultiplier;
                this._convertPrices(document.body, effectiveRate);
                new MutationObserver(mutations => mutations.forEach(m => m.addedNodes.forEach(n => {
                    if (n.nodeType === Node.ELEMENT_NODE && !n.closest('#powerup-fetch-iframe')) {
                        this._convertPrices(n, effectiveRate);
                    }
                }))).observe(document.body, { childList: true, subtree: true });
            });
        }
    };

    const DebugConsole = {
        _createButton() { GM_addStyle(`#debug-dump-btn { position: fixed; bottom: 20px; right: 20px; background-color: #c0392b; color: white; border: none; border-radius: 5px; padding: 10px 15px; font-size: 14px; z-index: 10000; box-shadow: 0 4px 8px rgba(0,0,0,0.4); }`); const btn = document.createElement('button'); btn.id = 'debug-dump-btn'; btn.textContent = 'Dump Project DB'; btn.addEventListener('click', this._dumpDatabaseToConsole); document.body.appendChild(btn); },
        async _dumpDatabaseToConsole() {
            console.groupCollapsed(`%c[DebugConsole] Project Database Dump`, 'color: #c0392b; font-weight: bold;');
            const projectDB = await GM_getValue('slimProjectDatabase_v10', {});
            const projectArray = Object.values(projectDB);
            if (projectArray.length === 0) {
                console.warn("The slim project database is currently empty.");
            } else {
                console.log(`Found ${projectArray.length} total projects in historical database.`);
                console.table(projectArray);
            }
            console.groupEnd();
        },
        init() { if (ENABLE_DEBUG_LOGGING) { this._createButton(); } }
    };

    async function main() {
        if (ENABLE_DEBUG_LOGGING) console.log(`${SCRIPT_PREFIX} Script loaded on: ${window.location.href}`);
        await Config.load();
        const url = window.location.href;
        if (url.includes('/payments') || url.includes('/projects')) {
            CurrencyConverter.init();
            SettingsGUI.init();
            DebugConsole.init();
        }
        if (url.includes('/projects')) {
            setTimeout(() => ProjectTracker.init(), 2000);
        }
    }
    main();

})();