From 72b7202a8a57c973a1561e9a98829a3a22c057ec Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Sun, 13 Sep 2026 00:01:02 +0200 Subject: [PATCH] Add mining historical recovery assistant --- custom/apps/mining-checker/assets/js/app.js | 126 ++++++++++++++++++ custom/apps/mining-checker/docs/README.md | 2 + custom/apps/mining-checker/src/Api/Router.php | 41 ++++++ docs/CONTENT.md | 1 + 4 files changed, 170 insertions(+) diff --git a/custom/apps/mining-checker/assets/js/app.js b/custom/apps/mining-checker/assets/js/app.js index bc9278d9..e1974243 100644 --- a/custom/apps/mining-checker/assets/js/app.js +++ b/custom/apps/mining-checker/assets/js/app.js @@ -54,6 +54,7 @@ || (document.body && document.body.dataset.moduleDebugEnabled === '1'); const fallbackSections = [ ['overview', 'Übersicht'], + ['recovery', 'Nachtragen'], ['upload', 'Upload'], ['measurements', 'Mining-History'], ['wallet', 'Wallet'], @@ -399,6 +400,9 @@ if (sectionId === 'overview') { return 'Kernkennzahlen, ROI, Break-even und Zielmonitor.'; } + if (sectionId === 'recovery') { + return 'Zusammenhaengende historische Miner-, Transfer-, Wallet- und Mining-Daten nachtragen.'; + } if (sectionId === 'upload') { return 'Neue Mining-Daten per OCR, Import oder manueller Eingabe erfassen.'; } @@ -1066,6 +1070,19 @@ note: '', source: 'manual', }); + const [recoveryForm, setRecoveryForm] = useState({ + miner_at: '2026-09-03T13:18:00', + miner_cost: '277.90', + doge_usd: '0.087812', + usd_eur: '0.86014', + transfer_at: '2026-09-08T02:19:00', + transfer_coins: '78.495864', + wallet_at: '2026-09-12T23:46:05', + wallet_coins: '80.90114043', + mining_at: '2026-09-12T23:46:05', + mining_coins: '81.293679', + mining_price_usd: '0.08472', + }); const [importForm, setImportForm] = useState({ rows_text: '', default_currency: 'USD', @@ -2219,6 +2236,89 @@ } } + async function submitHistoricalRecovery(event) { + event.preventDefault(); + const offer = scenarioMinerOffers.find((item) => String(item.payment_type || '').toLowerCase() === 'crypto' + && Number(item.runtime_months) === 36 + && Math.abs(toKhPerSecond(item.mining_speed_value, item.mining_speed_unit) - 50) < 0.0001); + if (!offer) { + setError('Das Referenzangebot 50 kH/s fuer 36 Monate konnte nicht geladen werden. Bitte Miner-Angebote pruefen und erneut versuchen.'); + return; + } + + setSaving(true); + setError(''); + setMessage(''); + try { + await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/historical-recovery-fx`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + measured_at: recoveryForm.miner_at, + doge_usd: recoveryForm.doge_usd, + usd_eur: recoveryForm.usd_eur, + }), + }); + await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/miner-offers/${encodeURIComponent(offer.id)}/purchase`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + computed_offer: offer, + purchased_at: recoveryForm.miner_at, + label: 'Crypto Server · 50 kH/s · 36 Monate', + mining_speed_value: '50', + mining_speed_unit: 'kH/s', + bonus_percent: '20', + total_cost_amount: recoveryForm.miner_cost, + currency: currentMiningCurrency, + auto_renew: false, + note: 'Historischer Nachtrag: +10 kH/s Bonus; zuvor HSH in DOGE gewandelt.', + }), + }); + await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/payouts`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + payout_at: recoveryForm.transfer_at, + coins_amount: recoveryForm.transfer_coins, + payout_currency: currentMiningCurrency, + note: 'Historischer Nachtrag: Transfer nach HSH-zu-DOGE-Umwandlung.', + }), + }); + await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/wallet-snapshots`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + measured_at: recoveryForm.wallet_at, + wallet_balance: recoveryForm.wallet_coins, + wallet_currency: currentMiningCurrency, + balances_json: { [currentMiningCurrency]: { balance: Number(recoveryForm.wallet_coins) } }, + source: 'manual', + note: 'Historischer Nachtrag: autoritativer Wallet-Bestand.', + }), + }); + await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/measurements`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + measured_at: recoveryForm.mining_at, + coins_total: recoveryForm.mining_coins, + coin_currency: currentMiningCurrency, + price_per_coin: recoveryForm.mining_price_usd, + price_currency: 'USD', + source: 'manual', + note: 'Historischer Nachtrag: CT-Pool-Screenshot.', + }), + }); + await loadBootstrap(projectKey); + setMessage('Historische Daten vollstaendig nachgetragen.'); + } catch (err) { + setError(`Nachtrag wurde nicht vollstaendig gespeichert: ${err.message}`); + } finally { + setSaving(false); + } + } + async function submitWalletSnapshotFromPreview(previewOverride) { const preview = normalizeOcrPreview(previewOverride || ocrPreview); const raw = { @@ -3365,6 +3465,32 @@ ]); } + if (activeTab === 'recovery') { + return h('div', { className: 'mc-stack' }, [ + panel('Historische Daten nachtragen', 'Speichert die bekannten Ereignisse chronologisch: Miner-Miete, Transfer ins Wallet, autoritativer Wallet-Snapshot und Mining-Stand. Vor dem Speichern wird ein historischer FX-Snapshot fuer die Krypto-Miete benoetigt.', h('form', { + className: 'mc-form', + onSubmit: submitHistoricalRecovery, + }, [ + inputField('Miner-Miete', 'datetime-local', recoveryForm.miner_at, (value) => setRecoveryForm({ ...recoveryForm, miner_at: value }), '1'), + inputField('Miner-Preis DOGE', 'number', recoveryForm.miner_cost, (value) => setRecoveryForm({ ...recoveryForm, miner_cost: value }), '0.000001'), + inputField('Historisch DOGE/USD', 'number', recoveryForm.doge_usd, (value) => setRecoveryForm({ ...recoveryForm, doge_usd: value }), '0.00000001'), + inputField('Historisch USD/EUR', 'number', recoveryForm.usd_eur, (value) => setRecoveryForm({ ...recoveryForm, usd_eur: value }), '0.00000001'), + inputField('Transfer Mining -> Wallet', 'datetime-local', recoveryForm.transfer_at, (value) => setRecoveryForm({ ...recoveryForm, transfer_at: value }), '1'), + inputField('Transfer DOGE', 'number', recoveryForm.transfer_coins, (value) => setRecoveryForm({ ...recoveryForm, transfer_coins: value }), '0.000001'), + inputField('Wallet-Snapshot', 'datetime-local', recoveryForm.wallet_at, (value) => setRecoveryForm({ ...recoveryForm, wallet_at: value }), '1'), + inputField('Wallet DOGE', 'number', recoveryForm.wallet_coins, (value) => setRecoveryForm({ ...recoveryForm, wallet_coins: value }), '0.00000001'), + inputField('Mining-Screenshot', 'datetime-local', recoveryForm.mining_at, (value) => setRecoveryForm({ ...recoveryForm, mining_at: value }), '1'), + inputField('Mining DOGE', 'number', recoveryForm.mining_coins, (value) => setRecoveryForm({ ...recoveryForm, mining_coins: value }), '0.000001'), + inputField('DOGE/USD im Screenshot', 'number', recoveryForm.mining_price_usd, (value) => setRecoveryForm({ ...recoveryForm, mining_price_usd: value }), '0.000001'), + h('button', { + type: 'submit', + className: 'mc-button mc-button--primary', + disabled: saving, + }, saving ? 'Speichert …' : 'Alle Daten nachtragen'), + ])), + ]); + } + if (activeTab === 'measurements') { return h('div', { className: 'mc-stack' }, [ panel('Mining-History', 'Die letzten 10 Mining-Uploads inkl. Performance-Werten und OCR-Metadaten.', h('div', { className: 'mc-table-shell' }, [ diff --git a/custom/apps/mining-checker/docs/README.md b/custom/apps/mining-checker/docs/README.md index 0e4bcb1d..46a83543 100644 --- a/custom/apps/mining-checker/docs/README.md +++ b/custom/apps/mining-checker/docs/README.md @@ -173,6 +173,8 @@ Fuer eine unvollstaendige Upload-Phase werden die belegbaren Ereignisse einzeln Fehlt ein exakter historischer FX-Snapshot, darf eine feste Krypto-Ausgabe nicht mit einem aktuellen Kurs gespeichert werden. Der fehlende historische Snapshot muss zuerst aus einer nachpruefbaren externen Quelle in `fx-rates` vorliegen; der Beleg oder Screenshot bleibt als Notiz am Nachtrag erhalten. +Fuer zusammenhaengende, belegbare Ausfaelle steht der Bereich `Nachtragen` bereit. Er legt zuerst einen historischen `fx-rates`-Snapshot an und speichert danach in fester Reihenfolge Krypto-Miete, Mining-Tool-Transfer, Wallet-Snapshot und Mining-Messpunkt. Wiederholte Ausfuehrung des Assistenten verwendet den FX-Snapshot mit identischem Zeitpunkt erneut. Die fachlichen Buchungen selbst duerfen nach einer erfolgreichen Ausfuehrung nicht erneut gestartet werden, damit keine doppelten Miner oder Transfers entstehen. + ## Break-even Der Gesamt-Break-even verwendet die Summe aus Fiat-Investitionen und `Reinvest fix` gegen aktuelle, noch gehaltene Miner- und Walletbestaende. Krypto-Reinvestitionen muessen immer den historischen festen Gegenwert aus dem zugeordneten `fx-rates`-Snapshot verwenden; eine Bewertung zum aktuellen Coin-Kurs ist unzulässig. diff --git a/custom/apps/mining-checker/src/Api/Router.php b/custom/apps/mining-checker/src/Api/Router.php index b353db2c..1187c6fc 100644 --- a/custom/apps/mining-checker/src/Api/Router.php +++ b/custom/apps/mining-checker/src/Api/Router.php @@ -166,6 +166,10 @@ final class Router $this->respond(['data' => $this->migrateLegacyFxRates($projectKey)], 201); } + if ($resource === 'historical-recovery-fx' && $method === 'POST') { + $this->respond(['data' => $this->saveHistoricalRecoveryFx($projectKey, Http::input())], 201); + } + if ($resource === 'bootstrap' && $method === 'GET') { $view = trim((string) ($_GET['view'] ?? 'overview')); $this->respond(['data' => $this->bootstrap($projectKey, $view)]); @@ -840,6 +844,43 @@ final class Router return $rows; } + private function saveHistoricalRecoveryFx(string $projectKey, array $input): array + { + $fxRepository = $this->sharedFxRatesRepository(); + if ($fxRepository === null) { + throw new ApiException('Das Modul fx-rates ist nicht verfuegbar.', 422); + } + + $measuredAt = $this->requiredDateTime($input['measured_at'] ?? null, 'measured_at', $this->projectTimezone($projectKey)); + $dogeUsd = $this->requiredDecimal($input['doge_usd'] ?? null, 'doge_usd'); + $usdEur = $this->requiredDecimal($input['usd_eur'] ?? null, 'usd_eur'); + $existing = $fxRepository->findFetchByBaseAndFetchedAt('USD', $measuredAt); + + if (is_array($existing) && is_numeric($existing['id'] ?? null)) { + return [ + 'fetch_id' => (int) $existing['id'], + 'reused' => true, + 'fetched_at' => $measuredAt, + ]; + } + + $saved = $fxRepository->saveFetch( + 'USD', + 'coingecko+valutafx', + substr($measuredAt, 0, 10), + ['DOGE' => $dogeUsd, 'EUR' => $usdEur], + $measuredAt, + 'migration' + ); + + return [ + 'fetch_id' => is_numeric($saved['fetch']['id'] ?? null) ? (int) $saved['fetch']['id'] : null, + 'reused' => false, + 'fetched_at' => $measuredAt, + 'provider' => 'coingecko+valutafx', + ]; + } + private function offerBasisHistory(string $projectKey): array { return $this->repository()->listOfferBasisHistory($projectKey, 50); diff --git a/docs/CONTENT.md b/docs/CONTENT.md index ea9fbf0f..e3df998e 100644 --- a/docs/CONTENT.md +++ b/docs/CONTENT.md @@ -136,6 +136,7 @@ Aus [custom/apps/README.md](/home/lars/Schreibtisch/Projekte/desktop.kusche.berl - `Mining-Checker` ist eine Desktop-App aus `custom/apps/mining-checker/` - Historische Mining-Nachtraege verwenden ihren belegten Zeitpunkt; fuer feste Krypto-Buchungen ist ein zeitnaher, referenzierter `fx-rates`-Snapshot Pflicht und ein aktueller Kurs darf keinen historischen Snapshot ersetzen. +- Der Bereich `Nachtragen` im Mining-Checker speichert zusammenhaengende historische Miner-, Transfer-, Wallet- und Mining-Daten in chronologischer Reihenfolge und legt den belegten FX-Snapshot davor an. - `Waehrungs-Checker` ist eine Desktop-App aus `custom/apps/fx-rates/` - die Desktop-Shell kann Moduldefinitionen zentral erkennen und als `App` bereitstellen - wiederverwendbare Modul-Helfer liegen im globalen Kern, die Fachlogik bleibt im Modul