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

This commit is contained in:
2026-08-21 01:14:32 +02:00
parent 95100d3a78
commit fb852e33e5
4 changed files with 92 additions and 14 deletions

View File

@@ -1361,8 +1361,8 @@
? convertCurrencyValue(preferredOfferRequiredCoins, currentMiningCurrency, reportCurrency)
: null;
const preferredOfferRemainingDaysAtUpload = preferredOfferRemainingCoins !== null
&& Number(latest?.doge_per_day_interval) > 0
? (preferredOfferRemainingCoins / Number(latest.doge_per_day_interval))
&& Number(latest?.doge_per_day_since_last_payout) > 0
? (preferredOfferRemainingCoins / Number(latest.doge_per_day_since_last_payout))
: null;
const preferredOfferUploadTimestamp = latest?.measured_at ? Date.parse(latest.measured_at) : NaN;
const preferredOfferEtaTimestamp = preferredOfferRemainingDaysAtUpload !== null && Number.isFinite(preferredOfferUploadTimestamp)
@@ -3337,8 +3337,8 @@
preferredOfferRemainingCoins !== null
? `Rest ${fmtNumber(preferredOfferRemainingCoins, 4)} ${currentMiningCurrency}`
: null,
latest?.doge_per_day_interval !== null && latest?.doge_per_day_interval !== undefined
? `Basis ${fmtNumber(latest.doge_per_day_interval, 4)} ${currentMiningCurrency}/Tag ab Upload`
latest?.doge_per_day_since_last_payout !== null && latest?.doge_per_day_since_last_payout !== undefined
? `Basis ${fmtNumber(latest.doge_per_day_since_last_payout, 4)} ${currentMiningCurrency}/Tag seit letztem Transfer`
: null,
preferredOfferEta ? `ETA ${fmtDate(preferredOfferEta)}` : null,
].filter(Boolean).join(' · '),
@@ -3381,7 +3381,7 @@
yLabel: `Kurs ${reportCurrency}`,
})),
]),
panel('Zielmonitor', `Die ETA wird ab dem letzten Upload mit dessen durchschnittlicher Tagesrate berechnet; die Restzeit laeuft bis zum naechsten Upload weiter.`,
panel('Zielmonitor', `Die ETA wird ab dem letzten Upload mit dem Durchschnitt seit dem letzten Transfer berechnet; die Restzeit laeuft bis zum naechsten Upload weiter.`,
h('div', { className: 'mc-target-grid' },
currentTargets.map((target, index) => h('div', {
key: index,

View File

@@ -143,12 +143,16 @@ Diese Regeln sind bei jeder Erweiterung der Wallet-, Miner- oder Uebersichtslogi
Beim Schema-Upgrade werden vorhandene Krypto-Miner einmalig bewertet:
1. Gibt es einen Referenzpreis in einer anderen Waehrung als der gezahlten Kryptowaehrung, wird dieser als historisch nachvollziehbarer, fester Gegenwert verwendet.
2. Fehlt eine belastbare historische Referenz, wird der ausgegebene Coin-Betrag mit dem fest vereinbarten Fallback `0,07 EUR` pro Coin bewertet und dauerhaft gespeichert.
2. Fehlt eine belastbare historische Referenz, gilt verbindlich der Fallback `1 Coin = 0,07 USD`. Fuer die Euroanzeige ohne historischen Snapshot gilt verbindlich `1 Coin = 0,068 EUR`. Diese beiden Fallbacks sind feste historische Werte und duerfen nicht mit einem aktuellen Wechselkurs weitergerechnet werden.
3. Die feste Bewertung wird in `settled_value_amount`, `settled_value_currency` und bei neuen Buchungen zusaetzlich in `settled_fx_fetch_id` des jeweiligen gekauften Miners gespeichert. Das Schema-Upgrade ist nach dem Deployment auszufuehren.
### Pflicht fuer neue Finanzdaten
Jede neu gespeicherte Buchung, die einen Kurs oder eine Waehrungsumrechnung benoetigt, muss die verwendete `fx-rates.fetch_id` gemeinsam mit Betrag, Quellwaehrung und Zielwaehrung speichern. Ein aktueller Kurs darf ausschliesslich fuer variable, noch gehaltene Bestaende verwendet werden. Fehlt fuer eine neue feste Buchung ein FX-Snapshot, muss der Speichervorgang fehlschlagen; ein stiller Fallback ist nicht erlaubt.
## Verbindliche Fachregeln: Zielminer-ETA
1. Die Zielminer-ETA basiert ausschliesslich auf dem letzten Mining-Upload, dessen Coin-Bestand und dessen durchschnittlicher Tagesrate (`doge_per_day_interval`, technisch historisch benannt).
1. Die Zielminer-ETA basiert ausschliesslich auf dem letzten Mining-Upload, dessen Coin-Bestand und dem Durchschnitt seit dem letzten Transfer (`doge_per_day_since_last_payout`, technisch historisch benannt).
2. Die absolute ETA lautet: `Zeitpunkt letzter Upload + (am Upload fehlende Coins / durchschnittliche Coins pro Tag)`.
3. Die angezeigte Restzeit ist die Differenz zwischen dieser festen ETA und der aktuellen Zeit. Sie muss bis zum naechsten Upload weiter herunterlaufen und darf nicht durch eine Neuberechnung ab der aktuellen Zeit nach hinten verschoben werden.
4. Ein neuer Upload ersetzt die Berechnungsbasis durch seinen dann aktuellen Bestand und seinen neu ermittelten Durchschnitt.

View File

@@ -356,11 +356,14 @@ final class AnalyticsService
$price = $latestPriceByCurrency[$currency] ?? $this->convertLatestPrice($latestPriceByCurrency, $currency, $latest);
$requiredDoge = ($price && $targetAmount !== null) ? $targetAmount / $price : null;
$remainingDogeAtUpload = $requiredDoge !== null ? $requiredDoge - (float) ($latest['coins_total_effective'] ?? $latest['coins_total']) : null;
$targetDailyRate = is_numeric($latest['doge_per_day_since_last_payout'] ?? null)
? (float) $latest['doge_per_day_since_last_payout']
: null;
$remainingDaysAtUpload = (
$remainingDogeAtUpload !== null &&
$latest['doge_per_day_interval'] !== null &&
(float) $latest['doge_per_day_interval'] > 0
) ? $remainingDogeAtUpload / (float) $latest['doge_per_day_interval'] : null;
$targetDailyRate !== null &&
$targetDailyRate > 0
) ? $remainingDogeAtUpload / $targetDailyRate : null;
$targetEtaAt = null;
if ($remainingDaysAtUpload !== null) {
if ($remainingDaysAtUpload <= 0) {
@@ -378,8 +381,8 @@ final class AnalyticsService
$remainingDays = $targetEtaAt !== null
? max(0.0, ($this->utcTimestamp($targetEtaAt) - time()) / 86400)
: null;
$remainingDoge = ($remainingDays !== null && $latest['doge_per_day_interval'] !== null)
? max(0.0, $remainingDays * (float) $latest['doge_per_day_interval'])
$remainingDoge = ($remainingDays !== null && $targetDailyRate !== null)
? max(0.0, $remainingDays * $targetDailyRate)
: $remainingDogeAtUpload;
$targetSummary[] = array_merge($target, [
@@ -392,6 +395,7 @@ final class AnalyticsService
'remaining_doge' => $this->roundOrNull($remainingDoge, 6),
'remaining_doge_at_upload' => $this->roundOrNull($remainingDogeAtUpload, 6),
'remaining_days_at_upload' => $this->roundOrNull($remainingDaysAtUpload, 4),
'target_daily_rate' => $this->roundOrNull($targetDailyRate, 6),
'remaining_days' => $this->roundOrNull($remainingDays, 4),
'target_eta_at' => $targetEtaAt,
'status' => $remainingDays !== null && $remainingDays <= 0 ? 'reached' : 'open',
@@ -1683,15 +1687,60 @@ final class AnalyticsService
$amount = is_numeric($miner['settled_value_amount'] ?? null) ? (float) $miner['settled_value_amount'] : null;
$currency = strtoupper(trim((string) ($miner['settled_value_currency'] ?? '')));
$spentAmount = is_numeric($miner['total_cost_amount'] ?? null) ? (float) $miner['total_cost_amount'] : null;
$spentCurrency = strtoupper(trim((string) ($miner['currency'] ?? '')));
// Old entries predate the settlement columns. The spent crypto amount itself
// remains the authoritative fixed amount when it matches the requested asset.
if ($spentAmount !== null && $spentCurrency !== '' && $spentCurrency === strtoupper(trim($targetCurrency))) {
$matched = true;
$total += $spentAmount;
continue;
}
if ($amount === null) {
$referenceAmount = is_numeric($miner['reference_price_amount'] ?? null)
? (float) $miner['reference_price_amount']
: null;
$referenceCurrency = strtoupper(trim((string) ($miner['reference_price_currency'] ?? '')));
if ($referenceAmount !== null && $referenceCurrency !== '' && $referenceCurrency !== $spentCurrency) {
$amount = $referenceAmount;
$currency = $referenceCurrency;
} elseif ($spentAmount !== null) {
$amount = $spentAmount * 0.07;
$currency = 'USD';
}
}
// The agreed legacy fallback is deliberately explicit: no current FX rate
// may alter a historic fallback value in USD or EUR.
$target = strtoupper(trim($targetCurrency));
$hasHistoricalSnapshot = is_numeric($miner['settled_fx_fetch_id'] ?? null) && (int) $miner['settled_fx_fetch_id'] > 0;
if (!$hasHistoricalSnapshot && $spentAmount !== null && $amount !== null && $currency === 'USD' && abs($amount - ($spentAmount * 0.07)) < 0.00000001 && $target === 'EUR') {
$matched = true;
$total += $spentAmount * 0.068;
continue;
}
if ($amount === null || $currency === '') {
continue;
}
if ($currency === strtoupper(trim($targetCurrency))) {
$matched = true;
$total += $amount;
continue;
}
$historicalFxContext = [
'fx_fetch_id' => $miner['settled_fx_fetch_id'] ?? null,
'measured_at' => $miner['purchased_at'] ?? null,
];
$converted = $this->convertAmount($amount, $currency, $targetCurrency, $historicalFxContext);
// Legacy records without a linked snapshot still remain visible. New records
// never use this fallback because their snapshot reference is mandatory.
if ($converted === null && !$hasHistoricalSnapshot) {
$converted = $this->convertAmount($amount, $currency, $targetCurrency, $fxContext);
}
if ($converted === null) {
continue;
}

View File

@@ -267,6 +267,9 @@ final class SchemaManager
$this->upgradePurchasedMinerSettlementColumns();
$applied[] = 'purchased_miner_settlement_columns';
}
if ($this->repairLegacySettlementFallbacks() > 0) {
$applied[] = 'legacy_settlement_fallbacks';
}
if ($this->tableExists($this->prefix . 'targets') && $this->tableExists($this->prefix . 'miner_offers')) {
$this->ensureTargetOfferForeignKey();
$applied[] = 'target_offer_foreign_key';
@@ -417,6 +420,9 @@ final class SchemaManager
$this->upgradePurchasedMinerSettlementColumns();
$applied[] = 'purchased_miner_settlement_columns';
}
if ($this->repairLegacySettlementFallbacks() > 0) {
$applied[] = 'legacy_settlement_fallbacks';
}
if ($this->tableExists($this->prefix . 'targets') && $this->tableExists($this->prefix . 'miner_offers')) {
$this->ensureTargetOfferForeignKey();
$applied[] = 'target_offer_foreign_key';
@@ -1533,13 +1539,13 @@ final class SchemaManager
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS settled_value_amount NUMERIC(20,8)',
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS settled_value_currency VARCHAR(10)',
'ALTER TABLE ' . $table . ' ADD COLUMN IF NOT EXISTS settled_fx_fetch_id BIGINT',
"UPDATE " . $table . " SET settled_value_amount = CASE WHEN reference_price_amount IS NOT NULL AND NULLIF(BTRIM(reference_price_currency), '') IS NOT NULL AND UPPER(reference_price_currency) <> UPPER(currency) THEN reference_price_amount ELSE total_cost_amount * 0.07 END, settled_value_currency = CASE WHEN reference_price_amount IS NOT NULL AND NULLIF(BTRIM(reference_price_currency), '') IS NOT NULL AND UPPER(reference_price_currency) <> UPPER(currency) THEN reference_price_currency ELSE 'EUR' END WHERE UPPER(currency) IN ('ADA','ARB','BNB','BTC','DAI','DOGE','DOT','ETH','LINK','LTC','SOL','USDC','USDT','XRP') AND settled_value_amount IS NULL",
"UPDATE " . $table . " SET settled_value_amount = CASE WHEN reference_price_amount IS NOT NULL AND NULLIF(BTRIM(reference_price_currency), '') IS NOT NULL AND UPPER(reference_price_currency) <> UPPER(currency) THEN reference_price_amount ELSE total_cost_amount * 0.07 END, settled_value_currency = CASE WHEN reference_price_amount IS NOT NULL AND NULLIF(BTRIM(reference_price_currency), '') IS NOT NULL AND UPPER(reference_price_currency) <> UPPER(currency) THEN reference_price_currency ELSE 'USD' END WHERE UPPER(currency) IN ('ADA','ARB','BNB','BTC','DAI','DOGE','DOT','ETH','LINK','LTC','SOL','USDC','USDT','XRP') AND settled_value_amount IS NULL",
]
: [
'ALTER TABLE `' . $table . '` ADD COLUMN settled_value_amount DECIMAL(20,8) NULL',
'ALTER TABLE `' . $table . '` ADD COLUMN settled_value_currency VARCHAR(10) NULL',
'ALTER TABLE `' . $table . '` ADD COLUMN settled_fx_fetch_id BIGINT UNSIGNED NULL',
"UPDATE `" . $table . "` SET settled_value_amount = CASE WHEN reference_price_amount IS NOT NULL AND NULLIF(TRIM(reference_price_currency), '') IS NOT NULL AND UPPER(reference_price_currency) <> UPPER(currency) THEN reference_price_amount ELSE total_cost_amount * 0.07 END, settled_value_currency = CASE WHEN reference_price_amount IS NOT NULL AND NULLIF(TRIM(reference_price_currency), '') IS NOT NULL AND UPPER(reference_price_currency) <> UPPER(currency) THEN reference_price_currency ELSE 'EUR' END WHERE UPPER(currency) IN ('ADA','ARB','BNB','BTC','DAI','DOGE','DOT','ETH','LINK','LTC','SOL','USDC','USDT','XRP') AND settled_value_amount IS NULL",
"UPDATE `" . $table . "` SET settled_value_amount = CASE WHEN reference_price_amount IS NOT NULL AND NULLIF(TRIM(reference_price_currency), '') IS NOT NULL AND UPPER(reference_price_currency) <> UPPER(currency) THEN reference_price_amount ELSE total_cost_amount * 0.07 END, settled_value_currency = CASE WHEN reference_price_amount IS NOT NULL AND NULLIF(TRIM(reference_price_currency), '') IS NOT NULL AND UPPER(reference_price_currency) <> UPPER(currency) THEN reference_price_currency ELSE 'USD' END WHERE UPPER(currency) IN ('ADA','ARB','BNB','BTC','DAI','DOGE','DOT','ETH','LINK','LTC','SOL','USDC','USDT','XRP') AND settled_value_amount IS NULL",
];
foreach ($statements as $statement) {
@@ -1554,6 +1560,25 @@ final class SchemaManager
}
}
private function repairLegacySettlementFallbacks(): int
{
$table = $this->prefix . 'purchased_miners';
if (
!$this->tableExists($table) ||
!$this->columnExists($table, 'settled_value_amount') ||
!$this->columnExists($table, 'settled_value_currency') ||
!$this->columnExists($table, 'settled_fx_fetch_id')
) {
return 0;
}
$tableReference = $this->driver === 'pgsql' ? $table : '`' . str_replace('`', '``', $table) . '`';
$sql = "UPDATE {$tableReference} SET settled_value_amount = total_cost_amount * 0.07, settled_value_currency = 'USD' WHERE settled_fx_fetch_id IS NULL AND UPPER(currency) IN ('ADA','ARB','BNB','BTC','DAI','DOGE','DOT','ETH','LINK','LTC','SOL','USDC','USDT','XRP') AND UPPER(COALESCE(settled_value_currency, '')) = 'EUR' AND settled_value_amount = total_cost_amount * 0.07";
$stmt = $this->pdo->prepare($sql);
$stmt->execute();
return $stmt->rowCount();
}
private function upgradeServerDailyCostColumns(): void
{
foreach ([$this->prefix . 'cost_plans', $this->prefix . 'purchased_miners'] as $table) {