diff --git a/custom/apps/mining-checker/assets/js/app.js b/custom/apps/mining-checker/assets/js/app.js index 3cfe6228..bc9278d9 100644 --- a/custom/apps/mining-checker/assets/js/app.js +++ b/custom/apps/mining-checker/assets/js/app.js @@ -3401,14 +3401,14 @@ return h('div', { className: 'mc-stack' }, [ h('div', { className: 'mc-two-col' }, [ renderSharedOcrPanel(), - panel('Mining manuell erfassen', 'Direkte Eingabe eines einzelnen Mining-Messpunkts mit serverseitiger Validierung.', h('form', { + panel('Mining manuell erfassen', 'Direkte Eingabe eines einzelnen Mining-Messpunkts. Ein belegter historischer Zeitpunkt wird unveraendert gespeichert.', h('form', { className: 'mc-form', onSubmit: function (event) { event.preventDefault(); submitMeasurement(false); }, }, [ - displayField('Zeitpunkt', 'Wird beim Speichern automatisch auf den aktuellen Bestätigungszeitpunkt gesetzt.'), + inputField('Zeitpunkt', 'datetime-local', measurementForm.measured_at, (value) => setMeasurementForm({ ...measurementForm, measured_at: value }), '1'), inputField('Coins total', 'number', measurementForm.coins_total, (value) => setMeasurementForm({ ...measurementForm, coins_total: value }), '0.000001'), inputField('Kurs', 'number', measurementForm.price_per_coin, (value) => setMeasurementForm({ ...measurementForm, price_per_coin: value }), '0.000001'), selectField('Waehrung', measurementForm.price_currency, [''].concat(selectableCurrencies.map((currency) => currency.code)), (value) => setMeasurementForm({ ...measurementForm, price_currency: value })), diff --git a/custom/apps/mining-checker/docs/README.md b/custom/apps/mining-checker/docs/README.md index e907df42..0e4bcb1d 100644 --- a/custom/apps/mining-checker/docs/README.md +++ b/custom/apps/mining-checker/docs/README.md @@ -112,7 +112,7 @@ Beispiel: `currencyapi.net` wird ueber das Modul `fx-rates` abgefragt. Aus dem Response werden `base`, `rates` und `updated` uebernommen; `valid` muss `true` sein. Die eigentlichen Fetches und Raten liegen im Modul `fx-rates`. -Pro Abruf entsteht genau ein Datensatz in `fx-rates` mit Basiswaehrung, Provider und Stichtag. Neue Mining-Messpunkte pruefen beim Speichern, ob ein neuer FX-Fetch noetig ist; falls nicht, wird die letzte passende `fetch_id` wiederverwendet. +Pro Abruf entsteht genau ein Datensatz in `fx-rates` mit Basiswaehrung, Provider und Stichtag. Neue Mining-Messpunkte pruefen beim Speichern, ob ein neuer FX-Fetch noetig ist; falls nicht, wird nur ein Snapshot innerhalb des zulaessigen Zeitfensters wiederverwendet. Ein historischer Messpunkt darf niemals auf einen heutigen oder sonst zeitlich fernen Fetch verweisen. Falls noch historische Mining-Checker-Fetches in `miningcheck_fx_fetches` und `miningcheck_fx_rates` liegen, kann `POST /api/mining-checker/v1/projects/{projectKey}/legacy-fx-migrate` diese nach `fx-rates` ueberfuehren. Danach werden bestehende Messpunkte soweit moeglich auf die passende `fx_fetch_id` aktualisiert. @@ -160,6 +160,19 @@ Beim Schema-Upgrade werden vorhandene Krypto-Miner einmalig bewertet: 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. +### Historische Nachtraege + +Ein belegter, nachtraeglich erfasster Mining-Stand verwendet seinen tatsaechlichen Zeitpunkt, nicht den Speicherzeitpunkt. Der manuelle Messpunkt besitzt deshalb ein optionales Feld `Zeitpunkt`; bleibt es leer, wird der aktuelle Zeitpunkt verwendet. + +Fuer eine unvollstaendige Upload-Phase werden die belegbaren Ereignisse einzeln und chronologisch erfasst: + +1. Krypto-Miete mit Zeitpunkt, tatsaechlichem Coin-Preis, Laufzeit, Hashrate inklusive Bonus und dem historischen FX-Snapshot. +2. Transfer aus dem Mining-Tool in das Wallet mit Zeitpunkt und Coin-Menge. +3. Wallet-Snapshot mit dem tatsaechlichen Wallet-Bestand; dieser ist ab seinem Zeitpunkt autoritativ. +4. Mining-Messpunkt mit Screenshot-Zeitpunkt, Miner-Bestand und dem im Screenshot angezeigten Kurs. + +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. + ## 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 c8b55fa0..b353db2c 100644 --- a/custom/apps/mining-checker/src/Api/Router.php +++ b/custom/apps/mining-checker/src/Api/Router.php @@ -1475,8 +1475,10 @@ final class Router $projectTimezone = $this->projectTimezone($projectKey); $source = $this->enumValue($input['source'] ?? 'manual', ['manual', 'image_ocr', 'seed_import'], 'source'); $payload = [ - 'measured_at' => $source === 'seed_import' - ? $this->requiredDateTime($input['measured_at'] ?? null, 'measured_at', $projectTimezone) + // A manual entry may document an older, verifiable upload. Only omit the + // timestamp when the user intentionally records the current state. + 'measured_at' => trim((string) ($input['measured_at'] ?? '')) !== '' + ? $this->requiredDateTime($input['measured_at'], 'measured_at', $projectTimezone) : $this->currentTimestamp(), 'coins_total' => $this->requiredDecimal($input['coins_total'] ?? null, 'coins_total'), 'coin_currency' => $this->measurementCoinCurrency($projectKey, $input), @@ -2861,18 +2863,21 @@ final class Router } if ($measuredAt !== '') { - $nearest = $this->fx()->nearestSnapshot('USD', $measuredAt, null, null); + // Never attach an unrelated current quote to a historical financial event. + $nearest = $this->fx()->nearestSnapshot('USD', $measuredAt, null, (int) round($maxAgeHours * 60)); if (is_array($nearest) && is_numeric($nearest['id'] ?? null)) { return (int) $nearest['id']; } } - $latest = $this->fx()->latestSnapshot('USD', null); - if (is_array($latest) && is_numeric($latest['id'] ?? null)) { - return (int) $latest['id']; + if ($measuredAt === '' || $this->isRecentTimestamp($measuredAt, $maxAgeHours)) { + $latest = $this->fx()->latestSnapshot('USD', null); + if (is_array($latest) && is_numeric($latest['id'] ?? null)) { + return (int) $latest['id']; + } } - if ($allowRefresh) { + if ($allowRefresh && ($measuredAt === '' || $this->isRecentTimestamp($measuredAt, $maxAgeHours))) { $fresh = $this->fx()->refreshLatestRates(null, 'USD'); if (is_array($fresh) && is_numeric($fresh['fetch_id'] ?? null)) { return (int) $fresh['fetch_id']; diff --git a/docs/CONTENT.md b/docs/CONTENT.md index 270bb786..ea9fbf0f 100644 --- a/docs/CONTENT.md +++ b/docs/CONTENT.md @@ -135,6 +135,7 @@ Aus [custom/apps/README.md](/home/lars/Schreibtisch/Projekte/desktop.kusche.berl ### Aktueller Modulstand - `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. - `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