asdsad
This commit is contained in:
@@ -8,6 +8,12 @@
|
||||
|
||||
<#elseif section = "form">
|
||||
<section class="kb-login-shell">
|
||||
<div class="kb-login-curtains" aria-hidden="true">
|
||||
<span class="kb-login-curtain kb-login-curtain-top kb-login-curtain-top-a"></span>
|
||||
<span class="kb-login-curtain kb-login-curtain-top kb-login-curtain-top-b"></span>
|
||||
<span class="kb-login-curtain kb-login-curtain-bottom kb-login-curtain-bottom-a"></span>
|
||||
<span class="kb-login-curtain kb-login-curtain-bottom kb-login-curtain-bottom-b"></span>
|
||||
</div>
|
||||
<div class="kb-login-ambient" aria-hidden="true"></div>
|
||||
|
||||
<div class="kb-login-panel">
|
||||
|
||||
@@ -4,6 +4,7 @@ body {
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(255, 221, 177, 0.30), transparent 26%),
|
||||
radial-gradient(circle at 80% 18%, rgba(132, 216, 166, 0.18), transparent 20%),
|
||||
@@ -42,12 +43,62 @@ body {
|
||||
|
||||
.kb-login-shell {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: calc(100vh - 80px);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.kb-login-curtains {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.kb-login-curtain {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
width: 180vmax;
|
||||
height: 180vmax;
|
||||
opacity: 0.72;
|
||||
border-radius: 999px;
|
||||
transform-origin: 0 50%;
|
||||
animation-duration: 1.1s;
|
||||
animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.kb-login-curtain-top,
|
||||
.kb-login-curtain-bottom {
|
||||
top: 50%;
|
||||
margin-top: -90vmax;
|
||||
}
|
||||
|
||||
.kb-login-curtain-top-a {
|
||||
background: linear-gradient(135deg, rgba(236, 101, 105, 0.84), rgba(238, 175, 129, 0.76));
|
||||
animation-name: kbLoginCurtainTopA;
|
||||
}
|
||||
|
||||
.kb-login-curtain-top-b {
|
||||
background: linear-gradient(135deg, rgba(255, 196, 104, 0.66), rgba(255, 138, 76, 0.6));
|
||||
animation-name: kbLoginCurtainTopB;
|
||||
animation-delay: 0.08s;
|
||||
}
|
||||
|
||||
.kb-login-curtain-bottom-a {
|
||||
background: linear-gradient(135deg, rgba(96, 184, 212, 0.7), rgba(55, 69, 181, 0.82));
|
||||
animation-name: kbLoginCurtainBottomA;
|
||||
animation-delay: 0.12s;
|
||||
}
|
||||
|
||||
.kb-login-curtain-bottom-b {
|
||||
background: linear-gradient(135deg, rgba(72, 125, 255, 0.5), rgba(36, 74, 179, 0.76));
|
||||
animation-name: kbLoginCurtainBottomB;
|
||||
animation-delay: 0.18s;
|
||||
}
|
||||
|
||||
.kb-login-ambient {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
@@ -70,6 +121,17 @@ body {
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
box-shadow: 0 24px 80px rgba(2, 6, 23, 0.42);
|
||||
backdrop-filter: blur(24px);
|
||||
opacity: 0;
|
||||
transform: translateY(28px) scale(0.98);
|
||||
animation: kbLoginPanelReveal 0.72s cubic-bezier(0.22, 1, 0.36, 1) 0.35s forwards;
|
||||
transition: transform 180ms ease, box-shadow 180ms ease, border-color 180ms ease;
|
||||
}
|
||||
|
||||
.kb-login-panel:hover,
|
||||
.kb-login-panel:focus-within {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 30px 90px rgba(2, 6, 23, 0.5);
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.kb-login-copy {
|
||||
@@ -129,10 +191,12 @@ body {
|
||||
color: #082032 !important;
|
||||
font-weight: 700;
|
||||
box-shadow: none !important;
|
||||
transition: transform 160ms ease, filter 160ms ease, background-color 160ms ease, border-color 160ms ease;
|
||||
}
|
||||
|
||||
#kc-form-login .pf-v5-c-button:hover,
|
||||
#kc-login:hover {
|
||||
transform: translateY(-1px);
|
||||
filter: brightness(1.03);
|
||||
}
|
||||
|
||||
@@ -162,6 +226,70 @@ body {
|
||||
border-radius: 14px !important;
|
||||
}
|
||||
|
||||
@keyframes kbLoginPanelReveal {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(28px) scale(0.98);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes kbLoginCurtainTopA {
|
||||
from {
|
||||
transform: rotate(46deg) translateX(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(46deg) translateX(22vmax);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes kbLoginCurtainTopB {
|
||||
from {
|
||||
transform: rotate(136deg) translateX(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(136deg) translateX(18vmax);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes kbLoginCurtainBottomA {
|
||||
from {
|
||||
transform: rotate(-44deg) translateX(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(-44deg) translateX(22vmax);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes kbLoginCurtainBottomB {
|
||||
from {
|
||||
transform: rotate(-134deg) translateX(0);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(-134deg) translateX(18vmax);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.kb-login-curtain,
|
||||
.kb-login-panel {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.kb-login-panel {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
#kc-content-wrapper {
|
||||
padding-top: 20px;
|
||||
|
||||
@@ -68,4 +68,18 @@ return [
|
||||
'module_name' => 'auth',
|
||||
'summary' => 'Uebergabepunkt fuer das spaetere Keycloak-Theme im Desktop-Look.',
|
||||
],
|
||||
[
|
||||
'app_id' => 'mining-checker',
|
||||
'title' => 'Mining-Checker',
|
||||
'icon' => 'MC',
|
||||
'entry_route' => '/apps/mining-checker',
|
||||
'window_mode' => 'window',
|
||||
'content_mode' => 'iframe',
|
||||
'default_width' => 1180,
|
||||
'default_height' => 760,
|
||||
'supports_widget' => false,
|
||||
'supports_tray' => true,
|
||||
'module_name' => 'finance',
|
||||
'summary' => 'Vollstaendiger Rechner fuer Mining-Profitabilitaet, Stromkosten, ROI und Snapshot-Historie.',
|
||||
],
|
||||
];
|
||||
|
||||
110
public/api/mining-checker/index.php
Normal file
110
public/api/mining-checker/index.php
Normal file
@@ -0,0 +1,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
|
||||
|
||||
use App\ConfigLoader;
|
||||
use App\KeycloakAuth;
|
||||
use MiningChecker\MiningCheckerCalculator;
|
||||
use MiningChecker\MiningCheckerStore;
|
||||
use MiningChecker\MiningCheckerUserScope;
|
||||
|
||||
session_start();
|
||||
|
||||
$projectRoot = dirname(__DIR__, 3);
|
||||
$keycloakConfig = ConfigLoader::load($projectRoot, 'keycloak');
|
||||
$auth = new KeycloakAuth($keycloakConfig);
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if (!$auth->shouldShowDesktop()) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['error' => 'Nicht autorisiert.'], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
$store = new MiningCheckerStore($projectRoot, MiningCheckerUserScope::current());
|
||||
$calculator = new MiningCheckerCalculator();
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||
|
||||
try {
|
||||
if ($method === 'GET') {
|
||||
$state = $store->load();
|
||||
$state['metrics'] = $calculator->calculate((array) ($state['settings'] ?? []));
|
||||
echo json_encode($state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
$payloadRaw = file_get_contents('php://input');
|
||||
$payload = json_decode($payloadRaw !== false ? $payloadRaw : '', true);
|
||||
$payload = is_array($payload) ? $payload : [];
|
||||
|
||||
if ($method === 'PUT') {
|
||||
$state = $store->load();
|
||||
$state['settings'] = normalizeSettings(array_merge((array) ($state['settings'] ?? []), $payload));
|
||||
$store->save($state);
|
||||
$state['metrics'] = $calculator->calculate((array) $state['settings']);
|
||||
echo json_encode($state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($method === 'POST') {
|
||||
$state = $store->load();
|
||||
$settings = normalizeSettings(array_merge((array) ($state['settings'] ?? []), is_array($payload['settings'] ?? null) ? $payload['settings'] : []));
|
||||
$metrics = $calculator->calculate($settings);
|
||||
$note = trim((string) ($payload['note'] ?? ''));
|
||||
$history = is_array($state['history'] ?? null) ? $state['history'] : [];
|
||||
|
||||
array_unshift($history, [
|
||||
'id' => bin2hex(random_bytes(8)),
|
||||
'created_at' => gmdate('c'),
|
||||
'project_name' => (string) ($settings['project_name'] ?? ''),
|
||||
'coin_symbol' => (string) ($settings['coin_symbol'] ?? ''),
|
||||
'currency' => (string) ($settings['currency'] ?? 'EUR'),
|
||||
'note' => $note,
|
||||
'settings' => $settings,
|
||||
'metrics' => $metrics,
|
||||
]);
|
||||
|
||||
$state['settings'] = $settings;
|
||||
$state['history'] = array_slice($history, 0, 25);
|
||||
$store->save($state);
|
||||
|
||||
$state['metrics'] = $metrics;
|
||||
echo json_encode($state, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
http_response_code(405);
|
||||
echo json_encode(['error' => 'Methode nicht erlaubt.'], JSON_UNESCAPED_UNICODE);
|
||||
} catch (Throwable $throwable) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => $throwable->getMessage()], JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $settings
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function normalizeSettings(array $settings): array
|
||||
{
|
||||
return [
|
||||
'project_name' => trim((string) ($settings['project_name'] ?? 'DOGE Mining')),
|
||||
'coin_symbol' => strtoupper(trim((string) ($settings['coin_symbol'] ?? 'DOGE'))),
|
||||
'algorithm' => trim((string) ($settings['algorithm'] ?? 'Scrypt')),
|
||||
'currency' => strtoupper(trim((string) ($settings['currency'] ?? 'EUR'))),
|
||||
'hashrate' => (float) ($settings['hashrate'] ?? 0),
|
||||
'hashrate_unit' => trim((string) ($settings['hashrate_unit'] ?? 'MH/s')),
|
||||
'network_hashrate' => (float) ($settings['network_hashrate'] ?? 0),
|
||||
'network_hashrate_unit' => trim((string) ($settings['network_hashrate_unit'] ?? 'TH/s')),
|
||||
'block_reward' => (float) ($settings['block_reward'] ?? 0),
|
||||
'block_time_seconds' => (float) ($settings['block_time_seconds'] ?? 60),
|
||||
'coin_price' => (float) ($settings['coin_price'] ?? 0),
|
||||
'power_watts' => (float) ($settings['power_watts'] ?? 0),
|
||||
'electricity_price' => (float) ($settings['electricity_price'] ?? 0),
|
||||
'pool_fee_percent' => (float) ($settings['pool_fee_percent'] ?? 0),
|
||||
'hardware_cost' => (float) ($settings['hardware_cost'] ?? 0),
|
||||
'notes' => trim((string) ($settings['notes'] ?? '')),
|
||||
];
|
||||
}
|
||||
164
public/apps/mining-checker/index.php
Normal file
164
public/apps/mining-checker/index.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(__DIR__, 3) . '/src/App/bootstrap.php';
|
||||
|
||||
use App\ConfigLoader;
|
||||
use App\KeycloakAuth;
|
||||
|
||||
session_start();
|
||||
|
||||
$projectRoot = dirname(__DIR__, 3);
|
||||
$keycloakConfig = ConfigLoader::load($projectRoot, 'keycloak');
|
||||
$auth = new KeycloakAuth($keycloakConfig);
|
||||
|
||||
if (!$auth->shouldShowDesktop()) {
|
||||
header('Location: /auth/login', true, 302);
|
||||
exit;
|
||||
}
|
||||
?><!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Mining-Checker</title>
|
||||
<link rel="stylesheet" href="/assets/apps/mining-checker/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<main class="mining-app" id="mining-checker-app">
|
||||
<section class="mining-hero">
|
||||
<div>
|
||||
<p class="mining-kicker">App Modul</p>
|
||||
<h1>Mining-Checker</h1>
|
||||
<p class="mining-copy">Profitabilitaet, Stromkosten, ROI und Messhistorie fuer dein Mining-Setup.</p>
|
||||
</div>
|
||||
<div class="mining-hero-actions">
|
||||
<button type="button" class="preset-button" data-preset="doge">DOGE Basis</button>
|
||||
<button type="button" class="preset-button" data-preset="ltc">LTC Basis</button>
|
||||
<button type="button" class="preset-button" data-preset="custom">Aktuelle Werte behalten</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mining-layout">
|
||||
<form class="mining-panel mining-form" id="mining-settings-form">
|
||||
<div class="panel-header">
|
||||
<h2>Setup</h2>
|
||||
<p>Alle Werte werden pro Benutzer gespeichert.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-grid">
|
||||
<label>
|
||||
<span>Projektname</span>
|
||||
<input name="project_name" type="text" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Coin Symbol</span>
|
||||
<input name="coin_symbol" type="text" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Algorithmus</span>
|
||||
<input name="algorithm" type="text" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Waehrung</span>
|
||||
<input name="currency" type="text" maxlength="3" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Hashrate</span>
|
||||
<input name="hashrate" type="number" min="0" step="0.01" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Hashrate Einheit</span>
|
||||
<select name="hashrate_unit">
|
||||
<option>KH/s</option>
|
||||
<option selected>MH/s</option>
|
||||
<option>GH/s</option>
|
||||
<option>TH/s</option>
|
||||
<option>PH/s</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Netzwerk Hashrate</span>
|
||||
<input name="network_hashrate" type="number" min="0" step="0.01" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Netzwerk Einheit</span>
|
||||
<select name="network_hashrate_unit">
|
||||
<option>GH/s</option>
|
||||
<option>TH/s</option>
|
||||
<option selected>PH/s</option>
|
||||
<option>EH/s</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Block Reward</span>
|
||||
<input name="block_reward" type="number" min="0" step="0.00000001" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Blockzeit in Sekunden</span>
|
||||
<input name="block_time_seconds" type="number" min="1" step="1" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Coin Preis</span>
|
||||
<input name="coin_price" type="number" min="0" step="0.00000001" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Leistungsaufnahme in Watt</span>
|
||||
<input name="power_watts" type="number" min="0" step="1" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Strompreis pro kWh</span>
|
||||
<input name="electricity_price" type="number" min="0" step="0.0001" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Pool Fee in %</span>
|
||||
<input name="pool_fee_percent" type="number" min="0" max="100" step="0.01" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Hardwarekosten</span>
|
||||
<input name="hardware_cost" type="number" min="0" step="0.01" required>
|
||||
</label>
|
||||
<label class="form-span-2">
|
||||
<span>Notizen</span>
|
||||
<textarea name="notes" rows="4" placeholder="Optional: Modell, Pool, Standort, Temperatur, Firmware ..."></textarea>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="primary-button">Einstellungen speichern</button>
|
||||
<button type="button" class="secondary-button" id="snapshot-button">Snapshot speichern</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mining-sidebar">
|
||||
<section class="mining-panel">
|
||||
<div class="panel-header">
|
||||
<h2>Kennzahlen</h2>
|
||||
<p>Direkt aus den aktuellen Werten berechnet.</p>
|
||||
</div>
|
||||
<div class="stats-grid" id="stats-grid"></div>
|
||||
</section>
|
||||
|
||||
<section class="mining-panel">
|
||||
<div class="panel-header">
|
||||
<h2>Snapshot Verlauf</h2>
|
||||
<p>Gespeicherte Stichtage pro Benutzer.</p>
|
||||
</div>
|
||||
<div class="history-list" id="history-list"></div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<template id="stat-card-template">
|
||||
<article class="stat-card">
|
||||
<span class="stat-label"></span>
|
||||
<strong class="stat-value"></strong>
|
||||
<small class="stat-meta"></small>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script src="/assets/apps/mining-checker/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
258
public/assets/apps/mining-checker/app.css
Normal file
258
public/assets/apps/mining-checker/app.css
Normal 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;
|
||||
}
|
||||
}
|
||||
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');
|
||||
});
|
||||
}());
|
||||
@@ -388,6 +388,20 @@ h1 {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.window-content--embedded {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.window-app-frame {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.taskbar {
|
||||
position: fixed;
|
||||
left: var(--taskbar-left);
|
||||
|
||||
@@ -107,6 +107,26 @@ if (payloadNode) {
|
||||
`;
|
||||
};
|
||||
|
||||
const buildWindowBody = (definition) => {
|
||||
if (definition.content_mode === 'iframe' && typeof definition.entry_route === 'string' && definition.entry_route !== '') {
|
||||
const title = escapeHtml(definition.title);
|
||||
const src = escapeHtml(definition.entry_route);
|
||||
|
||||
return `
|
||||
<div class="window-content window-content--embedded">
|
||||
<iframe class="window-app-frame" src="${src}" title="${title}" loading="lazy"></iframe>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="window-content">
|
||||
<p>${escapeHtml(definition.content)}</p>
|
||||
<p><strong>Route:</strong> ${escapeHtml(definition.app_id)}</p>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const loadWindowState = () => {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(storageKey);
|
||||
@@ -463,10 +483,7 @@ if (payloadNode) {
|
||||
node.dataset.windowControls = windowControlsMode;
|
||||
node.innerHTML = `
|
||||
${buildWindowHeader(definition)}
|
||||
<div class="window-content">
|
||||
<p>${escapeHtml(definition.content)}</p>
|
||||
<p><strong>Route:</strong> ${escapeHtml(definition.app_id)}</p>
|
||||
</div>
|
||||
${buildWindowBody(definition)}
|
||||
<div class="window-resize-handle" data-edge="top" aria-hidden="true"></div>
|
||||
<div class="window-resize-handle" data-edge="right" aria-hidden="true"></div>
|
||||
<div class="window-resize-handle" data-edge="bottom" aria-hidden="true"></div>
|
||||
@@ -564,6 +581,8 @@ if (payloadNode) {
|
||||
width: app.default_width || 640,
|
||||
height: app.default_height || 420,
|
||||
content: app.summary || '',
|
||||
entry_route: app.entry_route || '',
|
||||
content_mode: app.content_mode || 'summary',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ spl_autoload_register(static function (string $class): void {
|
||||
$prefixes = [
|
||||
'App\\' => __DIR__ . '/',
|
||||
'Desktop\\' => dirname(__DIR__) . '/Desktop/',
|
||||
'MiningChecker\\' => dirname(__DIR__) . '/MiningChecker/',
|
||||
];
|
||||
|
||||
foreach ($prefixes as $prefix => $baseDir) {
|
||||
|
||||
84
src/MiningChecker/MiningCheckerCalculator.php
Normal file
84
src/MiningChecker/MiningCheckerCalculator.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MiningChecker;
|
||||
|
||||
final class MiningCheckerCalculator
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $settings
|
||||
* @return array<string, float|null>
|
||||
*/
|
||||
public function calculate(array $settings): array
|
||||
{
|
||||
$hashrate = $this->toHashrateHps(
|
||||
(float) ($settings['hashrate'] ?? 0),
|
||||
(string) ($settings['hashrate_unit'] ?? 'MH/s')
|
||||
);
|
||||
$networkHashrate = $this->toHashrateHps(
|
||||
(float) ($settings['network_hashrate'] ?? 0),
|
||||
(string) ($settings['network_hashrate_unit'] ?? 'TH/s')
|
||||
);
|
||||
|
||||
$powerWatts = max(0.0, (float) ($settings['power_watts'] ?? 0));
|
||||
$electricityPrice = max(0.0, (float) ($settings['electricity_price'] ?? 0));
|
||||
$poolFeePercent = max(0.0, min(100.0, (float) ($settings['pool_fee_percent'] ?? 0)));
|
||||
$coinPrice = max(0.0, (float) ($settings['coin_price'] ?? 0));
|
||||
$blockReward = max(0.0, (float) ($settings['block_reward'] ?? 0));
|
||||
$blockTimeSeconds = max(1.0, (float) ($settings['block_time_seconds'] ?? 60));
|
||||
$hardwareCost = max(0.0, (float) ($settings['hardware_cost'] ?? 0));
|
||||
|
||||
$share = $networkHashrate > 0 ? $hashrate / $networkHashrate : null;
|
||||
$blocksPerDay = 86400 / $blockTimeSeconds;
|
||||
$dailyCoinsGross = $share !== null ? $share * $blocksPerDay * $blockReward : null;
|
||||
$dailyCoinsNet = $dailyCoinsGross !== null ? $dailyCoinsGross * (1 - ($poolFeePercent / 100)) : null;
|
||||
$dailyRevenue = $dailyCoinsNet !== null ? $dailyCoinsNet * $coinPrice : null;
|
||||
$dailyPowerCost = ($powerWatts / 1000) * 24 * $electricityPrice;
|
||||
$dailyProfit = $dailyRevenue !== null ? $dailyRevenue - $dailyPowerCost : null;
|
||||
$monthlyProfit = $dailyProfit !== null ? $dailyProfit * 30 : null;
|
||||
$annualProfit = $dailyProfit !== null ? $dailyProfit * 365 : null;
|
||||
$breakEvenDays = ($dailyProfit !== null && $dailyProfit > 0.0 && $hardwareCost > 0.0)
|
||||
? $hardwareCost / $dailyProfit
|
||||
: null;
|
||||
$efficiency = $hashrate > 0 ? $powerWatts / ($hashrate / 1000000) : null;
|
||||
|
||||
return [
|
||||
'hashrate_hps' => $hashrate > 0 ? $hashrate : null,
|
||||
'network_hashrate_hps' => $networkHashrate > 0 ? $networkHashrate : null,
|
||||
'share_ratio' => $share,
|
||||
'blocks_per_day' => $blocksPerDay,
|
||||
'daily_coins_gross' => $dailyCoinsGross,
|
||||
'daily_coins_net' => $dailyCoinsNet,
|
||||
'daily_revenue' => $dailyRevenue,
|
||||
'daily_power_cost' => $dailyPowerCost,
|
||||
'daily_profit' => $dailyProfit,
|
||||
'monthly_profit' => $monthlyProfit,
|
||||
'annual_profit' => $annualProfit,
|
||||
'break_even_days' => $breakEvenDays,
|
||||
'efficiency_w_per_mh' => $efficiency,
|
||||
];
|
||||
}
|
||||
|
||||
private function toHashrateHps(float $value, string $unit): float
|
||||
{
|
||||
if ($value <= 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$normalized = strtoupper(trim($unit));
|
||||
$multipliers = [
|
||||
'H/S' => 1,
|
||||
'KH/S' => 1000,
|
||||
'MH/S' => 1000000,
|
||||
'GH/S' => 1000000000,
|
||||
'TH/S' => 1000000000000,
|
||||
'PH/S' => 1000000000000000,
|
||||
'EH/S' => 1000000000000000000,
|
||||
];
|
||||
|
||||
$multiplier = $multipliers[$normalized] ?? 1000000;
|
||||
|
||||
return $value * $multiplier;
|
||||
}
|
||||
}
|
||||
92
src/MiningChecker/MiningCheckerStore.php
Normal file
92
src/MiningChecker/MiningCheckerStore.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MiningChecker;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class MiningCheckerStore
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $projectRoot,
|
||||
private readonly string $userScope,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function load(): array
|
||||
{
|
||||
$path = $this->path();
|
||||
|
||||
if (!is_file($path)) {
|
||||
return $this->defaultState();
|
||||
}
|
||||
|
||||
$raw = file_get_contents($path);
|
||||
if ($raw === false || trim($raw) === '') {
|
||||
return $this->defaultState();
|
||||
}
|
||||
|
||||
$data = json_decode($raw, true);
|
||||
|
||||
return is_array($data) ? array_replace_recursive($this->defaultState(), $data) : $this->defaultState();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $state
|
||||
*/
|
||||
public function save(array $state): void
|
||||
{
|
||||
$directory = dirname($this->path());
|
||||
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
|
||||
throw new RuntimeException('Mining-Checker-Verzeichnis konnte nicht erstellt werden.');
|
||||
}
|
||||
|
||||
$json = json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if ($json === false) {
|
||||
throw new RuntimeException('Mining-Checker-Daten konnten nicht serialisiert werden.');
|
||||
}
|
||||
|
||||
if (file_put_contents($this->path(), $json . PHP_EOL, LOCK_EX) === false) {
|
||||
throw new RuntimeException('Mining-Checker-Daten konnten nicht gespeichert werden.');
|
||||
}
|
||||
}
|
||||
|
||||
private function path(): string
|
||||
{
|
||||
$safeScope = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $this->userScope) ?: 'guest';
|
||||
|
||||
return $this->projectRoot . '/data/mining-checker/' . $safeScope . '.json';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function defaultState(): array
|
||||
{
|
||||
return [
|
||||
'settings' => [
|
||||
'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,
|
||||
'notes' => '',
|
||||
],
|
||||
'history' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
21
src/MiningChecker/MiningCheckerUserScope.php
Normal file
21
src/MiningChecker/MiningCheckerUserScope.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MiningChecker;
|
||||
|
||||
final class MiningCheckerUserScope
|
||||
{
|
||||
public static function current(): string
|
||||
{
|
||||
$auth = $_SESSION['desktop_auth'] ?? null;
|
||||
|
||||
if (!is_array($auth)) {
|
||||
return 'guest';
|
||||
}
|
||||
|
||||
$user = is_array($auth['user'] ?? null) ? $auth['user'] : [];
|
||||
|
||||
return (string) ($user['sub'] ?? $user['username'] ?? 'guest');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user