miner
All checks were successful
Deploy / deploy-staging (push) Successful in 1m10s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-08-21 23:37:27 +02:00
parent b3b3a27257
commit d8d1ad96ee
4 changed files with 99 additions and 8 deletions

View File

@@ -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) { async function submitPurchaseMiner(event) {
event.preventDefault(); event.preventDefault();
if (!purchaseMinerForm.offer_id) { if (!purchaseMinerForm.offer_id) {
@@ -3722,14 +3749,27 @@
]), ]),
h('td', { key: 'active' }, row.is_active ? 'ja' : 'nein'), h('td', { key: 'active' }, row.is_active ? 'ja' : 'nein'),
h('td', { key: 'action' }, h('td', { key: 'action' },
row.can_toggle_auto_renew h('div', { className: 'mc-inline-row' }, [
? h('button', { row.can_toggle_auto_renew
type: 'button', ? h('button', {
className: 'mc-button mc-button--ghost', key: 'renew',
onClick: () => toggleMinerAutoRenew(row), type: 'button',
disabled: saving, className: 'mc-button mc-button--ghost',
}, row.auto_renew ? 'Verlaengerung aus' : 'Verlaengerung an') 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))] : [h('tr', { key: 'empty' }, h('td', { colSpan: 9 }, emptyText))]

View File

@@ -50,6 +50,7 @@ custom/apps/mining-checker/
- `GET /api/mining-checker/v1/projects/{projectKey}/connection-test` - `GET /api/mining-checker/v1/projects/{projectKey}/connection-test`
- `GET /api/mining-checker/v1/projects/{projectKey}/fx-history` - `GET /api/mining-checker/v1/projects/{projectKey}/fx-history`
- `POST /api/mining-checker/v1/projects/{projectKey}/legacy-fx-migrate` - `POST /api/mining-checker/v1/projects/{projectKey}/legacy-fx-migrate`
- `DELETE /api/mining-checker/v1/projects/{projectKey}/purchased-miners/{minerId}`
## Integration ## 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. 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. 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. 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 ### Historische Krypto-Miner

View File

@@ -327,6 +327,11 @@ final class Router
Http::json(['data' => $this->updatePurchasedMiner($projectKey, (int) $matches[1], Http::input())]); 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') { if (preg_match('~^miner-offers/([^/]+)/purchase$~', $resource, $matches) && $method === 'POST') {
$this->repository()->ensureProject($projectKey); $this->repository()->ensureProject($projectKey);
Http::json(['data' => $this->purchaseMiner($projectKey, rawurldecode((string) $matches[1]), Http::input())], 201); 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 private function findComputedMinerOffer(string $projectKey, string $offerId): ?array
{ {
foreach ($this->minerOffers($projectKey) as $offer) { foreach ($this->minerOffers($projectKey) as $offer) {

View File

@@ -1144,6 +1144,37 @@ final class MiningRepository
return $this->normalizeRow($fetch->fetch() ?: []); 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 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'); $stmt = $this->pdo->prepare('SELECT * FROM ' . $this->table('miner_offers') . ' WHERE project_key = :project_key AND id = :id LIMIT 1');