asdsad
This commit is contained in:
298
public/assets/apps/mining-checker/app.js
Normal file
298
public/assets/apps/mining-checker/app.js
Normal file
@@ -0,0 +1,298 @@
|
||||
(function () {
|
||||
const form = document.getElementById('mining-settings-form');
|
||||
const statsGrid = document.getElementById('stats-grid');
|
||||
const historyList = document.getElementById('history-list');
|
||||
const snapshotButton = document.getElementById('snapshot-button');
|
||||
const statTemplate = document.getElementById('stat-card-template');
|
||||
const presetButtons = Array.from(document.querySelectorAll('[data-preset]'));
|
||||
|
||||
if (!form || !statsGrid || !historyList || !snapshotButton || !statTemplate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const presets = {
|
||||
doge: {
|
||||
project_name: 'DOGE Mining',
|
||||
coin_symbol: 'DOGE',
|
||||
algorithm: 'Scrypt',
|
||||
currency: 'EUR',
|
||||
hashrate: 950,
|
||||
hashrate_unit: 'MH/s',
|
||||
network_hashrate: 920,
|
||||
network_hashrate_unit: 'TH/s',
|
||||
block_reward: 10000,
|
||||
block_time_seconds: 60,
|
||||
coin_price: 0.14,
|
||||
power_watts: 3420,
|
||||
electricity_price: 0.32,
|
||||
pool_fee_percent: 1.5,
|
||||
hardware_cost: 8400,
|
||||
},
|
||||
ltc: {
|
||||
project_name: 'LTC Mining',
|
||||
coin_symbol: 'LTC',
|
||||
algorithm: 'Scrypt',
|
||||
currency: 'EUR',
|
||||
hashrate: 950,
|
||||
hashrate_unit: 'MH/s',
|
||||
network_hashrate: 1.35,
|
||||
network_hashrate_unit: 'PH/s',
|
||||
block_reward: 6.25,
|
||||
block_time_seconds: 150,
|
||||
coin_price: 82,
|
||||
power_watts: 3420,
|
||||
electricity_price: 0.32,
|
||||
pool_fee_percent: 1.5,
|
||||
hardware_cost: 8400,
|
||||
},
|
||||
};
|
||||
|
||||
const fmtNumber = (value, digits = 4) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
return Number(value).toLocaleString('de-DE', {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: digits,
|
||||
});
|
||||
};
|
||||
|
||||
const fmtMoney = (value, currency) => {
|
||||
if (value === null || value === undefined || currency === '') {
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
return new Intl.NumberFormat('de-DE', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
maximumFractionDigits: 4,
|
||||
}).format(Number(value));
|
||||
};
|
||||
|
||||
const fmtDateTime = (value) => {
|
||||
if (!value) {
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat('de-DE', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short',
|
||||
}).format(date);
|
||||
};
|
||||
|
||||
const toHashrateHps = (value, unit) => {
|
||||
const numericValue = Number(value || 0);
|
||||
if (!Number.isFinite(numericValue) || numericValue <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const multipliers = {
|
||||
'H/S': 1,
|
||||
'KH/S': 1e3,
|
||||
'MH/S': 1e6,
|
||||
'GH/S': 1e9,
|
||||
'TH/S': 1e12,
|
||||
'PH/S': 1e15,
|
||||
'EH/S': 1e18,
|
||||
};
|
||||
|
||||
return numericValue * (multipliers[String(unit || 'MH/s').toUpperCase()] || 1e6);
|
||||
};
|
||||
|
||||
const calculateMetrics = (settings) => {
|
||||
const hashrate = toHashrateHps(settings.hashrate, settings.hashrate_unit);
|
||||
const networkHashrate = toHashrateHps(settings.network_hashrate, settings.network_hashrate_unit);
|
||||
const powerWatts = Math.max(0, Number(settings.power_watts || 0));
|
||||
const electricityPrice = Math.max(0, Number(settings.electricity_price || 0));
|
||||
const poolFeePercent = Math.max(0, Math.min(100, Number(settings.pool_fee_percent || 0)));
|
||||
const coinPrice = Math.max(0, Number(settings.coin_price || 0));
|
||||
const blockReward = Math.max(0, Number(settings.block_reward || 0));
|
||||
const blockTimeSeconds = Math.max(1, Number(settings.block_time_seconds || 60));
|
||||
const hardwareCost = Math.max(0, Number(settings.hardware_cost || 0));
|
||||
|
||||
const share = networkHashrate > 0 ? hashrate / networkHashrate : null;
|
||||
const blocksPerDay = 86400 / blockTimeSeconds;
|
||||
const dailyCoinsGross = share !== null ? share * blocksPerDay * blockReward : null;
|
||||
const dailyCoinsNet = dailyCoinsGross !== null ? dailyCoinsGross * (1 - poolFeePercent / 100) : null;
|
||||
const dailyRevenue = dailyCoinsNet !== null ? dailyCoinsNet * coinPrice : null;
|
||||
const dailyPowerCost = (powerWatts / 1000) * 24 * electricityPrice;
|
||||
const dailyProfit = dailyRevenue !== null ? dailyRevenue - dailyPowerCost : null;
|
||||
const monthlyProfit = dailyProfit !== null ? dailyProfit * 30 : null;
|
||||
const annualProfit = dailyProfit !== null ? dailyProfit * 365 : null;
|
||||
const breakEvenDays = dailyProfit !== null && dailyProfit > 0 && hardwareCost > 0 ? hardwareCost / dailyProfit : null;
|
||||
|
||||
return {
|
||||
daily_coins_net: dailyCoinsNet,
|
||||
daily_revenue: dailyRevenue,
|
||||
daily_power_cost: dailyPowerCost,
|
||||
daily_profit: dailyProfit,
|
||||
monthly_profit: monthlyProfit,
|
||||
annual_profit: annualProfit,
|
||||
break_even_days: breakEvenDays,
|
||||
share_ratio: share,
|
||||
};
|
||||
};
|
||||
|
||||
const showToast = (message) => {
|
||||
const node = document.createElement('div');
|
||||
node.className = 'toast';
|
||||
node.textContent = message;
|
||||
document.body.appendChild(node);
|
||||
|
||||
window.setTimeout(() => {
|
||||
node.remove();
|
||||
}, 2400);
|
||||
};
|
||||
|
||||
const readForm = () => Object.fromEntries(new FormData(form).entries());
|
||||
|
||||
const writeForm = (settings) => {
|
||||
Object.entries(settings).forEach(([key, value]) => {
|
||||
const field = form.elements.namedItem(key);
|
||||
if (!(field instanceof HTMLInputElement || field instanceof HTMLSelectElement || field instanceof HTMLTextAreaElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
field.value = String(value ?? '');
|
||||
});
|
||||
};
|
||||
|
||||
const renderStats = (settings, metrics) => {
|
||||
const currency = String(settings.currency || 'EUR').toUpperCase();
|
||||
const cards = [
|
||||
['Taegliche Coins netto', fmtNumber(metrics.daily_coins_net, 6), `${settings.coin_symbol || 'COIN'} pro Tag`],
|
||||
['Taeglicher Umsatz', fmtMoney(metrics.daily_revenue, currency), 'vor Stromkosten'],
|
||||
['Taeglicher Strom', fmtMoney(metrics.daily_power_cost, currency), `${fmtNumber(settings.power_watts, 0)} W @ ${fmtMoney(settings.electricity_price, currency)}/kWh`],
|
||||
['Taeglicher Profit', fmtMoney(metrics.daily_profit, currency), 'nach Pool Fee und Strom'],
|
||||
['Monatlicher Profit', fmtMoney(metrics.monthly_profit, currency), '30 Tage Hochrechnung'],
|
||||
['Jaehrlicher Profit', fmtMoney(metrics.annual_profit, currency), '365 Tage Hochrechnung'],
|
||||
['ROI / Break-even', metrics.break_even_days ? `${fmtNumber(metrics.break_even_days, 1)} Tage` : 'n/a', 'bezogen auf Hardwarekosten'],
|
||||
['Rig Anteil am Netzwerk', metrics.share_ratio ? `${fmtNumber(metrics.share_ratio * 100, 8)} %` : 'n/a', 'Hashrate Share'],
|
||||
];
|
||||
|
||||
statsGrid.innerHTML = '';
|
||||
|
||||
cards.forEach(([label, value, meta]) => {
|
||||
const fragment = statTemplate.content.cloneNode(true);
|
||||
fragment.querySelector('.stat-label').textContent = label;
|
||||
fragment.querySelector('.stat-value').textContent = value;
|
||||
fragment.querySelector('.stat-meta').textContent = meta;
|
||||
statsGrid.appendChild(fragment);
|
||||
});
|
||||
};
|
||||
|
||||
const renderHistory = (entries) => {
|
||||
historyList.innerHTML = '';
|
||||
|
||||
if (!Array.isArray(entries) || entries.length === 0) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'history-empty';
|
||||
empty.textContent = 'Noch keine Snapshots gespeichert.';
|
||||
historyList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
entries.forEach((entry) => {
|
||||
const item = document.createElement('article');
|
||||
item.className = 'history-item';
|
||||
item.innerHTML = `
|
||||
<div class="history-head">
|
||||
<span class="history-title">${entry.project_name || 'Snapshot'}</span>
|
||||
<span>${fmtDateTime(entry.created_at)}</span>
|
||||
</div>
|
||||
<div class="history-meta">
|
||||
<span>${entry.coin_symbol || 'COIN'}</span>
|
||||
<span>Profit/Tag: ${fmtMoney(entry.metrics?.daily_profit, entry.currency || 'EUR')}</span>
|
||||
<span>ROI: ${entry.metrics?.break_even_days ? `${fmtNumber(entry.metrics.break_even_days, 1)} Tage` : 'n/a'}</span>
|
||||
</div>
|
||||
${entry.note ? `<p class="history-note">${entry.note}</p>` : ''}
|
||||
`;
|
||||
historyList.appendChild(item);
|
||||
});
|
||||
};
|
||||
|
||||
const load = async () => {
|
||||
const response = await fetch('/api/mining-checker/');
|
||||
const data = await response.json();
|
||||
writeForm(data.settings || {});
|
||||
renderStats(data.settings || {}, data.metrics || {});
|
||||
renderHistory(data.history || []);
|
||||
};
|
||||
|
||||
const saveSettings = async () => {
|
||||
const response = await fetch('/api/mining-checker/', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(readForm()),
|
||||
});
|
||||
const data = await response.json();
|
||||
writeForm(data.settings || {});
|
||||
renderStats(data.settings || {}, data.metrics || {});
|
||||
renderHistory(data.history || []);
|
||||
showToast('Mining-Checker gespeichert');
|
||||
};
|
||||
|
||||
const saveSnapshot = async () => {
|
||||
const note = window.prompt('Optionaler Snapshot-Hinweis', '');
|
||||
if (note === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('/api/mining-checker/', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
settings: readForm(),
|
||||
note,
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
writeForm(data.settings || {});
|
||||
renderStats(data.settings || {}, data.metrics || {});
|
||||
renderHistory(data.history || []);
|
||||
showToast('Snapshot gespeichert');
|
||||
};
|
||||
|
||||
form.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
await saveSettings();
|
||||
});
|
||||
|
||||
snapshotButton.addEventListener('click', async () => {
|
||||
await saveSnapshot();
|
||||
});
|
||||
|
||||
presetButtons.forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
const preset = button.dataset.preset;
|
||||
if (!preset || preset === 'custom') {
|
||||
showToast('Aktuelle Werte bleiben unveraendert');
|
||||
return;
|
||||
}
|
||||
|
||||
writeForm(presets[preset]);
|
||||
renderStats(readForm(), calculateMetrics(readForm()));
|
||||
showToast(`Preset ${preset.toUpperCase()} geladen`);
|
||||
});
|
||||
});
|
||||
|
||||
form.addEventListener('input', () => {
|
||||
const settings = readForm();
|
||||
renderStats(settings, calculateMetrics(settings));
|
||||
});
|
||||
|
||||
load().catch((error) => {
|
||||
console.error(error);
|
||||
showToast('Mining-Checker konnte nicht geladen werden');
|
||||
});
|
||||
}());
|
||||
Reference in New Issue
Block a user