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('👋 脚本已停止');
}
});
})();

