This commit is contained in:
BRanulf 2025-06-05 12:04:24 +08:00
parent 23a0518ca7
commit 1ffa91a98a
5 changed files with 58 additions and 81 deletions

View File

@ -6,7 +6,7 @@ minecraft_version=1.21.4
yarn_mappings=1.21.4+build.8 yarn_mappings=1.21.4+build.8
loader_version=0.16.10 loader_version=0.16.10
# Mod Properties # Mod Properties
mod_version=1.14.514.128 mod_version=1.14.514.129
maven_group=org.example1 maven_group=org.example1
archives_base_name=playerOnlineTimeTrackerMod archives_base_name=playerOnlineTimeTrackerMod
# Dependencies # Dependencies

View File

@ -9,6 +9,7 @@ public class ModConfig {
private final Path configPath; private final Path configPath;
private int webPort = 60048; private int webPort = 60048;
private String language = "zh_cn"; private String language = "zh_cn";
private long autoSaveSeconds = 300;
public ModConfig(Path configDir) { public ModConfig(Path configDir) {
this.configPath = configDir.resolve("playertime-config.json"); this.configPath = configDir.resolve("playertime-config.json");
@ -17,7 +18,7 @@ public class ModConfig {
private void loadConfig() { private void loadConfig() {
if (!Files.exists(configPath)) { if (!Files.exists(configPath)) {
PlayerTimeMod.LOGGER.info("[在线时间] 配置文件未找到,正在创建默认配置"); // 使用英文日志或者也本地化日志这里先用英文 PlayerTimeMod.LOGGER.info("[在线时间] 配置文件未找到,正在创建默认配置");
saveConfig(); saveConfig();
return; return;
} }
@ -35,6 +36,9 @@ public class ModConfig {
if (json.has("webPort")) { if (json.has("webPort")) {
webPort = json.get("webPort").getAsInt(); webPort = json.get("webPort").getAsInt();
} else {
PlayerTimeMod.LOGGER.info("[在线时间] 配置文件缺少“webPort”字段添加默认值'%s'并保存", webPort);
saveConfig();
} }
if (json.has("language")) { if (json.has("language")) {
language = json.get("language").getAsString(); language = json.get("language").getAsString();
@ -42,6 +46,12 @@ public class ModConfig {
PlayerTimeMod.LOGGER.info("[在线时间] 配置文件缺少“language”字段添加默认值'%s'并保存", language); PlayerTimeMod.LOGGER.info("[在线时间] 配置文件缺少“language”字段添加默认值'%s'并保存", language);
saveConfig(); saveConfig();
} }
if (json.has("autoSaveSeconds")) {
autoSaveSeconds = json.get("autoSaveSeconds").getAsLong();
} else {
PlayerTimeMod.LOGGER.info("[在线时间] 配置文件缺少“autoSaveSeconds”字段添加默认值'%s'并保存", autoSaveSeconds);
saveConfig();
}
} catch (Exception e) { } catch (Exception e) {
PlayerTimeMod.LOGGER.error("[在线时间] 加载配置文件失败,使用默认配置", e); PlayerTimeMod.LOGGER.error("[在线时间] 加载配置文件失败,使用默认配置", e);
@ -54,6 +64,7 @@ public class ModConfig {
JsonObject json = new JsonObject(); JsonObject json = new JsonObject();
json.addProperty("webPort", webPort); json.addProperty("webPort", webPort);
json.addProperty("language", language); json.addProperty("language", language);
json.addProperty("autoSaveSeconds", autoSaveSeconds);
try { try {
Files.createDirectories(configPath.getParent()); Files.createDirectories(configPath.getParent());
@ -75,4 +86,9 @@ public class ModConfig {
public String getLanguage() { public String getLanguage() {
return language; return language;
} }
// 获取保存间隔
public long getSeconds() {
return autoSaveSeconds;
}
} }

View File

@ -35,13 +35,13 @@ public class PlayerTimeMod implements ModInitializer {
// TODO 定时保存配置文件没整暂时硬编码 // TODO 定时保存配置文件没整暂时硬编码
private ScheduledExecutorService scheduler; private ScheduledExecutorService scheduler;
private ScheduledFuture<?> saveTask; private ScheduledFuture<?> saveTask;
private static final long AUTO_SAVE_INTERVAL_SECONDS = 5 * 60; private static long AUTO_SAVE_INTERVAL_SECONDS;
@Override @Override
public void onInitialize() { public void onInitialize() {
config = new ModConfig(FabricLoader.getInstance().getConfigDir()); config = new ModConfig(FabricLoader.getInstance().getConfigDir());
localizationManager = new LocalizationManager(config.getLanguage()); // 初始化本地化管理器 localizationManager = new LocalizationManager(config.getLanguage());
scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler = Executors.newSingleThreadScheduledExecutor();
@ -49,6 +49,8 @@ public class PlayerTimeMod implements ModInitializer {
try { try {
LOGGER.info("[在线时间] 初始化 玩家在线时长视奸Mod"); LOGGER.info("[在线时间] 初始化 玩家在线时长视奸Mod");
AUTO_SAVE_INTERVAL_SECONDS = config.getSeconds();
ServerLifecycleEvents.SERVER_STARTING.register(server -> { ServerLifecycleEvents.SERVER_STARTING.register(server -> {
timeTracker = new PlayerTimeTracker(server); timeTracker = new PlayerTimeTracker(server);
try { try {
@ -102,7 +104,7 @@ public class PlayerTimeMod implements ModInitializer {
} }
} catch (InterruptedException e) { } catch (InterruptedException e) {
LOGGER.error("[在线时间] 调度程序终止被中断", e); LOGGER.error("[在线时间] 调度程序终止被中断", e);
Thread.currentThread().interrupt(); // Restore interrupted status Thread.currentThread().interrupt();
} }
LOGGER.info("[在线时间] 调度程序关闭"); LOGGER.info("[在线时间] 调度程序关闭");
} }

View File

@ -1,17 +1,17 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// 1. 首先初始化所有变量 // 初始化变量
const tpsHistory = Array(30).fill(20); const tpsHistory = Array(30).fill(20);
const msptHistory = Array(30).fill(50); const msptHistory = Array(30).fill(50);
let memoryChart = null; let memoryChart = null;
let performanceChart = null; let performanceChart = null;
let lang = {}; // 新增:存储语言文件内容 let lang = {};
// 2. 获取DOM元素带安全检查 // 获取语言
const elements = { const elements = {
refreshBtn: document.getElementById('refresh-btn'), refreshBtn: document.getElementById('refresh-btn'),
themeToggle: document.getElementById('theme-toggle'), themeToggle: document.getElementById('theme-toggle'),
statsTableBody: document.getElementById('stats-table')?.getElementsByTagName('tbody')[0], // Changed ID for clarity statsTableBody: document.getElementById('stats-table')?.getElementsByTagName('tbody')[0],
statsTableHeader: document.getElementById('stats-table')?.getElementsByTagName('thead')[0], // Added for header localization statsTableHeader: document.getElementById('stats-table')?.getElementsByTagName('thead')[0],
whitelistPlayers: document.getElementById('whitelist-players'), whitelistPlayers: document.getElementById('whitelist-players'),
nonWhitelistPlayers: document.getElementById('non-whitelist-players'), nonWhitelistPlayers: document.getElementById('non-whitelist-players'),
totalCount: document.getElementById('total-count'), totalCount: document.getElementById('total-count'),
@ -20,30 +20,27 @@ document.addEventListener('DOMContentLoaded', function() {
memoryUsed: document.getElementById('memory-used'), memoryUsed: document.getElementById('memory-used'),
memoryFree: document.getElementById('memory-free'), memoryFree: document.getElementById('memory-free'),
memoryPercent: document.getElementById('memory-percent'), memoryPercent: document.getElementById('memory-percent'),
avgTick: document.getElementById('avg-tick'), // This element seems unused in updateServerStatus now, can remove if not needed avgTick: document.getElementById('avg-tick'),
uptime: document.getElementById('uptime'), uptime: document.getElementById('uptime'),
tpsValue: document.getElementById('tps-value'), tpsValue: document.getElementById('tps-value'),
msptValue: document.getElementById('mspt-value') msptValue: document.getElementById('mspt-value')
}; };
// 3. 初始化主题
const savedTheme = localStorage.getItem('theme') || 'light'; const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme); document.documentElement.setAttribute('data-theme', savedTheme);
// 4. 初始加载语言文件,然后加载其他数据并初始化图表 // 加载语言
loadLanguage().then(() => { loadLanguage().then(() => {
translatePage(); // 本地化页面静态文本 translatePage();
initCharts(); // 初始化图表(需要语言文本) initCharts();
loadAllData(); // 加载动态数据 loadAllData();
// 7. 设置定时刷新10秒 // 定时刷新
const refreshInterval = setInterval(loadAllData, 10000); const refreshInterval = setInterval(loadAllData, 10000);
}).catch(error => { }).catch(error => {
console.error('Failed to load language or initial data:', error); console.error('Failed to load language or initial data:', error);
showError('Failed to load language or initial data.'); // Fallback error message showError('Failed to load language or initial data.');
}); });
// 5. 事件监听器
if (elements.themeToggle) { if (elements.themeToggle) {
elements.themeToggle.addEventListener('click', toggleTheme); elements.themeToggle.addEventListener('click', toggleTheme);
} }
@ -52,9 +49,8 @@ document.addEventListener('DOMContentLoaded', function() {
elements.refreshBtn.addEventListener('click', handleRefresh); elements.refreshBtn.addEventListener('click', handleRefresh);
} }
/*** 功能函数 ***/
// 新增:加载语言文件 // 后端加载语言
async function loadLanguage() { async function loadLanguage() {
try { try {
const response = await fetch('/api/lang'); const response = await fetch('/api/lang');
@ -63,18 +59,15 @@ document.addEventListener('DOMContentLoaded', function() {
console.log('Language file loaded.'); console.log('Language file loaded.');
} catch (error) { } catch (error) {
console.error('Failed to load language file:', error); console.error('Failed to load language file:', error);
// Fallback to English keys if language file fails to load
lang = {}; lang = {};
throw error; // Propagate error to stop further loading if language is critical throw error;
} }
} }
// 新增根据data-lang-key属性本地化页面元素
function translatePage() { function translatePage() {
document.querySelectorAll('[data-lang-key]').forEach(element => { document.querySelectorAll('[data-lang-key]').forEach(element => {
const key = element.getAttribute('data-lang-key'); const key = element.getAttribute('data-lang-key');
if (lang[key]) { if (lang[key]) {
// Special handling for title tag
if (element.tagName === 'TITLE') { if (element.tagName === 'TITLE') {
document.title = lang[key]; document.title = lang[key];
} else { } else {
@ -86,11 +79,9 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} }
// 新增:获取本地化字符串
function getLangString(key, ...args) { function getLangString(key, ...args) {
const pattern = lang[key] || key; // Use key as fallback const pattern = lang[key] || key;
try { try {
// Simple placeholder replacement {0}, {1}, etc.
let result = pattern; let result = pattern;
args.forEach((arg, index) => { args.forEach((arg, index) => {
result = result.replace(new RegExp('\\{' + index + '\\}', 'g'), arg); result = result.replace(new RegExp('\\{' + index + '\\}', 'g'), arg);
@ -98,19 +89,18 @@ document.addEventListener('DOMContentLoaded', function() {
return result; return result;
} catch (e) { } catch (e) {
console.error(`Failed to format string for key: ${key}`, e); console.error(`Failed to format string for key: ${key}`, e);
return pattern; // Return unformatted pattern on error return pattern;
} }
} }
// 图表
function initCharts() { function initCharts() {
// 内存图表
const memoryCtx = document.getElementById('memory-chart')?.getContext('2d'); const memoryCtx = document.getElementById('memory-chart')?.getContext('2d');
if (memoryCtx) { if (memoryCtx) {
memoryChart = new Chart(memoryCtx, { memoryChart = new Chart(memoryCtx, {
type: 'doughnut', type: 'doughnut',
data: { data: {
// Use localized labels
labels: [getLangString('playertime.web.chart.memory_used'), getLangString('playertime.web.chart.memory_free')], labels: [getLangString('playertime.web.chart.memory_used'), getLangString('playertime.web.chart.memory_free')],
datasets: [{ datasets: [{
data: [0, 100], data: [0, 100],
@ -129,7 +119,6 @@ document.addEventListener('DOMContentLoaded', function() {
}); });
} }
// 性能图表
const perfCtx = document.getElementById('performance-chart')?.getContext('2d'); const perfCtx = document.getElementById('performance-chart')?.getContext('2d');
if (perfCtx) { if (perfCtx) {
performanceChart = new Chart(perfCtx, { performanceChart = new Chart(perfCtx, {
@ -138,7 +127,6 @@ document.addEventListener('DOMContentLoaded', function() {
labels: Array(30).fill(''), labels: Array(30).fill(''),
datasets: [ datasets: [
{ {
// Use localized labels
label: getLangString('playertime.web.chart.tps'), label: getLangString('playertime.web.chart.tps'),
data: tpsHistory, data: tpsHistory,
borderColor: '#4CAF50', borderColor: '#4CAF50',
@ -150,7 +138,6 @@ document.addEventListener('DOMContentLoaded', function() {
yAxisID: 'y' yAxisID: 'y'
}, },
{ {
// Use localized labels
label: getLangString('playertime.web.chart.mspt'), label: getLangString('playertime.web.chart.mspt'),
data: msptHistory, data: msptHistory,
borderColor: '#FF5722', borderColor: '#FF5722',
@ -203,7 +190,6 @@ document.addEventListener('DOMContentLoaded', function() {
position: 'left', position: 'left',
title: { title: {
display: true, display: true,
// Use localized labels
text: getLangString('playertime.web.chart.tps'), text: getLangString('playertime.web.chart.tps'),
font: { font: {
weight: 'bold' weight: 'bold'
@ -221,7 +207,6 @@ document.addEventListener('DOMContentLoaded', function() {
position: 'right', position: 'right',
title: { title: {
display: true, display: true,
// Use localized labels
text: getLangString('playertime.web.chart.mspt'), text: getLangString('playertime.web.chart.mspt'),
font: { font: {
weight: 'bold' weight: 'bold'
@ -246,10 +231,10 @@ document.addEventListener('DOMContentLoaded', function() {
} }
function handleRefresh() { function handleRefresh() {
if (this.classList.contains('loading')) return; // Prevent multiple clicks if (this.classList.contains('loading')) return;
this.classList.add('loading'); this.classList.add('loading');
loadAllData().finally(() => { // Use finally to ensure loading class is removed loadAllData().finally(() => {
setTimeout(() => { // Add a small delay for visual feedback setTimeout(() => {
this.classList.remove('loading'); this.classList.remove('loading');
}, 500); }, 500);
}); });
@ -257,7 +242,6 @@ document.addEventListener('DOMContentLoaded', function() {
async function loadAllData() { async function loadAllData() {
try { try {
// Fetch data concurrently
const [statsData, onlinePlayersData, playerCountsData, serverStatusData] = await Promise.all([ const [statsData, onlinePlayersData, playerCountsData, serverStatusData] = await Promise.all([
fetchData('/api/stats'), fetchData('/api/stats'),
fetchData('/api/online-players'), fetchData('/api/online-players'),
@ -265,7 +249,6 @@ document.addEventListener('DOMContentLoaded', function() {
fetchData('/api/server-status') fetchData('/api/server-status')
]); ]);
// Update UI with fetched data
updateTable(statsData); updateTable(statsData);
updateOnlinePlayers(onlinePlayersData); updateOnlinePlayers(onlinePlayersData);
updatePlayerCounts(playerCountsData); updatePlayerCounts(playerCountsData);
@ -273,14 +256,13 @@ document.addEventListener('DOMContentLoaded', function() {
} catch (error) { } catch (error) {
console.error('加载数据出错:', error); console.error('加载数据出错:', error);
showError(getLangString('playertime.web.error.load_failed')); // Use localized error message showError(getLangString('playertime.web.error.load_failed'));
} }
} }
async function fetchData(url) { async function fetchData(url) {
const response = await fetch(url); const response = await fetch(url);
if (!response.ok) { if (!response.ok) {
// Attempt to read error body if available
const errorBody = await response.text().catch(() => 'Unknown Error'); const errorBody = await response.text().catch(() => 'Unknown Error');
throw new Error(`HTTP error! status: ${response.status}, URL: ${url}, Body: ${errorBody}`); throw new Error(`HTTP error! status: ${response.status}, URL: ${url}, Body: ${errorBody}`);
} }
@ -290,9 +272,7 @@ document.addEventListener('DOMContentLoaded', function() {
function updateTable(data) { function updateTable(data) {
if (!elements.statsTableBody || !elements.statsTableHeader) return; if (!elements.statsTableBody || !elements.statsTableHeader) return;
elements.statsTableBody.innerHTML = ''; // Clear existing rows elements.statsTableBody.innerHTML = '';
// Update table headers using localization
const headers = elements.statsTableHeader.querySelectorAll('th[data-lang-key]'); const headers = elements.statsTableHeader.querySelectorAll('th[data-lang-key]');
headers.forEach(th => { headers.forEach(th => {
const key = th.getAttribute('data-lang-key'); const key = th.getAttribute('data-lang-key');
@ -304,25 +284,19 @@ document.addEventListener('DOMContentLoaded', function() {
const sortedPlayers = Object.entries(data) const sortedPlayers = Object.entries(data)
.map(([name, statString]) => { .map(([name, statString]) => {
// Parse the localized stat string
const stats = {}; const stats = {};
// Split by " | " first
statString.split(" | ").forEach(part => { statString.split(" | ").forEach(part => {
// Find the first colon to split label and value
const firstColonIndex = part.indexOf(':'); const firstColonIndex = part.indexOf(':');
if (firstColonIndex > 0) { if (firstColonIndex > 0) {
const label = part.substring(0, firstColonIndex).trim(); const label = part.substring(0, firstColonIndex).trim();
const value = part.substring(firstColonIndex + 1).trim(); const value = part.substring(firstColonIndex + 1).trim();
stats[label] = value; stats[label] = value;
} else { } else {
// Handle cases without colon if necessary, or log a warning
console.warn(`Could not parse stat part: ${part}`); console.warn(`Could not parse stat part: ${part}`);
} }
}); });
// Map localized labels back to internal keys for sorting
const internalStats = {}; const internalStats = {};
// This mapping assumes the order in the format string is consistent
const formatParts = getLangString('playertime.stats.format').split(" | ").map(p => p.split(":")[0].trim()); const formatParts = getLangString('playertime.stats.format').split(" | ").map(p => p.split(":")[0].trim());
if (formatParts.length >= 3) { if (formatParts.length >= 3) {
@ -331,7 +305,6 @@ document.addEventListener('DOMContentLoaded', function() {
internalStats['7Days'] = stats[formatParts[2]]; internalStats['7Days'] = stats[formatParts[2]];
} else { } else {
console.error("Unexpected format string structure:", getLangString('playertime.stats.format')); console.error("Unexpected format string structure:", getLangString('playertime.stats.format'));
// Fallback to using the raw statString if parsing fails
internalStats.totalTime = statString.split(" | ")[0]?.split(": ")[1] || "0h 00m"; internalStats.totalTime = statString.split(" | ")[0]?.split(": ")[1] || "0h 00m";
internalStats['30Days'] = statString.split(" | ")[1]?.split(": ")[1] || "0h 00m"; internalStats['30Days'] = statString.split(" | ")[1]?.split(": ")[1] || "0h 00m";
internalStats['7Days'] = statString.split(" | ")[2]?.split(": ")[1] || "0h 00m"; internalStats['7Days'] = statString.split(" | ")[2]?.split(": ")[1] || "0h 00m";
@ -345,8 +318,8 @@ document.addEventListener('DOMContentLoaded', function() {
if (sortedPlayers.length === 0) { if (sortedPlayers.length === 0) {
const row = elements.statsTableBody.insertRow(); const row = elements.statsTableBody.insertRow();
const cell = row.insertCell(0); const cell = row.insertCell(0);
cell.colSpan = 4; // Span across all columns cell.colSpan = 4;
cell.textContent = getLangString('playertime.web.stats_table.empty'); // Use localized empty message cell.textContent = getLangString('playertime.web.stats_table.empty');
cell.style.textAlign = 'center'; cell.style.textAlign = 'center';
cell.style.fontStyle = 'italic'; cell.style.fontStyle = 'italic';
return; return;
@ -374,7 +347,6 @@ document.addEventListener('DOMContentLoaded', function() {
element.appendChild(li); element.appendChild(li);
}); });
} else { } else {
// Use localized empty message
element.innerHTML = `<li>${getLangString(emptyMessageKey)}</li>`; element.innerHTML = `<li>${getLangString(emptyMessageKey)}</li>`;
} }
}; };
@ -390,7 +362,6 @@ document.addEventListener('DOMContentLoaded', function() {
} }
function updateServerStatus(data) { function updateServerStatus(data) {
// Update memory info
if (data.memory) { if (data.memory) {
const usedMB = Math.round(data.memory.used / (1024 * 1024)); const usedMB = Math.round(data.memory.used / (1024 * 1024));
const freeMB = Math.round((data.memory.max - data.memory.used) / (1024 * 1024)); const freeMB = Math.round((data.memory.max - data.memory.used) / (1024 * 1024));
@ -398,7 +369,6 @@ document.addEventListener('DOMContentLoaded', function() {
if (memoryChart) { if (memoryChart) {
memoryChart.data.datasets[0].data = [usedMB, freeMB]; memoryChart.data.datasets[0].data = [usedMB, freeMB];
// Update chart labels if they weren't set during initCharts (e.g., if lang loaded later)
memoryChart.data.labels = [getLangString('playertime.web.chart.memory_used'), getLangString('playertime.web.chart.memory_free')]; memoryChart.data.labels = [getLangString('playertime.web.chart.memory_used'), getLangString('playertime.web.chart.memory_free')];
memoryChart.update(); memoryChart.update();
} }
@ -408,60 +378,49 @@ document.addEventListener('DOMContentLoaded', function() {
if (elements.memoryPercent) elements.memoryPercent.textContent = percent; if (elements.memoryPercent) elements.memoryPercent.textContent = percent;
} }
// Update performance info
if (data.server) { if (data.server) {
// Calculate TPS (limit between 0-20)
const tps = Math.min(20, 1000 / (data.server.average_tick_time_ms || 50)).toFixed(1); const tps = Math.min(20, 1000 / (data.server.average_tick_time_ms || 50)).toFixed(1);
const mspt = data.server.average_tick_time_ms.toFixed(1); const mspt = data.server.average_tick_time_ms.toFixed(1);
// Update display
if (elements.tpsValue) elements.tpsValue.textContent = tps; if (elements.tpsValue) elements.tpsValue.textContent = tps;
if (elements.msptValue) elements.msptValue.textContent = mspt; if (elements.msptValue) elements.msptValue.textContent = mspt;
// Update chart data
if (performanceChart) { if (performanceChart) {
// Update history data
tpsHistory.shift(); tpsHistory.shift();
tpsHistory.push(parseFloat(tps)); tpsHistory.push(parseFloat(tps));
msptHistory.shift(); msptHistory.shift();
msptHistory.push(parseFloat(mspt)); msptHistory.push(parseFloat(mspt));
// Update chart labels if they weren't set during initCharts
performanceChart.data.datasets[0].label = getLangString('playertime.web.chart.tps'); performanceChart.data.datasets[0].label = getLangString('playertime.web.chart.tps');
performanceChart.data.datasets[1].label = getLangString('playertime.web.chart.mspt'); performanceChart.data.datasets[1].label = getLangString('playertime.web.chart.mspt');
if (performanceChart.options.scales.y.title) performanceChart.options.scales.y.title.text = getLangString('playertime.web.chart.tps'); if (performanceChart.options.scales.y.title) performanceChart.options.scales.y.title.text = getLangString('playertime.web.chart.tps');
if (performanceChart.options.scales.y1.title) performanceChart.options.scales.y1.title.text = getLangString('playertime.web.chart.mspt'); if (performanceChart.options.scales.y1.title) performanceChart.options.scales.y1.title.text = getLangString('playertime.web.chart.mspt');
// Only update data (do not recreate chart)
performanceChart.data.datasets[0].data = tpsHistory; performanceChart.data.datasets[0].data = tpsHistory;
performanceChart.data.datasets[1].data = msptHistory; performanceChart.data.datasets[1].data = msptHistory;
// Smooth update
performanceChart.update('none'); performanceChart.update('none');
// Set color based on value setMetricColor(elements.tpsValue, tps, 15, 10, false);
setMetricColor(elements.tpsValue, tps, 15, 10, false); // TPS: higher is better setMetricColor(elements.msptValue, mspt, 50, 100, true);
setMetricColor(elements.msptValue, mspt, 50, 100, true); // MSPT: lower is better
} }
// if (elements.avgTick) elements.avgTick.textContent = data.server.average_tick_time_ms?.toFixed(2) || '0'; // This element seems unused
} }
if (elements.uptime) elements.uptime.textContent = data.uptime_formatted || '0'; // uptime_formatted is already localized by server if (elements.uptime) elements.uptime.textContent = data.uptime_formatted || '0';
} }
function parseTimeToSeconds(timeStr) { function parseTimeToSeconds(timeStr) {
if (!timeStr) return 0; if (!timeStr) return 0;
// Ensure parsing works even if labels are present, by only looking for h and m
const parts = timeStr.match(/(\d+h)?\s*(\d+m)?/); const parts = timeStr.match(/(\d+h)?\s*(\d+m)?/);
let seconds = 0; let seconds = 0;
if (parts) { if (parts) {
if (parts[1]) { // hours part if (parts[1]) { // hour
seconds += parseInt(parts[1].replace('h', '')) * 3600; seconds += parseInt(parts[1].replace('h', '')) * 3600;
} }
if (parts[2]) { // minutes part if (parts[2]) { // min
seconds += parseInt(parts[2].replace('m', '')) * 60; seconds += parseInt(parts[2].replace('m', '')) * 60;
} }
} }
@ -480,18 +439,17 @@ document.addEventListener('DOMContentLoaded', function() {
setTimeout(() => { setTimeout(() => {
errorEl.classList.remove('show'); errorEl.classList.remove('show');
setTimeout(() => document.body.removeChild(errorEl), 300); setTimeout(() => document.body.removeChild(errorEl), 300);
}, 5000); // Display for 5 seconds }, 5000);
}, 10); // Small delay to allow CSS transition }, 10);
} }
// Helper function to set color based on metric value and thresholds
function setMetricColor(element, value, goodThreshold, badThreshold, reverse = false) { function setMetricColor(element, value, goodThreshold, badThreshold, reverse = false) {
if (!element) return; if (!element) return;
const numValue = parseFloat(value); const numValue = parseFloat(value);
let color = ''; let color = '';
if (reverse) { // For MSPT: lower is better if (reverse) {
if (numValue <= goodThreshold) { if (numValue <= goodThreshold) {
color = '#4CAF50'; // Green color = '#4CAF50'; // Green
} else if (numValue <= badThreshold) { } else if (numValue <= badThreshold) {
@ -499,7 +457,7 @@ document.addEventListener('DOMContentLoaded', function() {
} else { } else {
color = '#FF5722'; // Red color = '#FF5722'; // Red
} }
} else { // For TPS: higher is better } else {
if (numValue >= goodThreshold) { if (numValue >= goodThreshold) {
color = '#4CAF50'; // Green color = '#4CAF50'; // Green
} else if (numValue >= badThreshold) { } else if (numValue >= badThreshold) {

View File

@ -1,4 +1,5 @@
{ {
"webPort": 60048, "webPort": 60048,
"language": "zh_cn" "language": "zh_cn",
"autoSaveSeconds": 60
} }