minder change
All checks were successful
Deploy / deploy-staging (push) Successful in 32s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-07-03 02:27:57 +02:00
parent 17c4bf16c2
commit cb8c494a23
4 changed files with 281 additions and 192 deletions

View File

@@ -477,6 +477,9 @@
daily_cost_currency: 'EUR',
report_currency: 'EUR',
crypto_currency: 'DOGE',
crypto_base_price_amount: '',
crypto_base_price_currency: 'USD',
fiat_base_price_amount: '',
preferred_currencies: ['DOGE', 'USD', 'EUR'],
cost_plans: [],
currencies: [],
@@ -882,6 +885,13 @@
report_currency: 'EUR',
crypto_currency: 'DOGE',
});
const [offerPreviewForm, setOfferPreviewForm] = useState({
crypto_base_price_amount: '',
crypto_base_price_currency: 'USD',
fiat_base_price_amount: '',
});
const [previewMinerOffers, setPreviewMinerOffers] = useState([]);
const [previewOffersLoading, setPreviewOffersLoading] = useState(false);
const [moduleAuthForm, setModuleAuthForm] = useState({
required: true,
users: '',
@@ -943,16 +953,6 @@
});
const [walletWithdrawalModalOpen, setWalletWithdrawalModalOpen] = useState(false);
const [walletWithdrawalRows, setWalletWithdrawalRows] = useState([]);
const [minerOfferForm, setMinerOfferForm] = useState({
label: '',
base_price_amount: '',
base_price_currency: 'USD',
payment_type: 'crypto',
auto_renew: false,
note: '',
is_active: true,
});
const [minerOfferModalOpen, setMinerOfferModalOpen] = useState(false);
const [purchaseMinerModalOpen, setPurchaseMinerModalOpen] = useState(false);
const [purchaseMinerForm, setPurchaseMinerForm] = useState({
offer_id: '',
@@ -1003,7 +1003,6 @@
const currentPayouts = walletTransferRows.length ? walletTransferRows : (Array.isArray(currentSettings.payouts) ? currentSettings.payouts : []);
const currentWalletWithdrawals = walletWithdrawalRows.length ? walletWithdrawalRows : (Array.isArray(currentSettings.wallet_withdrawals) ? currentSettings.wallet_withdrawals : []);
const currentWalletSnapshots = Array.isArray(payload?.wallet_snapshots) ? payload.wallet_snapshots : [];
const currentMinerOffers = Array.isArray(currentSettings.miner_offers) ? currentSettings.miner_offers : [];
const currentPurchasedMiners = Array.isArray(currentSettings.purchased_miners) ? currentSettings.purchased_miners : [];
const currentMiningCurrency = String((latest && latest.coin_currency) || currentSettings.crypto_currency || 'DOGE').toUpperCase();
const latestMeasurementDate = latest && latest.measured_at ? parseStoredUtcDate(latest.measured_at) : null;
@@ -1020,12 +1019,6 @@
: `Verwendet den letzten Upload von ${fmtDate(latest.measured_at)} mit ${fmtNumber(fullTransferCoins, 6)} ${currentMiningCurrency}. Backend erlaubt den Kompletttransfer nur, wenn dieser Upload hoechstens 3 Minuten alt ist.`;
const latestWalletSnapshot = currentWalletSnapshots.length ? currentWalletSnapshots[0] : null;
const currentWalletMiningBalance = walletAssetBalance(latestWalletSnapshot, currentMiningCurrency);
const renewableOfferIds = new Set(
currentMinerOffers
.filter((offer) => !!offer.auto_renew)
.map((offer) => Number(offer.id))
.filter((value) => Number.isFinite(value) && value > 0)
);
const preferredCurrencyCodes = Array.isArray(currentSettings.preferred_currencies)
? currentSettings.preferred_currencies.map((code) => String(code || '').toUpperCase()).filter(Boolean)
: [];
@@ -1034,8 +1027,10 @@
? currencies.filter((currency) => preferredCurrencySet.has(String(currency.code || '').toUpperCase()))
: currencies;
const selectableCurrencies = preferredSelectableCurrencies.length ? preferredSelectableCurrencies : currencies;
const evaluatedMinerOffers = Array.isArray(payload?.summary?.miner_offers) ? payload.summary.miner_offers : [];
const evaluatedMinerOffers = Array.isArray(previewMinerOffers) ? previewMinerOffers : [];
const selectedOfferType = String(minerOfferFilters.offer_type || 'crypto').toLowerCase() === 'fiat' ? 'fiat' : 'crypto';
const hasCryptoOfferBasis = Number(offerPreviewForm.crypto_base_price_amount) > 0;
const hasFiatOfferBasis = Number(offerPreviewForm.fiat_base_price_amount) > 0;
const filteredMinerOffers = evaluatedMinerOffers.filter((offer) => {
const paymentType = String(offer.payment_type || '').toLowerCase();
const speedMin = minerOfferFilters.speed_min === '' ? null : Number(minerOfferFilters.speed_min);
@@ -1111,7 +1106,7 @@
? 'crypto'
: 'fiat',
is_active: miner.is_active !== false,
can_toggle_auto_renew: Number(miner.runtime_months) > 0 && renewableOfferIds.has(Number(miner.miner_offer_id)),
can_toggle_auto_renew: Number(miner.runtime_months) > 0,
hashrate_text: formatHashrateWithBonus(miner.mining_speed_value, miner.mining_speed_unit, miner.bonus_speed_value, miner.bonus_speed_unit),
type_label: 'Aus Angebot gemietet',
})).concat(currentCostPlans.map((plan) => ({
@@ -1149,6 +1144,63 @@
}
}, [scenarioMinerOffers, selectedMinerScenarioId]);
useEffect(() => {
setOfferPreviewForm((current) => {
const cryptoCurrency = String(currentSettings.crypto_currency || 'DOGE').toUpperCase();
const selectedCurrency = String(current.crypto_base_price_currency || 'USD').toUpperCase();
if (selectedCurrency === 'USD' || selectedCurrency === cryptoCurrency) {
return current;
}
return {
...current,
crypto_base_price_currency: 'USD',
};
});
}, [currentSettings.crypto_currency]);
useEffect(() => {
const shouldPreview = selectedOfferType === 'crypto'
? Number(offerPreviewForm.crypto_base_price_amount) > 0
: Number(offerPreviewForm.fiat_base_price_amount) > 0 && Number(minerOfferFilters.speed_min) > 0;
if (!shouldPreview) {
setPreviewMinerOffers([]);
setPreviewOffersLoading(false);
return undefined;
}
const timeoutId = window.setTimeout(async () => {
setPreviewOffersLoading(true);
try {
const data = await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/offer-preview`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
crypto_base_price_amount: offerPreviewForm.crypto_base_price_amount || null,
crypto_base_price_currency: offerPreviewForm.crypto_base_price_currency || 'USD',
fiat_base_price_amount: offerPreviewForm.fiat_base_price_amount || null,
}),
});
setPreviewMinerOffers(Array.isArray(data?.miner_offers) ? data.miner_offers : []);
} catch (err) {
setPreviewMinerOffers([]);
setError(err.message);
} finally {
setPreviewOffersLoading(false);
}
}, 250);
return () => window.clearTimeout(timeoutId);
}, [
apiBase,
offerPreviewForm.crypto_base_price_amount,
offerPreviewForm.crypto_base_price_currency,
offerPreviewForm.fiat_base_price_amount,
minerOfferFilters.speed_min,
projectKey,
selectedOfferType,
]);
useEffect(() => {
if (!purchaseMinerModalOpen) {
return;
@@ -1164,7 +1216,7 @@
setPurchaseMinerForm((current) => ({
...current,
offer_id: current.offer_id || String(selectedOffer.id),
base_offer_id: current.base_offer_id || String(selectedOffer.base_offer_id || selectedOffer.id || ''),
base_offer_id: current.base_offer_id || String(selectedOffer.id || ''),
label: current.label || String(selectedOffer.label || ''),
mining_speed_value: current.mining_speed_value || String(selectedOffer.mining_speed_value || ''),
mining_speed_unit: current.mining_speed_unit || String(selectedOffer.mining_speed_unit || ''),
@@ -2110,41 +2162,6 @@
}
}
async function submitMinerOffer(event) {
event.preventDefault();
setSaving(true);
setError('');
try {
const offerPayload = {
...minerOfferForm,
base_price_currency: minerOfferForm.payment_type === 'crypto'
? (minerOfferForm.base_price_currency || 'USD')
: 'EUR',
};
await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/miner-offers`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(offerPayload),
});
setMessage('Miner-Angebot gespeichert.');
setMinerOfferForm({
label: '',
base_price_amount: '',
base_price_currency: 'USD',
payment_type: 'crypto',
auto_renew: false,
note: '',
is_active: true,
});
setMinerOfferModalOpen(false);
await loadBootstrap(projectKey);
} catch (err) {
setError(err.message);
} finally {
setSaving(false);
}
}
async function purchaseMinerOffer(offerId, overrides) {
setSaving(true);
setError('');
@@ -2192,7 +2209,8 @@
return;
}
await purchaseMinerOffer(String(selectedOffer.base_offer_id || selectedOffer.id), {
await purchaseMinerOffer(String(selectedOffer.id), {
computed_offer: selectedOffer,
label: purchaseMinerForm.label || null,
mining_speed_value: purchaseMinerForm.mining_speed_value || null,
mining_speed_unit: purchaseMinerForm.mining_speed_unit || null,
@@ -3055,15 +3073,13 @@
: 'Crypto-Angebote sind aktiv. Solange kein Wallet-Bestand erkannt wird, bleiben die Krypto-Angebote sichtbar.')
: 'Fiat-Angebote sind aktiv. Die Berechnung startet erst, wenn du eine Geschwindigkeit eingibst.',
[
h('div', { key: 'actions', className: 'mc-inline-row' }, [
h('button', {
key: 'add-offer',
type: 'button',
className: 'mc-button mc-button--secondary',
onClick: () => setMinerOfferModalOpen(true),
}, 'Basis-Angebot anlegen'),
]),
h('div', { key: 'filters', className: 'mc-filter-grid' }, [
selectedOfferType === 'crypto'
? inputField('Basispreis fuer 50 kH/s auf 3 Monate', 'number', offerPreviewForm.crypto_base_price_amount, (value) => setOfferPreviewForm({ ...offerPreviewForm, crypto_base_price_amount: value }), '0.000001')
: inputField('Basispreis fuer 100 kH/s auf 3 Monate in EUR', 'number', offerPreviewForm.fiat_base_price_amount, (value) => setOfferPreviewForm({ ...offerPreviewForm, fiat_base_price_amount: value }), '0.000001'),
selectedOfferType === 'crypto'
? selectField('Basiswaehrung', offerPreviewForm.crypto_base_price_currency || 'USD', Array.from(new Set(['USD', String(currentSettings.crypto_currency || 'DOGE').toUpperCase()])), (value) => setOfferPreviewForm({ ...offerPreviewForm, crypto_base_price_currency: value || 'USD' }))
: displayField('Basiswaehrung', 'EUR'),
selectField('Angebotsart', minerOfferFilters.offer_type, [
{ value: 'crypto', label: 'Crypto' },
{ value: 'fiat', label: 'Fiat' },
@@ -3140,7 +3156,7 @@
onClick: () => {
setPurchaseMinerForm({
offer_id: String(offer.id),
base_offer_id: String(offer.base_offer_id || offer.id || ''),
base_offer_id: String(offer.id || ''),
purchased_at: nowDateTimeLocalValue(),
label: String(offer.label || ''),
mining_speed_value: String(offer.mining_speed_value || ''),
@@ -3159,7 +3175,15 @@
}, 'Mieten'),
]),
]))
: [h('tr', { key: 'empty' }, h('td', { colSpan: 7 }, selectedOfferType === 'fiat' && minerOfferFilters.speed_min === '' ? 'Bitte zuerst eine Geschwindigkeit eingeben, damit Fiat-Angebote berechnet werden.' : 'Keine Angebote passen auf die gesetzten Filter.'))]
: [h('tr', { key: 'empty' }, h('td', { colSpan: 7 }, previewOffersLoading
? 'Berechne Angebote …'
: !hasCryptoOfferBasis && selectedOfferType === 'crypto'
? 'Bitte zuerst direkt hier den Crypto-Basispreis fuer 50 kH/s auf 3 Monate eingeben.'
: !hasFiatOfferBasis && selectedOfferType === 'fiat'
? 'Bitte zuerst direkt hier den Fiat-Basispreis fuer 100 kH/s auf 3 Monate eingeben.'
: selectedOfferType === 'fiat' && minerOfferFilters.speed_min === ''
? 'Bitte zuerst eine Geschwindigkeit eingeben, damit Fiat-Angebote berechnet werden.'
: 'Keine Angebote passen auf die gesetzten Filter.'))]
),
]),
]),
@@ -3317,38 +3341,6 @@
]),
]),
], () => setWalletWithdrawalModalOpen(false)) : null,
minerOfferModalOpen ? renderModal('Basis-Miner-Angebot anlegen', [
h('form', { key: 'form', className: 'mc-form', onSubmit: submitMinerOffer }, [
inputField('Label', 'text', minerOfferForm.label, (value) => setMinerOfferForm({ ...minerOfferForm, label: value })),
displayField('Basis-Geschwindigkeit', baseOfferSpeedLabel(minerOfferForm.payment_type)),
displayField('Basis-Laufzeit', '3 Monate'),
displayField('Ableitung', baseOfferDerivationLabel(minerOfferForm.payment_type)),
inputField(baseOfferPriceLabel(minerOfferForm.payment_type, currentSettings.crypto_currency), 'number', minerOfferForm.base_price_amount, (value) => setMinerOfferForm({ ...minerOfferForm, base_price_amount: value }), '0.000001'),
selectField('Zahlungsart', minerOfferForm.payment_type, [{ value: 'fiat', label: 'FIAT' }, { value: 'crypto', label: 'Krypto' }], (value) => setMinerOfferForm({
...minerOfferForm,
payment_type: value,
base_price_currency: value === 'crypto'
? 'USD'
: 'EUR',
})),
minerOfferForm.payment_type === 'crypto'
? selectField('Basiswaehrung', minerOfferForm.base_price_currency, Array.from(new Set(['USD', String(currentSettings.crypto_currency || 'DOGE').toUpperCase()])), (value) => setMinerOfferForm({ ...minerOfferForm, base_price_currency: value || 'USD' }))
: displayField('Basiswaehrung', 'EUR'),
h('label', { className: 'mc-checkbox' }, [
h('input', { type: 'checkbox', checked: !!minerOfferForm.auto_renew, onChange: (event) => setMinerOfferForm({ ...minerOfferForm, auto_renew: event.target.checked }) }),
'Automatische Verlängerung',
]),
textareaField('Notiz', minerOfferForm.note, (value) => setMinerOfferForm({ ...minerOfferForm, note: value })),
h('label', { className: 'mc-checkbox' }, [
h('input', { type: 'checkbox', checked: !!minerOfferForm.is_active, onChange: (event) => setMinerOfferForm({ ...minerOfferForm, is_active: event.target.checked }) }),
'Als verfuegbar markieren',
]),
h('div', { className: 'mc-inline-row' }, [
h('button', { type: 'button', className: 'mc-button mc-button--ghost', onClick: () => setMinerOfferModalOpen(false) }, 'Abbrechen'),
h('button', { type: 'submit', className: 'mc-button mc-button--secondary', disabled: saving }, saving ? 'Speichert …' : 'Angebot speichern'),
]),
]),
], () => setMinerOfferModalOpen(false)) : null,
purchaseMinerModalOpen ? renderModal('Neuen Miner mieten', [
h('form', { key: 'form', className: 'mc-form', onSubmit: submitPurchaseMiner }, [
selectField('Angebot', purchaseMinerForm.offer_id, [{ value: '', label: 'Bitte waehlen' }].concat(scenarioMinerOffers.map((offer) => ({
@@ -3358,7 +3350,7 @@
const offer = scenarioMinerOffers.find((item) => String(item.id) === String(value));
setPurchaseMinerForm({
offer_id: value,
base_offer_id: offer ? String(offer.base_offer_id || offer.id || '') : '',
base_offer_id: offer ? String(offer.id || '') : '',
purchased_at: purchaseMinerForm.purchased_at || nowDateTimeLocalValue(),
label: offer ? String(offer.label || '') : '',
mining_speed_value: offer && offer.mining_speed_value !== null && offer.mining_speed_value !== undefined ? String(offer.mining_speed_value) : '',
@@ -3395,19 +3387,7 @@
targetModalOpen ? renderModal('Ziel anlegen', [
h('form', { key: 'form', className: 'mc-form', onSubmit: submitTarget }, [
inputField('Label', 'text', targetForm.label, (value) => setTargetForm({ ...targetForm, label: value })),
selectField('Angebots-Verknuepfung', targetForm.miner_offer_id || '', [{ value: '', label: 'Kein verknuepftes Angebot' }].concat(currentMinerOffers.map((offer) => ({
value: String(offer.id),
label: `${offer.label} · ${fmtNumber(offer.base_price_amount, 6)} ${offer.base_price_currency}`,
}))), (value) => {
const offer = currentMinerOffers.find((item) => String(item.id) === String(value));
setTargetForm({
...targetForm,
miner_offer_id: value,
label: targetForm.label || offer?.label || '',
target_amount_fiat: offer ? String(offer.base_price_amount ?? offer.effective_price_amount ?? '') : targetForm.target_amount_fiat,
currency: offer?.base_price_currency || offer?.effective_price_currency || targetForm.currency,
});
}),
displayField('Angebots-Verknuepfung', 'Direkte Angebots-Links sind bei Live-Berechnung deaktiviert.'),
inputField('Betrag', 'number', targetForm.target_amount_fiat, (value) => setTargetForm({ ...targetForm, target_amount_fiat: value }), '0.01'),
selectField('Waehrung', targetForm.currency, selectableCurrencies.map((currency) => currency.code), (value) => setTargetForm({ ...targetForm, currency: value })),
inputField('Sortierung', 'number', String(targetForm.sort_order), (value) => setTargetForm({ ...targetForm, sort_order: Number(value) || 0 })),
@@ -3684,22 +3664,6 @@
return `${label ? label + ' ' : ''}${fmtNumber(value, 4)} ${unit}`;
}
function baseOfferSpeedLabel(paymentType) {
return String(paymentType || 'fiat') === 'crypto' ? '50 kH/s' : '100 kH/s';
}
function baseOfferPriceLabel(paymentType, cryptoCurrency) {
return String(paymentType || 'fiat') === 'crypto'
? `Basispreis fuer 50 kH/s auf 3 Monate in USD oder ${String(cryptoCurrency || 'DOGE').toUpperCase()}`
: 'Basispreis fuer 100 kH/s auf 3 Monate in EUR';
}
function baseOfferDerivationLabel(paymentType) {
return String(paymentType || 'fiat') === 'crypto'
? 'Varianten fuer 3, 6, 12, 18, 24 und 36 Monate mit festen Rabatten und Bonuswerten'
: 'Varianten fuer 3, 6 und 12 Monate mit festen Rabatten und Bonuswerten';
}
function formatHashrateWithBonus(speedValue, speedUnit, bonusValue, bonusUnit) {
const parts = [
formatSpeed(speedValue, speedUnit, 'Basis'),