Wrote an upgrade progress plugin to nudge myself to upgrade

Diamonds are too hard; use AI to write a Tampermonkey script that nudges me to slack off and level up, showing the upgrade progress after each opening (auto‑refresh every 5 minutes or manual refresh):

Currently there is a bug where using the default avatar fails to retrieve data, looking into how to fix it The loading bug has been attempted to fix.

Code reply visible:

// ==UserScript==
// @name         NodeLoc User Info Display
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Displays NodeLoc user information and refreshes periodically
// @match        https://www.nodeloc.com/*
// @grant        none
// ==/UserScript==

// NodeLoc User Info Display Script
(async function() {
    'use strict';

    // Configuration parameters
    const config = {
        progressUpdateInterval: 5 * 60 * 1000  // Progress update interval (ms), default 5 minutes
    };

    // Control variables
    let userID = null;
    let progressUpdateTimer = null;

    // Progress tracking object
    let progressTracker = {
        initial: {},      // Initial values at startup
        current: {},      // Current values
        hasStarted: false // Whether tracking has started
    };

    // Logger
    const Logger = {
        maxLogs: 100, // Keep up to 100 logs
        logCount: 0,

        // Add log
        log(message, type = 'info') {
            // Also output to console
            const consoleMsg = message;
            if (type === 'error') {
                console.error(consoleMsg);
            } else if (type === 'warning') {
                console.warn(consoleMsg);
            } else {
                console.log(consoleMsg);
            }

           
            const container = document.getElementById('log-container');
            if (!container) return;

            const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
            const entry = document.createElement('div');
            entry.className = `log-entry ${type}`;
            entry.innerHTML = `<span class="log-time">${time}</span>${this.escapeHtml(message)}`;

            container.appendChild(entry);
            this.logCount++;

            // Limit log count
            while (this.logCount > this.maxLogs) {
                const firstLog = container.firstElementChild;
                if (firstLog) {
                    container.removeChild(firstLog);
                    this.logCount--;
                }
            }

            // Auto‑scroll to bottom
            container.scrollTop = container.scrollHeight;
        },

        // HTML escape to prevent XSS
        escapeHtml(text) {
            const div = document.createElement('div');
            div.textContent = text;
            return div.innerHTML;
        },

        // Clear logs
        clear() {
            const container = document.getElementById('log-container');
            if (container) {
                container.innerHTML = '';
                this.logCount = 0;
            }
        },

        // Different log level methods
        info(message) {
            this.log(message, 'info');
        },

        success(message) {
            this.log(message, 'success');
        },

        warning(message) {
            this.log(message, 'warning');
        },

        error(message) {
            this.log(message, 'error');
        }
    };

    // Prevent duplicate execution
    if (window.nodelocUserInfoRunning) {
        Logger.warning('⚠️ Script is already running, do not execute again');
        return;
    }
    window.nodelocUserInfoRunning = true;

    // Get user ID (execute only once)
    async function getUserID() {
        return new Promise((resolve, reject) => {
            let retryCount = 0;
            const maxRetries = 60; // 1 minute (60 attempts)
            const retryInterval = setInterval(() => {
                try {
                    // let userImgBTN = document.getElementById("toggle-current-user");
                    // if (userImgBTN) {
                    //     let srcURL = userImgBTN.getElementsByTagName("img")[0].src;
                    //     let srcArr = srcURL.split('/');
                    //     const id = srcArr[srcArr.length - 3];
                    //     clearInterval(retryInterval);
                    //     Logger.success('✅ Got user ID: ' + id);
                    //     resolve(id);
                    // } else if (retryCount >= maxRetries) {
                    //     clearInterval(retryInterval);
                    //     Logger.error('❌ User ID element not found, retry timeout');
                    //     resolve(null);
                    // }
                    let preloaded = JSON.parse($("discourse-assets-json div").attr("data-preloaded"));
                    if(preloaded){
                        const id = JSON.parse(preloaded.currentUser).username;
                        clearInterval(retryInterval);
                        Logger.success('✅ Got user ID: ' + id);
                        resolve(id);
                    } else if (retryCount >= maxRetries) {
                        clearInterval(retryInterval);
                        Logger.error('❌ User ID element not found, retry timeout');
                        resolve(null);
                    }

                    retryCount++;
                } catch (err) {
                    Logger.error('❌ Error getting user ID: ' + err);
                    if (retryCount >= maxRetries) {
                        clearInterval(retryInterval);
                        resolve(null);
                    }
                }
            }, 1000); // Retry every second
        });
    }

    // Get user power value
    async function getUserPower(uid) {
        if (!uid) {
            Logger.warning('⚠️ userID is empty, cannot get power');
            return null;
        }
        try {
            const url = `${window.location.origin}/u/${uid}.json`;
            const response = await fetch(url);
            if (!response.ok) {
                throw new Error('Network request failed');
            }
            const data = await response.json();
            return data.user.gamification_score || 0;
        } catch (error) {
            Logger.error('❌ Error getting user power: ' + error);
            return null;
        }
    }

    // Get upgrade progress
    async function getUpgradeProgress(uid) {
        if (!uid) {
            Logger.warning('⚠️ userID is empty, cannot get upgrade progress');
            return null;
        }
        try {
            const url = `${window.location.origin}/u/${uid}/upgrade-progress.json`;
            const response = await fetch(url);
            if (!response.ok) {
                throw new Error('Network request failed');
            }
            const data = await response.json();
            Logger.info('📊 Successfully fetched upgrade progress');
            return {
                nextLevel: data.next_level_name || 'Unknown',
                unmetConditions: data.unmet_conditions || [],
                metConditions: data.met_conditions || []
            };
        } catch (error) {
            Logger.error('❌ Error fetching upgrade progress: ' + error);
            return null;
        }
    }

    // Parse progress string, extract numbers
    // Example: "阅读帖子数(近100天):15130/20000" => { text: "阅读帖子数(近100天)", current: 15130, total: 20000 }
    function parseProgressItem(item) {
        const match = item.match(/^(.+?):(\d+)\/(\d+)$/);
        if (match) {
            return {
                text: match[1],
                current: parseInt(match[2], 10),
                total: parseInt(match[3], 10),
                original: item
            };
        }
        // If no numeric format, return original text
        return {
            text: item,
            current: null,
            total: null,
            original: item
        };
    }

    // Save initial progress values (called only on first start)
    function saveInitialProgress(unmetConditions) {
        if (progressTracker.hasStarted) {
            return; // Already saved initial values
        }

        unmetConditions.forEach(item => {
            const parsed = parseProgressItem(item);
            if (parsed.current !== null) {
                progressTracker.initial[parsed.text] = parsed.current;
            }
        });

        progressTracker.hasStarted = true;
        Logger.info('💾 Saved initial progress: ' + JSON.stringify(progressTracker.initial));
    }

    // Update current progress values and calculate delta
    function updateCurrentProgress(unmetConditions) {
        const progressWithDelta = [];

        unmetConditions.forEach(item => {
            const parsed = parseProgressItem(item);

            if (parsed.current !== null) {
                // Update current value
                progressTracker.current[parsed.text] = parsed.current;

                // Calculate delta
                let delta = 0;
                if (progressTracker.hasStarted && progressTracker.initial[parsed.text] !== undefined) {
                    delta = parsed.current - progressTracker.initial[parsed.text];
                }

                progressWithDelta.push({
                    ...parsed,
                    delta: delta
                });
            } else {
                // No numeric value, keep as is
                progressWithDelta.push(parsed);
            }
        });

        return progressWithDelta;
    }

    // Update progress display
    async function updateProgressDisplay() {
        if (!userID) {
            Logger.warning('⚠️ User ID not yet obtained');
            return;
        }

        const power = await getUserPower(userID);
        const progress = await getUpgradeProgress(userID);

        if (power !== null) {
            const powerEl = document.getElementById('user-power');
            if (powerEl) {
                powerEl.textContent = power;
            }
        }

        if (progress) {
            const levelEl = document.getElementById('next-level');
            if (levelEl) {
                levelEl.textContent = progress.nextLevel;
            }

            // Save initial values on first load
            if (!progressTracker.hasStarted && progress.unmetConditions.length > 0) {
                saveInitialProgress(progress.unmetConditions);
            }

            // Compute progress changes
            const progressWithDelta = updateCurrentProgress(progress.unmetConditions);

            // Update unmet conditions list
            const unmetListEl = document.getElementById('unmet-conditions-list');
            if (unmetListEl) {
                if (progressWithDelta.length === 0) {
                    unmetListEl.innerHTML = '<div class="progress-item">🎉 All conditions met!</div>';
                } else {
                    unmetListEl.innerHTML = progressWithDelta.map(item => {
                        let deltaStr = '';
                        if (item.delta && item.delta !== 0) {
                            const deltaClass = item.delta > 0 ? 'delta-positive' : 'delta-negative';
                            const deltaSign = item.delta > 0 ? '+' : '';
                            deltaStr = `<span class="${deltaClass}">(${deltaSign}${item.delta})</span>`;
                        }

                        if (item.current !== null && item.total !== null) {
                            return `<div class="progress-item">${item.text}:${item.current}/${item.total} ${deltaStr}</div>`;
                        } else {
                            return `<div class="progress-item">${item.original}</div>`;
                        }
                    }).join('');
                }
            }
        }

        const timeEl = document.getElementById('last-update-time');
        if (timeEl) {
            const now = new Date();
            timeEl.textContent = now.toLocaleTimeString('zh-CN', { hour12: false });
        }
    }

    // Start timed progress updates
    function startProgressUpdateTimer() {
        if (progressUpdateTimer) {
            clearInterval(progressUpdateTimer);
        }
        // Timed update
        progressUpdateTimer = setInterval(async () => {
            Logger.info('⏰ Timed user progress update');
            await updateProgressDisplay();
        }, config.progressUpdateInterval);
    }

    // Stop timed updates
    function stopProgressUpdateTimer() {
        if (progressUpdateTimer) {
            clearInterval(progressUpdateTimer);
            progressUpdateTimer = null;
        }
    }

    // Create floating control panel
    function createControlPanel() {
        const panel = document.createElement('div');
        panel.id = 'nodeloc-user-info-panel';
        panel.innerHTML = `
            <div class="panel-header">
                <span>NodeLoc 用户信息</span>
                <button id="panel-close" title="关闭面板">×</button>
            </div>
            <div class="panel-body">
                <div class="status-row">
                    <span class="status-label">当前时间:</span>
                    <span id="current-time" class="status-value">--:--:--</span>
                </div>
                <div class="divider"></div>

                <!-- Tab navigation -->
                <div class="tab-navigation">
                    <button class="tab-btn active" data-tab="user-info">用户信息</button>
                    <button class="tab-btn" data-tab="logs">运行日志</button>
                </div>

                <!-- Tab content -->
                <div class="tab-content">
                    <!-- User Info Tab -->
                    <div class="tab-panel active" id="tab-user-info">
                        <div class="status-row">
                            <span class="status-label">能量:</span>
                            <span id="user-power" class="status-value highlight">加载中...</span>
                        </div>
                        <div class="status-row progress-info-row" title="点击刷新进度">
                            <span class="status-label">下一等级:</span>
                            <span id="next-level" class="status-value">加载中...</span>
                        </div>
                        <div class="progress-section">
                            <div class="progress-header">未完成条件:</div>
                            <div id="unmet-conditions-list" class="progress-list">
                                <div class="progress-item">加载中...</div>
                            </div>
                        </div>
                        <div class="status-row">
                            <span class="status-label" style="font-size: 11px; opacity: 0.7;">更新时间:</span>
                            <span id="last-update-time" class="status-value" style="font-size: 11px; opacity: 0.7;">--:--:--</span>
                        </div>
                        <button id="refresh-btn" class="control-btn refresh-btn">手动刷新</button>
                    </div>

                    <!-- Logs Tab -->
                    <div class="tab-panel" id="tab-logs">
                        <div class="log-section">
                            <div class="log-header">
                                <span>运行日志</span>
                                <button id="clear-log-btn" class="clear-log-btn" title="清空日志">清空</button>
                            </div>
                            <div id="log-container" class="log-container"></div>
                        </div>
                    </div>
                </div>
            </div>
        `;

        // Add styles
        const style = document.createElement('style');
        style.textContent = `
            #nodeloc-user-info-panel {
                position: fixed;
                top: 20px;
                right: 20px;
                width: 320px;
                max-height: 90vh;
                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                border-radius: 12px;
                box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
                z-index: 999999;
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
                color: white;
                overflow: hidden;
                display: flex;
                flex-direction: column;
            }
            .panel-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 12px 16px;
                background: rgba(0, 0, 0, 0.2);
                font-weight: 600;
                font-size: 14px;
                cursor: move;
                flex-shrink: 0;
            }
            #panel-close {
                background: none;
                border: none;
                color: white;
                font-size: 24px;
                line-height: 1;
                cursor: pointer;
                padding: 0;
                width: 24px;
                height: 24px;
                display: flex;
                align-items: center;
                justify-content: center;
                border-radius: 4px;
                transition: background 0.2s;
            }
            #panel-close:hover {
                background: rgba(255, 255, 255, 0.2);
            }
            .panel-body {
                padding: 16px;
                overflow-y: auto;
                flex: 1;
            }
            .status-row {
                display: flex;
                justify-content: space-between;
                margin-bottom: 10px;
                font-size: 13px;
            }
            .status-label {
                opacity: 0.9;
            }
            .status-value {
                font-weight: 600;
            }
            .status-value.highlight {
                color: #fbbf24;
                font-size: 14px;
            }
            .divider {
                height: 1px;
                background: rgba(255, 255, 255, 0.2);
                margin: 12px 0;
            }
            .progress-info-row {
                cursor: pointer;
                transition: background 0.2s;
                padding: 4px;
                margin: -4px;
                margin-bottom: 6px;
                border-radius: 4px;
            }
            .progress-info-row:hover {
                background: rgba(255, 255, 255, 0.1);
            }
            .progress-section {
                margin: 12px 0;
            }
            .progress-header {
                font-size: 12px;
                opacity: 0.9;
                margin-bottom: 8px;
                font-weight: 600;
            }
            .progress-list {
                background: rgba(0, 0, 0, 0.15);
                border-radius: 6px;
                padding: 8px;
                max-height: 200px;
                overflow-y: auto;
            }
            .progress-item {
                font-size: 11px;
                line-height: 1.5;
                padding: 4px 6px;
                margin-bottom: 4px;
                background: rgba(255, 255, 255, 0.1);
                border-radius: 4px;
                word-break: break-word;
            }
            .progress-item:last-child {
                margin-bottom: 0;
            }
            .delta-positive {
                color: #10b981;
                font-weight: 700;
                margin-left: 4px;
            }
            .delta-negative {
                color: #ef4444;
                font-weight: 700;
                margin-left: 4px;
            }
            .control-btn {
                width: 100%;
                padding: 10px;
                border: none;
                border-radius: 8px;
                font-size: 14px;
                font-weight: 600;
                cursor: pointer;
                transition: all 0.3s;
                margin-top: 8px;
            }
            .refresh-btn {
                background: #10b981;
                color: white;
            }
            .refresh-btn:hover {
                background: #059669;
                transform: translateY(-1px);
                box-shadow: 0 4px 12px rgba(16, 185, 129, 0.4);
            }
            .control-btn:active {
                transform: translateY(0);
            }
            /* Scrollbar styles */
            .panel-body::-webkit-scrollbar,
            .progress-list::-webkit-scrollbar {
                width: 6px;
            }
            .panel-body::-webkit-scrollbar-track,
            .progress-list::-webkit-scrollbar-track {
                background: rgba(0, 0, 0, 0.1);
                border-radius: 3px;
            }
            .panel-body::-webkit-scrollbar-thumb,
            .progress-list::-webkit-scrollbar-thumb {
                background: rgba(255, 255, 255, 0.3);
                border-radius: 3px;
            }
            .panel-body::-webkit-scrollbar-thumb:hover,
            .progress-list::-webkit-scrollbar-thumb:hover {
                background: rgba(255, 255, 255, 0.5);
            }
            /* Tab navigation styles */
            .tab-navigation {
                display: flex;
                gap: 8px;
                margin-bottom: 12px;
            }
            .tab-btn {
                flex: 1;
                padding: 8px 12px;
                background: rgba(255, 255, 255, 0.1);
                border: none;
                border-radius: 6px;
                color: rgba(255, 255, 255, 0.7);
                font-size: 12px;
                font-weight: 600;
                cursor: pointer;
                transition: all 0.2s;
            }
            .tab-btn:hover {
                background: rgba(255, 255, 255, 0.15);
                color: rgba(255, 255, 255, 0.9);
            }
            .tab-btn.active {
                background: rgba(255, 255, 255, 0.25);
                color: white;
            }
            .tab-content {
                margin-bottom: 8px;
            }
            .tab-panel {
                display:;
            }
            .tab-panel.active {
                display: block;
            }
            /* Log area styles */
            .log-section {
                margin: 0;
            }
            .log-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                font-size: 12px;
                opacity: 0.9;
                margin-bottom: 8px;
                font-weight: 600;
            }
            .clear-log-btn {
                background: rgba(255, 255, 255, 0.15);
                border: none;
                color: white;
                font-size: 11px;
                padding: 4px 10px;
                border-radius: 4px;
                cursor: pointer;
                transition: all 0.2s;
            }
            .clear-log-btn:hover {
                background: rgba(255, 255, 255, 0.25);
            }
            .log-container {
                background: rgba(0, 0, 0, 0.25);
                border-radius: 6px;
                padding: 8px;
                max-height: 200px;
                min-height: 120px;
                overflow-y: auto;
                font-family: 'Courier New', monospace;
                font-size: 11px;
                line-height: 1.5;
            }
            .log-entry {
                padding: 3px 6px;
                margin-bottom: 3px;
                border-radius: 3px;
                word-wrap: break-word;
                white-space: pre-wrap;
                border-left: 3px solid transparent;
            }
            .log-entry.info {
                border-left-color: #3b82f6;
                background: rgba(59, 130, 246, 0.1);
            }
            .log-entry.success {
                border-left-color: #10b981;
                background: rgba(16, 185, 129, 0.1);
            }
            .log-entry.warning {
                border-left-color: #f59e0b;
                background: rgba(245, 158, 11, 0.1);
            }
            .log-entry.error {
                border-left-color: #ef4444;
                background: rgba(239, 68, 68, 0.1);
            }
            .log-time {
                opacity: 0.6;
                margin-right: 6px;
            }
            .log-container::-webkit-scrollbar {
                width: 6px;
            }
            .log-container::-webkit-scrollbar-track {
                background: rgba(0, 0, 0, 0.1);
                border-radius: 3px;
            }
            .log-container::-webkit-scrollbar-thumb {
                background: rgba(255, 255, 255, 0.3);
                border-radius: 3px;
            }
            .log-container::-webkit-scrollbar-thumb:hover {
                background: rgba(255, 255, 255, 0.5);
            }
        `;
        document.head.appendChild(style);
        document.body.appendChild(panel);

        // Make panel draggable
        makeDraggable(panel);

        // Tab switching functionality
        const tabButtons = panel.querySelectorAll('.tab-btn');
        const tabPanels = panel.querySelectorAll('.tab-panel');

        tabButtons.forEach(button => {
            button.addEventListener('click', () => {
                const targetTab = button.getAttribute('data-tab');

                // Remove all active classes
                tabButtons.forEach(btn => btn.classList.remove('active'));
                tabPanels.forEach(pnl => pnl.classList.remove('active'));

                // Add active class to current tab
                button.classList.add('active');
                const targetPanel = panel.querySelector(`#tab-${targetTab}`);
                if (targetPanel) {
                    targetPanel.classList.add('active');
                }
            });
        });

        return panel;
    }

    // Make element draggable
    function makeDraggable(element) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        const header = element.querySelector('.panel-header');

        header.onmousedown = dragMouseDown;

        function dragMouseDown(e) {
            e.preventDefault();
            pos3 = e.clientX;
            pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        }

        function elementDrag(e) {
            e.preventDefault();
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;
            element.style.top = (element.offsetTop - pos2) + "px";
            element.style.left = (element.offsetLeft - pos1) + "px";
            element.style.right = "auto";
        }

        function closeDragElement() {
            document.onmouseup = null;
            document.onmousemove = null;
        }
    }

    // Update time display
    function updateTime() {
        const timeEl = document.getElementById('current-time');
        if (timeEl) {
            const now = new Date();
            timeEl.textContent = now.toLocaleTimeString('zh-CN', { hour12: false });
        }
    }

    // Create control panel
    const panel = createControlPanel();
    const closeBtn = document.getElementById('panel-close');
    const clearLogBtn = document.getElementById('clear-log-btn');
    const refreshBtn = document.getElementById('refresh-btn');

    // Clear log button event
    clearLogBtn.addEventListener('click', () => {
        Logger.clear();
        Logger.info('日志已清空');
    });

    // Manual refresh button event
    refreshBtn.addEventListener('click', async () => {
        Logger.info('🔄 手动刷新进度信息');
        await updateProgressDisplay();
    });

    // Timed time update
    setInterval(updateTime, 1000);
    updateTime();

    // On init, get user ID and start timed updates
    (async () => {
        Logger.info('🔍 正在获取用户ID...');
        userID = await getUserID();
        if (userID) {
            Logger.success('✅ 用户ID获取成功,准备加载进度信息');
            await updateProgressDisplay();

            // Start timed updates
            Logger.info(`⏰ 启动定时更新,间隔 ${config.progressUpdateInterval / 60000} 分钟`);
            startProgressUpdateTimer();
        } else {
            Logger.warning('⚠️ 用户ID获取失败,进度信息将不可用');
        }
    })();

    // Progress info row click event (manual refresh)
    const progressRow = document.querySelector('.progress-info-row');
    if (progressRow) {
        progressRow.addEventListener('click', async () => {
            Logger.info('🔄 手动刷新进度信息');
            await updateProgressDisplay();
        });
    }

    // Close button event
    closeBtn.addEventListener('click', () => {
        if (confirm('确定要关闭控制面板吗?\n\n关闭后脚本将停止运行。')) {
            stopProgressUpdateTimer(); // Stop timer
            panel.remove();
            window.nodelocUserInfoRunning = false;
            Logger.info('👋 脚本已停止');
        }
    });
})();
19 Likes

Let’s see if it’s useful.

3 Likes

Just added a bit of timely feedback! I wish I had known earlier!

3 Likes

Keep upgrading!

4 Likes

Give it a try, thanks for sharing, boss.

3 Likes

If you discuss this, I guess the admin or site owner will increase the difficulty level of your upgrade.

5 Likes

Ah, this just adds a bit of real‑time feedback, it’s not a violation, right?

4 Likes

This is kind of interesting.

3 Likes

This looks good.
Looks great, give it some support

3 Likes

Thank you for sharing

2 Likes

It keeps showing loading and doesn’t move.

3 Likes

Let me see

2 Likes

Thank you for sharing

2 Likes

It’s possible, keep leveling up

2 Likes

Support the Diamond Dream

2 Likes

Not bad​:relieved_face:

2 Likes

Check if there are any errors in the runtime log.

看看隐藏代码

It seems that diamond users are not working.

Take a look