diff --git a/custom/apps/mining-checker/assets/js/app.js b/custom/apps/mining-checker/assets/js/app.js index 24b7b58f..59bf7a97 100644 --- a/custom/apps/mining-checker/assets/js/app.js +++ b/custom/apps/mining-checker/assets/js/app.js @@ -2775,6 +2775,33 @@ } } + async function deletePurchasedMiner(miner) { + if (!miner || !miner.id) { + return; + } + + const label = String(miner.label || 'diesen Miner'); + if (!window.confirm(`Miete ${label} wirklich loeschen? Kosten, feste Krypto-Bewertung und der Wallet-Abzug werden ebenfalls entfernt.`)) { + return; + } + + setSaving(true); + setError(''); + setMessage(''); + try { + await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/purchased-miners/${encodeURIComponent(miner.id)}`, { + method: 'DELETE', + }); + invalidateProjectBootstrapCache(projectKey); + await loadBootstrap(projectKey); + setMessage('Gemieteten Miner mit allen zugeordneten Kosten geloescht.'); + } catch (err) { + setError(err.message); + } finally { + setSaving(false); + } + } + async function submitPurchaseMiner(event) { event.preventDefault(); if (!purchaseMinerForm.offer_id) { @@ -3722,14 +3749,27 @@ ]), h('td', { key: 'active' }, row.is_active ? 'ja' : 'nein'), h('td', { key: 'action' }, - row.can_toggle_auto_renew - ? h('button', { - type: 'button', - className: 'mc-button mc-button--ghost', - onClick: () => toggleMinerAutoRenew(row), - disabled: saving, - }, row.auto_renew ? 'Verlaengerung aus' : 'Verlaengerung an') - : '—' + h('div', { className: 'mc-inline-row' }, [ + row.can_toggle_auto_renew + ? h('button', { + key: 'renew', + type: 'button', + className: 'mc-button mc-button--ghost', + onClick: () => toggleMinerAutoRenew(row), + disabled: saving, + }, row.auto_renew ? 'Verlaengerung aus' : 'Verlaengerung an') + : null, + row.source === 'miete' + ? h('button', { + key: 'delete', + type: 'button', + className: 'mc-button mc-button--ghost', + onClick: () => deletePurchasedMiner({ id: row.miner_id, label: row.label }), + disabled: saving, + }, 'Loeschen') + : null, + !row.can_toggle_auto_renew && row.source !== 'miete' ? '—' : null, + ]) ), ])) : [h('tr', { key: 'empty' }, h('td', { colSpan: 9 }, emptyText))] diff --git a/custom/apps/mining-checker/docs/README.md b/custom/apps/mining-checker/docs/README.md index 24a41a1b..a309418a 100644 --- a/custom/apps/mining-checker/docs/README.md +++ b/custom/apps/mining-checker/docs/README.md @@ -50,6 +50,7 @@ custom/apps/mining-checker/ - `GET /api/mining-checker/v1/projects/{projectKey}/connection-test` - `GET /api/mining-checker/v1/projects/{projectKey}/fx-history` - `POST /api/mining-checker/v1/projects/{projectKey}/legacy-fx-migrate` +- `DELETE /api/mining-checker/v1/projects/{projectKey}/purchased-miners/{minerId}` ## Integration @@ -137,6 +138,7 @@ Diese Regeln sind bei jeder Erweiterung der Wallet-, Miner- oder Uebersichtslogi 6. Jede erfasste Auszahlung aus dem Mining-Tool muss den Walletbestand automatisch fortschreiben. Externe Auszahlungen aus dem hier verfolgten Wallet reduzieren ihn. 7. Der angezeigte bisherige Wert trennt variable Werte (Coins im Mining-Tool und Wallet zum aktuellen Kurs) von fixen Werten (bereits mit Krypto bezahlte Miner zum Ausgabezeitpunkt). Die Gesamtsumme besteht aus beiden Kategorien. 8. Jede neue feste Krypto-Buchung referenziert die `fetch_id` des Moduls `fx-rates`. Der Mining-Checker fuehrt keine eigene Kurstabelle. Umrechnungen in andere Berichtswährungen verwenden fuer fixe Werte denselben historischen API-Snapshot. +9. Wird ein gemieteter Miner geloescht, wird sein kompletter Datensatz entfernt: Mietkosten, feste Krypto-Bewertung, FX-Referenz und der daraus abgeleitete Wallet-Abzug entfallen. Der Miner beeinflusst danach weder Walletbestand noch Ausgaben, Reinvest, Hashrate oder Break-even. Basis-Angebote, Mining-Uploads und andere Wallet-Buchungen bleiben unveraendert. ### Historische Krypto-Miner diff --git a/custom/apps/mining-checker/src/Api/Router.php b/custom/apps/mining-checker/src/Api/Router.php index 3131d614..c8b55fa0 100644 --- a/custom/apps/mining-checker/src/Api/Router.php +++ b/custom/apps/mining-checker/src/Api/Router.php @@ -327,6 +327,11 @@ final class Router Http::json(['data' => $this->updatePurchasedMiner($projectKey, (int) $matches[1], Http::input())]); } + if (preg_match('~^purchased-miners/(\d+)$~', $resource, $matches) && $method === 'DELETE') { + $this->deletePurchasedMiner($projectKey, (int) $matches[1]); + Http::json(['data' => ['deleted' => true]]); + } + if (preg_match('~^miner-offers/([^/]+)/purchase$~', $resource, $matches) && $method === 'POST') { $this->repository()->ensureProject($projectKey); Http::json(['data' => $this->purchaseMiner($projectKey, rawurldecode((string) $matches[1]), Http::input())], 201); @@ -2179,6 +2184,19 @@ final class Router ); } + private function deletePurchasedMiner(string $projectKey, int $minerId): void + { + if ($minerId <= 0) { + throw new ApiException('miner_id ist ungueltig.', 422); + } + + if (!is_array($this->repository()->getPurchasedMiner($projectKey, $minerId))) { + throw new ApiException('Gemieteter Miner nicht gefunden.', 404); + } + + $this->repository()->deletePurchasedMiner($projectKey, $minerId); + } + private function findComputedMinerOffer(string $projectKey, string $offerId): ?array { foreach ($this->minerOffers($projectKey) as $offer) { diff --git a/custom/apps/mining-checker/src/Infrastructure/MiningRepository.php b/custom/apps/mining-checker/src/Infrastructure/MiningRepository.php index 0c3f1f09..5f518cf3 100644 --- a/custom/apps/mining-checker/src/Infrastructure/MiningRepository.php +++ b/custom/apps/mining-checker/src/Infrastructure/MiningRepository.php @@ -1144,6 +1144,37 @@ final class MiningRepository return $this->normalizeRow($fetch->fetch() ?: []); } + public function deletePurchasedMiner(string $projectKey, int $minerId): void + { + if ($minerId <= 0) { + return; + } + + $delete = $this->pdo->prepare( + 'DELETE FROM ' . $this->table('purchased_miners') . ' + WHERE id = :id AND project_key = :project_key AND owner_sub = :owner_sub' + ); + $delete->execute([ + 'id' => $minerId, + 'project_key' => $projectKey, + 'owner_sub' => $this->ownerSub, + ]); + + if ($delete->rowCount() > 0) { + return; + } + + // Older rows without an owner assignment remain deletable like the existing read/update fallback. + $legacyDelete = $this->pdo->prepare( + 'DELETE FROM ' . $this->table('purchased_miners') . ' + WHERE id = :id AND project_key = :project_key AND owner_sub IS NULL' + ); + $legacyDelete->execute([ + 'id' => $minerId, + 'project_key' => $projectKey, + ]); + } + public function getMinerOffer(string $projectKey, int $offerId): ?array { $stmt = $this->pdo->prepare('SELECT * FROM ' . $this->table('miner_offers') . ' WHERE project_key = :project_key AND id = :id LIMIT 1');