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

This commit is contained in:
2026-06-21 02:21:10 +02:00
parent cf3dacfd6b
commit 5c615aa878
3 changed files with 101 additions and 42 deletions

View File

@@ -305,10 +305,10 @@
return 'Historie der letzten Uploads mit Performance- und Trendwerten.';
}
if (sectionId === 'wallet') {
return 'Wallet-Snapshots und erkannte Asset-Bestände auswerten.';
return 'Wallet-Snapshots, Transfers und erkannte Asset-Bestände auswerten.';
}
if (sectionId === 'mining') {
return 'Miner, Angebote, Auszahlungen und operative Daten verwalten.';
return 'Miner, Angebote und operative Daten verwalten.';
}
if (sectionId === 'dashboards') {
return 'Gespeicherte Auswertungen und Visualisierungen anzeigen.';
@@ -1910,7 +1910,7 @@
});
applySavedPayout(savedPayout);
invalidateProjectBootstrapCache(projectKey);
setMessage('Auszahlung gespeichert.');
setMessage('Wallet-Transfer gespeichert.');
setPayoutForm({ payout_at: '', coins_amount: '', payout_currency: currentSettings.crypto_currency || 'DOGE', note: '' });
setPayoutModalOpen(false);
} catch (err) {
@@ -1920,6 +1920,32 @@
}
}
async function deleteWalletTransfer(transfer) {
if (!transfer || !transfer.id) {
return;
}
const label = `${fmtNumber(transfer.coins_amount, 6)} ${transfer.payout_currency || ''}`.trim();
if (!window.confirm(`Diesen Wallet-Transfer (${label}) wirklich loeschen?`)) {
return;
}
setSaving(true);
setError('');
setMessage('');
try {
await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/payouts/${encodeURIComponent(transfer.id)}`, {
method: 'DELETE',
});
invalidateProjectBootstrapCache(projectKey);
await reloadBootstrapAfterMutation('Wallet-Transfer geloescht.');
} catch (err) {
setError(err.message);
} finally {
setSaving(false);
}
}
async function submitMinerOffer(event) {
event.preventDefault();
setSaving(true);
@@ -2414,13 +2440,13 @@
}),
h(StatCard, {
key: 'perday-since-payout',
label: `${currentCoinCurrency} pro Tag seit letzter Auszahlung`,
label: `${currentCoinCurrency} pro Tag seit letztem Transfer`,
value: latest && latest.doge_per_day_since_last_payout !== null && latest.doge_per_day_since_last_payout !== undefined
? fmtNumber(latest.doge_per_day_since_last_payout, 4)
: 'n/a',
sub: latest && latest.last_payout_at
? `Seit ${fmtDate(latest.last_payout_at)} · ${fmtNumber(latest.coins_since_last_payout, 6)} ${currentCoinCurrency}`
: 'Noch keine Auszahlung vor dem letzten Upload',
: 'Noch kein Transfer vor dem letzten Upload',
}),
h(StatCard, {
key: 'value',
@@ -2492,7 +2518,7 @@
panel('Mining-History', 'Die letzten 10 Mining-Uploads inkl. Performance-Werten und OCR-Metadaten.', h('div', { className: 'mc-table-shell' }, [
h('table', { key: 'table', className: 'mc-table' }, [
h('thead', { key: 'thead' }, h('tr', null, [
'Zeit', 'Coins', 'Kurs', 'Quelle', perDayLabel, 'Seit Auszahlung/Tag', 'Trend', 'Notiz', 'Aktion'
'Zeit', 'Coins', 'Kurs', 'Quelle', perDayLabel, 'Seit Transfer/Tag', 'Trend', 'Notiz', 'Aktion'
].map((label) => h('th', { key: label }, label)))),
h('tbody', { key: 'tbody' },
measurements.slice(-10).reverse().map((row) => h('tr', { key: row.id }, [
@@ -2652,6 +2678,45 @@
),
]),
])),
panel('Wallet-Transfers', 'Transfers zum hier verfolgten Wallet erhoehen den Wallet-Bestand und reduzieren den sichtbaren Miner-Bestand. Reinvestierte Krypto-Minerkaeufe werden davon automatisch wieder abgezogen.', [
h('div', { key: 'actions', className: 'mc-inline-row' }, [
h('button', {
key: 'add-transfer',
type: 'button',
className: 'mc-button mc-button--secondary',
onClick: () => {
setPayoutForm((previous) => ({
payout_at: previous.payout_at || nowDateTimeLocalValue(),
coins_amount: previous.coins_amount || '',
payout_currency: currentCoinCurrency,
note: previous.note || '',
}));
setPayoutModalOpen(true);
},
}, 'Zu Wallet transferieren'),
]),
h('div', { key: 'transfer-list', className: 'mc-table-shell' }, [
h('table', { key: 'transfer-table', className: 'mc-table' }, [
h('thead', { key: 'head' }, h('tr', null, ['Zeit', 'Coins', 'Waehrung', 'Notiz', 'Aktion'].map((label) => h('th', { key: label }, label)))),
h('tbody', { key: 'body' },
currentPayouts.length
? currentPayouts.slice().reverse().map((transfer) => h('tr', { key: transfer.id }, [
h('td', { key: 'time' }, fmtDate(transfer.payout_at)),
h('td', { key: 'coins' }, fmtNumber(transfer.coins_amount, 6)),
h('td', { key: 'currency' }, transfer.payout_currency),
h('td', { key: 'note' }, transfer.note || '—'),
h('td', { key: 'action' }, h('button', {
type: 'button',
className: 'mc-button mc-button--ghost',
onClick: () => deleteWalletTransfer(transfer),
disabled: saving,
}, 'Loeschen')),
]))
: [h('tr', { key: 'empty' }, h('td', { colSpan: 5 }, 'Noch keine Wallet-Transfers hinterlegt.'))]
),
]),
]),
]),
]);
}
@@ -2768,39 +2833,6 @@
]),
]),
]),
panel('Auszahlungen', 'Auszahlungen reduzieren den sichtbaren Coin-Bestand, bleiben aber in der Gesamtleistung erhalten.', [
h('div', { key: 'actions', className: 'mc-inline-row' }, [
h('button', {
key: 'add-payout',
type: 'button',
className: 'mc-button mc-button--secondary',
onClick: () => {
setPayoutForm((previous) => ({
payout_at: previous.payout_at || nowDateTimeLocalValue(),
coins_amount: previous.coins_amount || '',
payout_currency: currentCoinCurrency,
note: previous.note || '',
}));
setPayoutModalOpen(true);
},
}, 'Auszahlung erfassen'),
]),
h('div', { key: 'payout-list', className: 'mc-table-shell' }, [
h('table', { key: 'payout-table', className: 'mc-table' }, [
h('thead', { key: 'head' }, h('tr', null, ['Zeit', 'Coins', 'Waehrung', 'Notiz'].map((label) => h('th', { key: label }, label)))),
h('tbody', { key: 'body' },
currentPayouts.length
? currentPayouts.slice().reverse().map((payout) => h('tr', { key: payout.id }, [
h('td', { key: 'time' }, fmtDate(payout.payout_at)),
h('td', { key: 'coins' }, fmtNumber(payout.coins_amount, 6)),
h('td', { key: 'currency' }, payout.payout_currency),
h('td', { key: 'note' }, payout.note || '—'),
]))
: [h('tr', { key: 'empty' }, h('td', { colSpan: 4 }, 'Noch keine Auszahlungen hinterlegt.'))]
),
]),
]),
]),
panel('Miner-Angebote', currentWalletMiningBalance !== null
? `Hier werden nur Basis-Miner gepflegt. Krypto-Angebote werden gegen deinen aktuellen Wallet-Bestand (${fmtNumber(currentWalletMiningBalance, 8)} ${currentMiningCurrency}) gefiltert.`
: 'Hier werden nur Basis-Miner gepflegt. Alle vorgegebenen Geschwindigkeiten und Preise werden daraus automatisch abgeleitet.',
@@ -3036,15 +3068,15 @@
]),
]),
], () => setCostPlanModalOpen(false)) : null,
payoutModalOpen ? renderModal('Auszahlung erfassen', [
payoutModalOpen ? renderModal('Zu Wallet transferieren', [
h('form', { key: 'form', className: 'mc-form', onSubmit: submitPayout }, [
inputField('Auszahlungszeitpunkt', 'datetime-local', payoutForm.payout_at, (value) => setPayoutForm({ ...payoutForm, payout_at: value })),
inputField('Transferzeitpunkt', 'datetime-local', payoutForm.payout_at, (value) => setPayoutForm({ ...payoutForm, payout_at: value })),
inputField('Coins', 'number', payoutForm.coins_amount, (value) => setPayoutForm({ ...payoutForm, coins_amount: value }), '0.000001'),
selectField('Waehrung', payoutForm.payout_currency, [currentCoinCurrency].concat(selectableCurrencies.map((currency) => currency.code).filter((code) => code !== currentCoinCurrency)), (value) => setPayoutForm({ ...payoutForm, payout_currency: value })),
textareaField('Notiz', payoutForm.note, (value) => setPayoutForm({ ...payoutForm, note: value })),
h('div', { className: 'mc-inline-row' }, [
h('button', { type: 'button', className: 'mc-button mc-button--ghost', onClick: () => setPayoutModalOpen(false) }, 'Abbrechen'),
h('button', { type: 'submit', className: 'mc-button mc-button--secondary', disabled: saving }, saving ? 'Speichert …' : 'Auszahlung speichern'),
h('button', { type: 'submit', className: 'mc-button mc-button--secondary', disabled: saving }, saving ? 'Speichert …' : 'Transfer speichern'),
]),
]),
], () => setPayoutModalOpen(false)) : null,

View File

@@ -257,6 +257,11 @@ final class Router
Http::json(['data' => $this->savePayout($projectKey, Http::input())], 201);
}
if (preg_match('~^payouts/(\d+)$~', $resource, $matches) && $method === 'DELETE') {
$this->deletePayout($projectKey, (int) $matches[1]);
Http::json(['data' => ['deleted' => true]]);
}
if ($resource === 'wallet-snapshots' && $method === 'GET') {
Http::json(['data' => $this->walletSnapshots($projectKey)]);
}
@@ -1766,6 +1771,11 @@ final class Router
return $this->repository()->savePayout($projectKey, $payload);
}
private function deletePayout(string $projectKey, int $payoutId): void
{
$this->repository()->deletePayout($projectKey, $payoutId);
}
private function walletSnapshots(string $projectKey): array
{
return $this->repository()->listWalletSnapshots($projectKey, 100);

View File

@@ -442,6 +442,23 @@ final class MiningRepository
return $this->normalizeRow($fetch->fetch() ?: []);
}
public function deletePayout(string $projectKey, int $payoutId): void
{
if ($payoutId <= 0) {
return;
}
$stmt = $this->pdo->prepare(
'DELETE FROM ' . $this->table('payouts') . '
WHERE id = :id AND project_key = :project_key AND owner_sub = :owner_sub'
);
$stmt->execute([
'id' => $payoutId,
'project_key' => $projectKey,
'owner_sub' => $this->ownerSub,
]);
}
public function listWalletSnapshots(string $projectKey, int $limit = 100): array
{
$stmt = $this->pdo->prepare(