230 lines
9.7 KiB
JavaScript
230 lines
9.7 KiB
JavaScript
document.addEventListener('DOMContentLoaded', function() {
|
|
// Helper to format hashrate
|
|
function formatHashrate(mhs) {
|
|
if (mhs >= 1000) {
|
|
return (mhs / 1000).toFixed(2) + ' GH/s';
|
|
}
|
|
return parseFloat(mhs).toFixed(2) + ' MH/s';
|
|
}
|
|
|
|
// Initialize Chart
|
|
const ctx = document.getElementById('hashrateChart').getContext('2d');
|
|
const hashrateChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
label: 'Hashrate',
|
|
data: [],
|
|
borderColor: '#0d6efd',
|
|
backgroundColor: 'rgba(13, 110, 253, 0.1)',
|
|
borderWidth: 2,
|
|
fill: true,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
return formatHashrate(context.raw);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
grid: { color: '#333' },
|
|
ticks: { color: '#aaa' }
|
|
},
|
|
y: {
|
|
grid: { color: '#333' },
|
|
ticks: { color: '#aaa' },
|
|
beginAtZero: true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Helper to format large numbers
|
|
function formatNumber(num) {
|
|
return num.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
|
|
}
|
|
|
|
function updateDashboard() {
|
|
fetch('/api/data')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const summary = data.summary;
|
|
const devs = data.devs;
|
|
const pools = data.pools;
|
|
|
|
// Update Top Stats
|
|
if (summary) {
|
|
const mhs = summary['MHS 5s'] || summary['MHS av'] || 0;
|
|
|
|
// Parse unit
|
|
const formatted = formatHashrate(mhs);
|
|
// Check if GH/s or MH/s
|
|
let unit = 'MH/s';
|
|
let val = mhs;
|
|
if (formatted.includes('GH/s')) {
|
|
unit = 'GH/s';
|
|
val = (mhs / 1000).toFixed(2);
|
|
} else {
|
|
val = parseFloat(mhs).toFixed(2);
|
|
}
|
|
|
|
document.getElementById('stat-mhs').innerText = val;
|
|
// Note: HTML might still say MH/s in static part, need to update ID or text node of <p>
|
|
// But simplified:
|
|
const unitElem = document.querySelector('#stat-mhs').nextElementSibling;
|
|
if(unitElem) unitElem.innerText = unit;
|
|
|
|
document.getElementById('stat-accepted').innerText = formatNumber(summary['Accepted'] || 0);
|
|
document.getElementById('stat-hw').innerText = formatNumber(summary['Hardware Errors'] || 0);
|
|
}
|
|
|
|
// Update Device Count
|
|
if (devs) {
|
|
document.getElementById('stat-devices').innerText = devs.length;
|
|
}
|
|
|
|
// Helper to find temp in stats if missing in devs
|
|
// Cgminer 'stats' command returns specific driver info.
|
|
// Structure: stats ->STATS -> list of dicts. usually one dict per device with ID keys like 'MM ID 0', etc.
|
|
// Or simplified list.
|
|
function getTempFallback(devId, statsData) {
|
|
// Try to find matching ID in stats
|
|
// This is heuristic as structure varies wildy between forks
|
|
return 0;
|
|
}
|
|
|
|
// Update Devs Table & Max Temp
|
|
let maxTemp = 0;
|
|
const tbody = document.getElementById('devs-table-body');
|
|
tbody.innerHTML = '';
|
|
|
|
if (devs && devs.length > 0) {
|
|
devs.forEach(dev => {
|
|
let temp = dev['Temperature'] || 0;
|
|
|
|
// Fallback logic could go here if we understood the STATS structure for this specific fork
|
|
// For now we rely on standard API.
|
|
|
|
if (temp > maxTemp) maxTemp = temp;
|
|
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${dev['ID']}</td>
|
|
<td>${dev['Name'] || 'Gridseed'}</td>
|
|
<td>${dev['Enabled']}</td>
|
|
<td>${dev['Status']}</td>
|
|
<td class="${temp > 50 ? 'text-warning' : 'text-success'}">${temp > 0 ? temp.toFixed(1) : '-'}</td>
|
|
<td>${formatHashrate(dev['MHS 5s'] || 0)}</td>
|
|
<td>${formatHashrate(dev['MHS av'] || 0)}</td>
|
|
<td>${dev['Accepted']}</td>
|
|
<td>${dev['Rejected']}</td>
|
|
<td>${dev['Hardware Errors']}</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
} else {
|
|
tbody.innerHTML = '<tr><td colspan="10" class="text-center text-muted">Keine Geräte gefunden...</td></tr>';
|
|
}
|
|
document.getElementById('stat-temp').innerText = maxTemp.toFixed(1);
|
|
|
|
// Update Chart
|
|
const now = new Date();
|
|
const timeLabel = now.getHours() + ':' + String(now.getMinutes()).padStart(2, '0') + ':' + String(now.getSeconds()).padStart(2, '0');
|
|
|
|
if (summary) {
|
|
const currentMhs = summary['MHS 5s'] || summary['MHS av'] || 0;
|
|
|
|
if (hashrateChart.data.labels.length > 60) { // Keep last 60 points
|
|
hashrateChart.data.labels.shift();
|
|
hashrateChart.data.datasets[0].data.shift();
|
|
}
|
|
hashrateChart.data.labels.push(timeLabel);
|
|
hashrateChart.data.datasets[0].data.push(currentMhs);
|
|
hashrateChart.update();
|
|
}
|
|
|
|
// Update Devs Table
|
|
const tbody = document.getElementById('devs-table-body');
|
|
tbody.innerHTML = '';
|
|
if (devs && devs.length > 0) {
|
|
devs.forEach(dev => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${dev['ID']}</td>
|
|
<td>${dev['Name'] || 'Gridseed'}</td>
|
|
<td>${dev['Enabled']}</td>
|
|
<td>${dev['Status']}</td>
|
|
<td class="${dev['Temperature'] > 50 ? 'text-warning' : 'text-success'}">${dev['Temperature']}</td>
|
|
<td>${parseFloat(dev['MHS 5s'] || 0).toFixed(2)}</td>
|
|
<td>${parseFloat(dev['MHS av'] || 0).toFixed(2)}</td>
|
|
<td>${dev['Accepted']}</td>
|
|
<td>${dev['Rejected']}</td>
|
|
<td>${dev['Hardware Errors']}</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
} else {
|
|
tbody.innerHTML = '<tr><td colspan="10" class="text-center text-muted">Keine Geräte gefunden...</td></tr>';
|
|
}
|
|
|
|
// Update Pools
|
|
const poolList = document.getElementById('pool-list');
|
|
poolList.innerHTML = '';
|
|
if (pools && pools.length > 0) {
|
|
pools.forEach(pool => {
|
|
const div = document.createElement('div');
|
|
div.className = 'list-group-item bg-dark text-white border-secondary d-flex justify-content-between align-items-center';
|
|
div.innerHTML = `
|
|
<div>
|
|
<div class="fw-bold">Pool ${pool['POOL']}</div>
|
|
<small class="text-muted">${pool['URL']}</small>
|
|
</div>
|
|
<span class="badge ${pool['Status'] === 'Alive' ? 'bg-success' : 'bg-danger'} rounded-pill">${pool['Status']}</span>
|
|
`;
|
|
poolList.appendChild(div);
|
|
});
|
|
} else {
|
|
poolList.innerHTML = '<div class="text-center text-muted">Keine Pools konfiguriert...</div>';
|
|
}
|
|
|
|
})
|
|
.catch(err => console.error('Error fetching data:', err));
|
|
}
|
|
|
|
// Update Log
|
|
function updateLog() {
|
|
fetch('/api/log')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const logDiv = document.getElementById('miner-log');
|
|
if (data.log) {
|
|
const isScrolledToBottom = logDiv.scrollHeight - logDiv.clientHeight <= logDiv.scrollTop + 1;
|
|
logDiv.innerText = data.log;
|
|
if (isScrolledToBottom) {
|
|
logDiv.scrollTop = logDiv.scrollHeight;
|
|
}
|
|
}
|
|
})
|
|
.catch(err => console.error('Error fetching log:', err));
|
|
}
|
|
|
|
// Update every 3 seconds
|
|
setInterval(updateDashboard, 3000);
|
|
setInterval(updateLog, 5000);
|
|
updateDashboard(); // Initial call
|
|
updateLog();
|
|
});
|