asdsad
All checks were successful
Deploy / deploy-staging (push) Successful in 45s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-06-09 01:22:55 +02:00
parent 84e76bff6c
commit 94c985b118
13 changed files with 1213 additions and 4 deletions

View File

@@ -0,0 +1,258 @@
:root {
color-scheme: light;
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at top left, rgba(253, 224, 71, 0.16), transparent 26%),
linear-gradient(180deg, #f7fafc 0%, #edf2f7 100%);
color: #0f172a;
}
.mining-app {
display: grid;
gap: 20px;
min-height: 100vh;
padding: 20px;
}
.mining-hero,
.mining-panel {
border: 1px solid rgba(148, 163, 184, 0.24);
border-radius: 24px;
background: rgba(255, 255, 255, 0.88);
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.08);
backdrop-filter: blur(14px);
}
.mining-hero {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 20px;
padding: 24px;
}
.mining-kicker {
margin: 0 0 10px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.16em;
text-transform: uppercase;
color: #92400e;
}
.mining-hero h1,
.panel-header h2 {
margin: 0;
}
.mining-copy,
.panel-header p {
margin: 8px 0 0;
color: #475569;
line-height: 1.5;
}
.mining-hero-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: flex-end;
}
.preset-button,
.primary-button,
.secondary-button {
border: 0;
border-radius: 999px;
padding: 11px 16px;
font: inherit;
font-weight: 700;
cursor: pointer;
}
.preset-button,
.secondary-button {
background: #e2e8f0;
color: #0f172a;
}
.primary-button {
background: linear-gradient(180deg, #facc15, #f59e0b);
color: #111827;
}
.mining-layout {
display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(320px, 0.9fr);
gap: 20px;
}
.mining-form,
.mining-sidebar {
display: grid;
gap: 20px;
}
.mining-panel {
padding: 22px;
}
.panel-header {
margin-bottom: 18px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
.form-grid label,
.stat-card,
.history-item {
display: grid;
gap: 8px;
}
.form-grid span {
font-size: 13px;
font-weight: 700;
color: #334155;
}
.form-grid input,
.form-grid select,
.form-grid textarea {
width: 100%;
border: 1px solid #cbd5e1;
border-radius: 14px;
padding: 12px 14px;
font: inherit;
background: #fff;
color: #0f172a;
}
.form-grid textarea {
resize: vertical;
}
.form-span-2 {
grid-column: 1 / -1;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 18px;
flex-wrap: wrap;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
}
.stat-card {
padding: 16px;
border-radius: 18px;
background: #f8fafc;
border: 1px solid rgba(148, 163, 184, 0.18);
}
.stat-label,
.stat-meta {
color: #64748b;
font-size: 13px;
}
.stat-value {
font-size: 22px;
line-height: 1.2;
}
.history-list {
display: grid;
gap: 12px;
}
.history-empty {
padding: 18px;
border-radius: 18px;
background: #f8fafc;
color: #64748b;
}
.history-item {
padding: 16px;
border-radius: 18px;
background: #f8fafc;
border: 1px solid rgba(148, 163, 184, 0.16);
}
.history-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.history-title {
font-weight: 700;
}
.history-note {
margin: 0;
color: #475569;
line-height: 1.45;
}
.history-meta {
display: flex;
gap: 12px;
flex-wrap: wrap;
color: #64748b;
font-size: 13px;
}
.toast {
position: fixed;
right: 18px;
bottom: 18px;
z-index: 30;
padding: 12px 16px;
border-radius: 14px;
background: rgba(15, 23, 42, 0.92);
color: #fff;
box-shadow: 0 18px 40px rgba(15, 23, 42, 0.18);
}
@media (max-width: 1100px) {
.mining-layout {
grid-template-columns: 1fr;
}
}
@media (max-width: 760px) {
.mining-app {
padding: 14px;
}
.mining-hero {
flex-direction: column;
}
.form-grid,
.stats-grid {
grid-template-columns: 1fr;
}
}

View 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');
});
}());