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) {
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))]

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}/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

View File

@@ -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) {

View File

@@ -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');