Miner-Upgrade
This commit is contained in:
@@ -344,6 +344,7 @@
|
||||
purchased_miners: [],
|
||||
measurement_rates: [],
|
||||
},
|
||||
wallet_snapshots: Array.isArray(normalized.wallet_snapshots) ? normalized.wallet_snapshots : [],
|
||||
measurements: Array.isArray(normalized.measurements) ? normalized.measurements : [],
|
||||
targets: Array.isArray(normalized.targets) ? normalized.targets : [],
|
||||
dashboards: Array.isArray(normalized.dashboards) ? normalized.dashboards : [],
|
||||
@@ -352,7 +353,15 @@
|
||||
latest_measurement: null,
|
||||
baseline: normalized.settings || null,
|
||||
targets: Array.isArray(normalized.targets) ? normalized.targets : [],
|
||||
payouts: { total_count: 0, total_coins: 0, current_visible_coins: null, current_effective_coins: null },
|
||||
payouts: {
|
||||
total_count: 0,
|
||||
total_coins: 0,
|
||||
current_visible_coins: null,
|
||||
current_effective_coins: null,
|
||||
wallet_balances: {},
|
||||
wallet_balance_current_asset: null,
|
||||
holdings_current_asset: null,
|
||||
},
|
||||
current_hashrate_mh: null,
|
||||
miner_offers: [],
|
||||
},
|
||||
@@ -380,6 +389,7 @@
|
||||
: {};
|
||||
|
||||
return {
|
||||
kind: normalized.kind === 'wallet' ? 'wallet' : 'measurement',
|
||||
suggested: {
|
||||
measured_at: suggested.measured_at || '',
|
||||
coins_total: suggested.coins_total ?? '',
|
||||
@@ -388,6 +398,29 @@
|
||||
note: suggested.note || '',
|
||||
source: suggested.source || 'image_ocr',
|
||||
},
|
||||
suggested_wallet: normalized.suggested_wallet && typeof normalized.suggested_wallet === 'object'
|
||||
? {
|
||||
measured_at: normalized.suggested_wallet.measured_at || '',
|
||||
total_value_amount: normalized.suggested_wallet.total_value_amount ?? '',
|
||||
total_value_currency: normalized.suggested_wallet.total_value_currency || '',
|
||||
wallet_balance: normalized.suggested_wallet.wallet_balance ?? '',
|
||||
wallet_currency: normalized.suggested_wallet.wallet_currency || '',
|
||||
balances_json: normalized.suggested_wallet.balances_json && typeof normalized.suggested_wallet.balances_json === 'object'
|
||||
? normalized.suggested_wallet.balances_json
|
||||
: {},
|
||||
note: normalized.suggested_wallet.note || '',
|
||||
source: normalized.suggested_wallet.source || 'image_ocr',
|
||||
}
|
||||
: {
|
||||
measured_at: '',
|
||||
total_value_amount: '',
|
||||
total_value_currency: '',
|
||||
wallet_balance: '',
|
||||
wallet_currency: '',
|
||||
balances_json: {},
|
||||
note: '',
|
||||
source: 'image_ocr',
|
||||
},
|
||||
confidence: typeof normalized.confidence === 'number' ? normalized.confidence : 0,
|
||||
flags: Array.isArray(normalized.flags) ? normalized.flags : [],
|
||||
image_path: normalized.image_path || '',
|
||||
@@ -790,6 +823,7 @@
|
||||
];
|
||||
const currentCostPlans = Array.isArray(currentSettings.cost_plans) ? currentSettings.cost_plans : [];
|
||||
const currentPayouts = Array.isArray(currentSettings.payouts) ? currentSettings.payouts : [];
|
||||
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 renewableOfferIds = new Set(
|
||||
@@ -925,13 +959,13 @@
|
||||
setPurchaseMinerForm((current) => ({
|
||||
...current,
|
||||
offer_id: current.offer_id || String(selectedOffer.id),
|
||||
currency: current.currency || selectedOffer.effective_price_currency || selectedOffer.base_price_currency || 'USD',
|
||||
currency: current.currency || (!selectedOffer.auto_renew ? (currentSettings.crypto_currency || 'DOGE') : (selectedOffer.effective_price_currency || selectedOffer.base_price_currency || 'USD')),
|
||||
total_cost_amount: current.total_cost_amount || (selectedOffer.effective_price_amount !== null && selectedOffer.effective_price_amount !== undefined ? String(selectedOffer.effective_price_amount) : ''),
|
||||
reference_price_amount: current.reference_price_amount || (selectedOffer.reference_price_amount !== null && selectedOffer.reference_price_amount !== undefined ? String(selectedOffer.reference_price_amount) : ''),
|
||||
reference_price_currency: current.reference_price_currency || selectedOffer.reference_price_currency || '',
|
||||
auto_renew: current.auto_renew || !!selectedOffer.auto_renew,
|
||||
}));
|
||||
}, [purchaseMinerModalOpen, availableMinerOffers, purchaseMinerForm.offer_id]);
|
||||
}, [purchaseMinerModalOpen, availableMinerOffers, purchaseMinerForm.offer_id, currentSettings.crypto_currency]);
|
||||
|
||||
function measurementFxRate(measurementId, fromCurrency, toCurrency) {
|
||||
const from = String(fromCurrency || '').toUpperCase();
|
||||
@@ -1167,6 +1201,7 @@
|
||||
}, [payload, projectKey]);
|
||||
|
||||
const overviewCharts = useMemo(() => {
|
||||
const chartCoinCurrency = String(currentSettings.crypto_currency || 'DOGE').toUpperCase();
|
||||
const comparisonRows = measurements.filter((row) => {
|
||||
const miningRate = Number(row.doge_per_hour_per_mh_interval);
|
||||
const price = Number(row.effective_price_per_coin ?? row.price_per_coin);
|
||||
@@ -1185,7 +1220,7 @@
|
||||
miningVsPrice: baseMining && basePrice ? [
|
||||
{
|
||||
key: 'mining-rate',
|
||||
label: 'Mining/h je MH/s Index',
|
||||
label: `${chartCoinCurrency}/h je MH/s Index`,
|
||||
color: '#2dd4bf',
|
||||
data: comparisonRows.map((row) => ({
|
||||
x: fmtDate(row.measured_at),
|
||||
@@ -1194,7 +1229,7 @@
|
||||
},
|
||||
{
|
||||
key: 'doge-price',
|
||||
label: 'DOGE-Kurs Index',
|
||||
label: `${chartCoinCurrency}-Kurs Index`,
|
||||
color: '#f59e0b',
|
||||
data: comparisonRows.map((row) => ({
|
||||
x: fmtDate(row.measured_at),
|
||||
@@ -1203,7 +1238,7 @@
|
||||
},
|
||||
] : [],
|
||||
};
|
||||
}, [measurements]);
|
||||
}, [currentSettings.crypto_currency, measurements]);
|
||||
|
||||
async function submitMeasurement(fromPreview) {
|
||||
const preview = normalizeOcrPreview(ocrPreview);
|
||||
@@ -1242,6 +1277,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function submitWalletSnapshotFromPreview() {
|
||||
const preview = normalizeOcrPreview(ocrPreview);
|
||||
const raw = {
|
||||
...preview.suggested_wallet,
|
||||
image_path: preview.image_path,
|
||||
ocr_raw_text: preview.raw_text,
|
||||
ocr_confidence: preview.confidence,
|
||||
ocr_flags: preview.flags,
|
||||
};
|
||||
|
||||
setSaving(true);
|
||||
setError('');
|
||||
setMessage('');
|
||||
try {
|
||||
await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/wallet-snapshots`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(raw),
|
||||
});
|
||||
setMessage('Wallet-Snapshot gespeichert.');
|
||||
setOcrPreview(null);
|
||||
await loadBootstrap(projectKey);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteMeasurement(id) {
|
||||
if (!id) {
|
||||
return;
|
||||
@@ -1320,6 +1384,7 @@
|
||||
body.append('image', nextForm.image);
|
||||
body.append('date_context', nextForm.date_context);
|
||||
body.append('ocr_hint_text', nextForm.ocr_hint_text);
|
||||
body.append('wallet_currency_hint', currentSettings.crypto_currency || 'DOGE');
|
||||
const data = await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/ocr-preview`, {
|
||||
method: 'POST',
|
||||
body,
|
||||
@@ -1534,7 +1599,7 @@
|
||||
body: JSON.stringify(payoutForm),
|
||||
});
|
||||
setMessage('Auszahlung gespeichert.');
|
||||
setPayoutForm({ payout_at: '', coins_amount: '', payout_currency: 'DOGE', note: '' });
|
||||
setPayoutForm({ payout_at: '', coins_amount: '', payout_currency: currentSettings.crypto_currency || 'DOGE', note: '' });
|
||||
setPayoutModalOpen(false);
|
||||
await loadBootstrap(projectKey);
|
||||
} catch (err) {
|
||||
@@ -1792,6 +1857,75 @@
|
||||
setReportCurrencyOverride('');
|
||||
setCookie('mining_checker_report_currency', '', 0);
|
||||
}
|
||||
|
||||
function renderSharedOcrPanel() {
|
||||
const preview = normalizeOcrPreview(ocrPreview);
|
||||
const isWalletPreview = preview.kind === 'wallet';
|
||||
const hasUsableOcrSuggestion = isWalletPreview
|
||||
? (preview.suggested_wallet.wallet_balance !== '' && preview.suggested_wallet.wallet_balance !== null)
|
||||
: (preview.suggested.coins_total !== '' && preview.suggested.coins_total !== null);
|
||||
const ocrStatus = getOcrStatusMessage(preview);
|
||||
|
||||
return panel('OCR Upload', 'Screenshot auswaehlen, Ergebnis direkt pruefen und speichern.', [
|
||||
h('div', {
|
||||
key: 'ocr-form',
|
||||
className: 'mc-form',
|
||||
}, [
|
||||
fileField('Screenshot', (file) => loadOcrPreview(file, { image: file })),
|
||||
]),
|
||||
saving && ocrForm.image
|
||||
? h('div', { key: 'ocr-loading', className: 'mc-empty' }, 'Analysiere Screenshot …')
|
||||
: null,
|
||||
ocrPreview
|
||||
? h('div', { key: 'ocr-preview', className: 'mc-form' }, [
|
||||
h('div', { key: 'badges', className: 'mc-inline-row' }, [
|
||||
h(Badge, { key: 'kind', tone: isWalletPreview ? 'info' : 'success' }, isWalletPreview ? 'Wallet' : 'Mining'),
|
||||
h(Badge, { key: 'confidence', tone: preview.confidence >= 0.75 ? 'success' : 'warn' }, `confidence ${fmtNumber(preview.confidence, 4)}`),
|
||||
]),
|
||||
isWalletPreview
|
||||
? h('div', { key: 'wallet-form', className: 'mc-two-col' }, [
|
||||
displayField('Erkannter Typ', 'Wallet-Snapshot'),
|
||||
displayField('Mining-Waehrung im Wallet', `${fmtNumber(preview.suggested_wallet.wallet_balance, 8)} ${preview.suggested_wallet.wallet_currency || currentSettings.crypto_currency || 'DOGE'}`),
|
||||
displayField('Gesamtwert', preview.suggested_wallet.total_value_amount !== '' && preview.suggested_wallet.total_value_amount !== null
|
||||
? `${fmtNumber(preview.suggested_wallet.total_value_amount, 4)} ${preview.suggested_wallet.total_value_currency || ''}`.trim()
|
||||
: 'n/a'),
|
||||
displayField('Erkannte Wallet-Assets', Object.entries(preview.suggested_wallet.balances_json || {})
|
||||
.slice(0, 8)
|
||||
.map(([code, amount]) => `${fmtNumber(amount, 8)} ${code}`)
|
||||
.join(' · ') || 'n/a'),
|
||||
])
|
||||
: h('div', { key: 'measurement-form', className: 'mc-two-col' }, [
|
||||
displayField('Erkannter Typ', 'Mining-Messpunkt'),
|
||||
displayField('Datum/Zeit', 'Wird beim Speichern auf den aktuellen Bestätigungszeitpunkt gesetzt.'),
|
||||
displayField('Coins total', fmtNumber(preview.suggested.coins_total, 6)),
|
||||
displayField('Kurs', fmtNumber(preview.suggested.price_per_coin, 6)),
|
||||
displayField('Waehrung', preview.suggested.price_currency || 'n/a'),
|
||||
]),
|
||||
ocrStatus
|
||||
? h('div', {
|
||||
key: 'ocr-status',
|
||||
className: cx(
|
||||
'mc-alert',
|
||||
ocrStatus.tone === 'error' ? 'mc-alert--error' : 'mc-alert--warning'
|
||||
),
|
||||
}, ocrStatus.text)
|
||||
: null,
|
||||
!hasUsableOcrSuggestion
|
||||
? h('div', { key: 'ocr-warning', className: 'mc-alert mc-alert--error' },
|
||||
'Kein verwertbarer OCR-Vorschlag erkannt. Bitte Bild erneut hochladen oder die Daten manuell erfassen.')
|
||||
: null,
|
||||
h('button', {
|
||||
key: 'confirm',
|
||||
type: 'button',
|
||||
className: 'mc-button mc-button--primary',
|
||||
onClick: () => isWalletPreview ? submitWalletSnapshotFromPreview() : submitMeasurement(true),
|
||||
disabled: saving || !hasUsableOcrSuggestion,
|
||||
}, saving ? 'Speichert …' : 'Ergebnis speichern'),
|
||||
])
|
||||
: h('div', { key: 'ocr-empty', className: 'mc-empty' },
|
||||
'Noch kein Screenshot ausgewaehlt.'),
|
||||
]);
|
||||
}
|
||||
return h('div', {
|
||||
className: 'mc-grid-bg',
|
||||
}, [
|
||||
@@ -1804,6 +1938,9 @@
|
||||
]);
|
||||
|
||||
function renderTab() {
|
||||
const currentCoinCurrency = String((latest && latest.coin_currency) || currentSettings.crypto_currency || 'DOGE').toUpperCase();
|
||||
const perDayLabel = `${currentCoinCurrency} pro Tag`;
|
||||
|
||||
if (activeTab === 'overview') {
|
||||
const latestValue = latest ? convertMeasurementMoney(latest, latest.current_value, reportCurrency) : null;
|
||||
const latestPriceSource = latest && latest.effective_price_per_coin !== null && latest.effective_price_per_coin !== undefined
|
||||
@@ -1822,18 +1959,14 @@
|
||||
const breakEvenDaysOverall = latest && latest.break_even_days_overall !== null && latest.break_even_days_overall !== undefined
|
||||
? Number(latest.break_even_days_overall)
|
||||
: null;
|
||||
const investedCapital = latest ? convertMeasurementMoney(latest, latest.invested_capital, reportCurrency) : null;
|
||||
const investedCapital = latest ? convertMeasurementMoney(latest, latest.cash_invested_capital ?? latest.invested_capital, reportCurrency) : null;
|
||||
const reinvestedCapital = latest ? convertMeasurementMoney(latest, latest.reinvested_capital, reportCurrency) : null;
|
||||
const walletValue = latest ? convertMeasurementMoney(latest, latest.wallet_value, reportCurrency) : null;
|
||||
const totalHoldingsValue = latest ? convertMeasurementMoney(latest, latest.total_holdings_value, reportCurrency) : null;
|
||||
const breakEvenReached = breakEvenRemainingAmount !== null && breakEvenRemainingAmount <= 0;
|
||||
const breakEvenDate = (() => {
|
||||
if (!latest || breakEvenDaysOverall === null || !Number.isFinite(breakEvenDaysOverall)) {
|
||||
return null;
|
||||
}
|
||||
const baseTimestamp = Date.parse(String(latest.measured_at || ''));
|
||||
if (!Number.isFinite(baseTimestamp)) {
|
||||
return null;
|
||||
}
|
||||
return new Date(baseTimestamp + (breakEvenDaysOverall * 24 * 60 * 60 * 1000));
|
||||
})();
|
||||
const breakEvenEta = latest && latest.break_even_eta_at ? fmtDate(latest.break_even_eta_at) : null;
|
||||
const walletBalanceCurrentAsset = payload?.summary?.payouts?.wallet_balance_current_asset;
|
||||
const holdingsCurrentAsset = payload?.summary?.payouts?.holdings_current_asset;
|
||||
|
||||
return h('div', { className: 'mc-stack' }, [
|
||||
panel('Berichtswährung', 'Bestimmt die Währung für Kennzahlen im Überblick. Standard kommt aus den Settings, diese Auswahl gilt nur für den aktuellen Besuch.', [
|
||||
@@ -1855,19 +1988,21 @@
|
||||
h('div', { key: 'stats', className: 'mc-stats-grid' }, [
|
||||
h(StatCard, {
|
||||
key: 'coins',
|
||||
label: 'Coins sichtbar',
|
||||
label: `${currentCoinCurrency} im Miner`,
|
||||
value: latest ? fmtNumber(latest.coins_total_visible || latest.coins_total, 6) : 'n/a',
|
||||
sub: latest ? `Stand ${fmtDate(latest.measured_at)}` : '',
|
||||
}),
|
||||
h(StatCard, {
|
||||
key: 'coins-effective',
|
||||
label: 'Coins effektiv',
|
||||
value: payload?.summary?.payouts ? fmtNumber(payload.summary.payouts.current_effective_coins, 6) : 'n/a',
|
||||
sub: payload?.summary?.payouts ? `Ausgezahlt ${fmtNumber(payload.summary.payouts.total_coins, 6)} DOGE` : '',
|
||||
key: 'holdings',
|
||||
label: `Theoretischer Bestand ${currentCoinCurrency}`,
|
||||
value: payload?.summary?.payouts ? fmtNumber(holdingsCurrentAsset, 6) : 'n/a',
|
||||
sub: payload?.summary?.payouts
|
||||
? `Wallet ${fmtNumber(walletBalanceCurrentAsset, 6)} ${currentCoinCurrency} · Miner ${fmtNumber(latest?.coins_total_visible || latest?.coins_total, 6)} ${currentCoinCurrency}`
|
||||
: '',
|
||||
}),
|
||||
h(StatCard, {
|
||||
key: 'perday',
|
||||
label: 'DOGE pro Tag',
|
||||
label: perDayLabel,
|
||||
value: latest ? fmtNumber(latest.doge_per_day_interval, 4) : 'n/a',
|
||||
sub: payload?.summary?.current_hashrate_mh ? `Hashrate ${fmtNumber(payload.summary.current_hashrate_mh, 4)} MH/s` : (latest ? `Trend ${latest.trend_label}` : ''),
|
||||
}),
|
||||
@@ -1884,17 +2019,19 @@
|
||||
label: 'Theoretischer Tagesgewinn',
|
||||
value: dailyProfit !== null ? fmtMoney(dailyProfit, reportCurrency) : 'n/a',
|
||||
sub: dailyCost !== null
|
||||
? `Tageskosten ${fmtMoney(dailyCost, reportCurrency)} · Break-even ${breakEvenPrice !== null ? `${fmtNumber(breakEvenPrice, 6)} ${reportCurrency}` : 'n/a'}`
|
||||
? `Tageskosten ${fmtMoney(dailyCost, reportCurrency)} · Walletwert ${walletValue !== null ? fmtMoney(walletValue, reportCurrency) : 'n/a'}`
|
||||
: 'Kein aktiver Miner fuer diese Waehrung',
|
||||
}),
|
||||
h(StatCard, {
|
||||
key: 'break-even-point',
|
||||
label: 'Break-even',
|
||||
value: breakEvenDaysOverall !== null
|
||||
label: 'Cash-Break-even',
|
||||
value: breakEvenReached
|
||||
? 'Erreicht'
|
||||
: breakEvenDaysOverall !== null
|
||||
? `${fmtNumber(breakEvenDaysOverall, 2)} Tage`
|
||||
: (breakEvenReached ? 'Erreicht' : (investedCapital === null ? 'Keine Mietbasis' : 'n/a')),
|
||||
: (investedCapital === null ? 'Keine Mietbasis' : 'Nicht erreichbar'),
|
||||
sub: investedCapital !== null
|
||||
? `${breakEvenDate ? `Theoretisch ${fmtDate(breakEvenDate.toISOString())} · ` : ''}Basis ${fmtMoney(investedCapital, reportCurrency)}${dailyRevenue !== null ? ` · Tagesumsatz ${fmtMoney(dailyRevenue, reportCurrency)}` : ''}`
|
||||
? `${breakEvenEta ? `ETA ${breakEvenEta} · ` : ''}Cash ${fmtMoney(investedCapital, reportCurrency)}${reinvestedCapital !== null ? ` · Reinvest ${fmtMoney(reinvestedCapital, reportCurrency)}` : ''}${totalHoldingsValue !== null ? ` · Bestand ${fmtMoney(totalHoldingsValue, reportCurrency)}` : ''}`
|
||||
: (breakEvenPrice !== null
|
||||
? `Break-even-Kurs ${fmtNumber(breakEvenPrice, 6)} ${reportCurrency}`
|
||||
: (investedCapital === null
|
||||
@@ -1904,14 +2041,14 @@
|
||||
]),
|
||||
h('div', { key: 'charts', className: 'mc-overview-grid' }, [
|
||||
panel('Mining-Verlauf', 'Coins total der letzten 15 Tage.', h(SimpleChart, { type: 'line', data: overviewCharts.mining })),
|
||||
panel('Performance-Verlauf', 'DOGE-pro-Tag-Raten der letzten 15 Tage.', h(SimpleChart, { type: 'area', data: overviewCharts.performance })),
|
||||
panel('Performance-Verlauf', `${perDayLabel}-Raten der letzten 15 Tage.`, h(SimpleChart, { type: 'area', data: overviewCharts.performance })),
|
||||
panel('Kurs-Verlauf', 'Preiswerte der letzten 15 Tage.', h(SimpleChart, { type: 'line', data: overviewCharts.pricing })),
|
||||
panel('Mining vs. Kurs', 'Index-Vergleich der letzten 15 Tage. Mining wird auf DOGE pro Stunde je MH/s normalisiert, beide Reihen starten bei 100.', h(SimpleChart, {
|
||||
panel('Mining vs. Kurs', `Index-Vergleich der letzten 15 Tage. Mining wird auf ${currentCoinCurrency} pro Stunde je MH/s normalisiert, beide Reihen starten bei 100.`, h(SimpleChart, {
|
||||
type: 'line',
|
||||
series: overviewCharts.miningVsPrice,
|
||||
})),
|
||||
]),
|
||||
panel('Zielmonitor', 'Rest-DOGE und Resttage werden gegen den letzten verfuegbaren Kurs je Zielwaehrung berechnet.',
|
||||
panel('Zielmonitor', `Rest-${currentCoinCurrency} und Resttage werden gegen den letzten verfuegbaren Kurs je Zielwaehrung berechnet.`,
|
||||
h('div', { className: 'mc-target-grid' },
|
||||
currentTargets.map((target, index) => h('div', {
|
||||
key: index,
|
||||
@@ -1924,8 +2061,8 @@
|
||||
h('div', { key: 'body', className: 'mc-text mc-target-grid' }, [
|
||||
h('div', { key: 'amount' }, `Ziel: ${fmtMoney(target.target_amount_fiat, target.currency)}`),
|
||||
h('div', { key: 'price' }, `Letzter Kurs: ${target.latest_price_for_currency ? fmtNumber(target.latest_price_for_currency, 6) + ' ' + target.currency : 'n/a'}`),
|
||||
h('div', { key: 'doge' }, `Benoetigte DOGE: ${fmtNumber(target.required_doge, 6)}`),
|
||||
h('div', { key: 'remaining' }, `Rest-DOGE: ${fmtNumber(target.remaining_doge, 6)}`),
|
||||
h('div', { key: 'doge' }, `Benoetigte ${currentCoinCurrency}: ${fmtNumber(target.required_doge, 6)}`),
|
||||
h('div', { key: 'remaining' }, `Rest-${currentCoinCurrency}: ${fmtNumber(target.remaining_doge, 6)}`),
|
||||
h('div', { key: 'days' }, `Resttage: ${fmtNumber(target.remaining_days, 4)}`),
|
||||
]),
|
||||
]))
|
||||
@@ -1937,56 +2074,7 @@
|
||||
if (activeTab === 'measurements') {
|
||||
return h('div', { className: 'mc-main-grid' }, [
|
||||
h('div', { className: 'mc-stack' }, [
|
||||
(function () {
|
||||
const preview = normalizeOcrPreview(ocrPreview);
|
||||
const hasUsableOcrSuggestion = preview.suggested.coins_total !== '' && preview.suggested.coins_total !== null;
|
||||
const ocrStatus = getOcrStatusMessage(preview);
|
||||
return panel('OCR Upload', 'Screenshot auswaehlen, Ergebnis direkt pruefen und speichern.', [
|
||||
h('div', {
|
||||
key: 'ocr-form',
|
||||
className: 'mc-form',
|
||||
}, [
|
||||
fileField('Screenshot', (file) => loadOcrPreview(file, { image: file })),
|
||||
]),
|
||||
saving && ocrForm.image
|
||||
? h('div', { key: 'ocr-loading', className: 'mc-empty' }, 'Analysiere Screenshot …')
|
||||
: null,
|
||||
ocrPreview
|
||||
? h('div', { key: 'ocr-preview', className: 'mc-form' }, [
|
||||
h('div', { key: 'badges', className: 'mc-inline-row' }, [
|
||||
h(Badge, { key: 'confidence', tone: preview.confidence >= 0.75 ? 'success' : 'warn' }, `confidence ${fmtNumber(preview.confidence, 4)}`),
|
||||
]),
|
||||
h('div', { key: 'form', className: 'mc-two-col' }, [
|
||||
displayField('Datum/Zeit', 'Wird beim Speichern auf den aktuellen Bestätigungszeitpunkt gesetzt.'),
|
||||
displayField('Coins total', fmtNumber(preview.suggested.coins_total, 6)),
|
||||
displayField('Kurs', fmtNumber(preview.suggested.price_per_coin, 6)),
|
||||
displayField('Waehrung', preview.suggested.price_currency || 'n/a'),
|
||||
]),
|
||||
ocrStatus
|
||||
? h('div', {
|
||||
key: 'ocr-status',
|
||||
className: cx(
|
||||
'mc-alert',
|
||||
ocrStatus.tone === 'error' ? 'mc-alert--error' : 'mc-alert--warning'
|
||||
),
|
||||
}, ocrStatus.text)
|
||||
: null,
|
||||
!hasUsableOcrSuggestion
|
||||
? h('div', { key: 'ocr-warning', className: 'mc-alert mc-alert--error' },
|
||||
'Kein verwertbarer OCR-Vorschlag erkannt. Bitte Bild erneut hochladen oder den Messpunkt manuell erfassen.')
|
||||
: null,
|
||||
h('button', {
|
||||
key: 'confirm',
|
||||
type: 'button',
|
||||
className: 'mc-button mc-button--primary',
|
||||
onClick: () => submitMeasurement(true),
|
||||
disabled: saving || !hasUsableOcrSuggestion,
|
||||
}, saving ? 'Speichert …' : 'Ergebnis speichern'),
|
||||
])
|
||||
: h('div', { key: 'ocr-empty', className: 'mc-empty' },
|
||||
'Noch kein Screenshot ausgewaehlt.'),
|
||||
]);
|
||||
})(),
|
||||
renderSharedOcrPanel(),
|
||||
panel('Messpunkt manuell erfassen', 'Direkte Eingabe eines einzelnen Messpunkts mit serverseitiger Validierung.', h('form', {
|
||||
className: 'mc-form',
|
||||
onSubmit: function (event) {
|
||||
@@ -2068,12 +2156,12 @@
|
||||
panel('Messhistorie', 'Die letzten 10 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', 'DOGE/Tag', 'Trend', 'Notiz', 'Aktion'
|
||||
'Zeit', 'Coins', 'Kurs', 'Quelle', perDayLabel, '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 }, [
|
||||
h('td', { key: 'measured' }, fmtDate(row.measured_at)),
|
||||
h('td', { key: 'coins' }, fmtNumber(row.coins_total, 6)),
|
||||
h('td', { key: 'coins' }, `${fmtNumber(row.coins_total, 6)} ${row.coin_currency || currentCoinCurrency}`),
|
||||
h('td', { key: 'price' }, row.price_per_coin ? `${fmtNumber(row.price_per_coin, 6)} ${row.price_currency}` : 'n/a'),
|
||||
h('td', { key: 'source' }, row.source),
|
||||
h('td', { key: 'rate' }, fmtNumber(row.doge_per_day_interval, 4)),
|
||||
@@ -2092,6 +2180,33 @@
|
||||
]);
|
||||
}
|
||||
|
||||
if (activeTab === 'wallet') {
|
||||
return h('div', { className: 'mc-main-grid' }, [
|
||||
h('div', { className: 'mc-stack' }, [
|
||||
renderSharedOcrPanel(),
|
||||
panel('Wallet-Historie', `Erkannte Wallet-Snapshots. Fuer Reinvestitionen ist aktuell ${currentSettings.crypto_currency || 'DOGE'} entscheidend.`, h('div', { className: 'mc-table-shell' }, [
|
||||
h('table', { key: 'wallet-table', className: 'mc-table' }, [
|
||||
h('thead', { key: 'thead' }, h('tr', null, [
|
||||
'Zeit', 'Mining-Waehrung', 'Bestand', 'Gesamtwert', 'Quelle', 'Assets'
|
||||
].map((label) => h('th', { key: label }, label)))),
|
||||
h('tbody', { key: 'tbody' },
|
||||
currentWalletSnapshots.length
|
||||
? currentWalletSnapshots.map((row) => h('tr', { key: row.id }, [
|
||||
h('td', { key: 'measured' }, fmtDate(row.measured_at)),
|
||||
h('td', { key: 'currency' }, row.wallet_currency || currentSettings.crypto_currency || 'DOGE'),
|
||||
h('td', { key: 'balance' }, row.wallet_balance !== null && row.wallet_balance !== undefined ? `${fmtNumber(row.wallet_balance, 8)} ${row.wallet_currency || ''}`.trim() : 'n/a'),
|
||||
h('td', { key: 'value' }, row.total_value_amount !== null && row.total_value_amount !== undefined ? `${fmtNumber(row.total_value_amount, 4)} ${row.total_value_currency || ''}`.trim() : 'n/a'),
|
||||
h('td', { key: 'source' }, row.source || 'manual'),
|
||||
h('td', { key: 'assets' }, Object.entries(row.balances_json || {}).slice(0, 6).map(([code, amount]) => `${fmtNumber(amount, 8)} ${code}`).join(' · ') || 'n/a'),
|
||||
]))
|
||||
: [h('tr', { key: 'empty' }, h('td', { colSpan: 6 }, 'Noch keine Wallet-Snapshots gespeichert.'))]
|
||||
),
|
||||
]),
|
||||
])),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
if (activeTab === 'dashboards') {
|
||||
return h('div', { className: 'mc-main-grid' }, [
|
||||
panel('Dashboard-Builder V1', 'Chart-Typ, X/Y-Feld, Aggregation und einfache Filter werden gespeichert.', h('form', {
|
||||
@@ -2194,7 +2309,15 @@
|
||||
key: 'add-payout',
|
||||
type: 'button',
|
||||
className: 'mc-button mc-button--secondary',
|
||||
onClick: () => setPayoutModalOpen(true),
|
||||
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' }, [
|
||||
@@ -2254,7 +2377,7 @@
|
||||
])
|
||||
: null,
|
||||
]),
|
||||
h('td', { key: 'day' }, offer.expected_doge_per_day !== null ? `${fmtNumber(offer.expected_doge_per_day, 6)} DOGE` : 'n/a'),
|
||||
h('td', { key: 'day' }, offer.expected_doge_per_day !== null ? `${fmtNumber(offer.expected_doge_per_day, 6)} ${currentCoinCurrency}` : 'n/a'),
|
||||
h('td', { key: 'break' }, offer.break_even_days !== null ? `${fmtNumber(offer.break_even_days, 2)} Tage` : 'n/a'),
|
||||
h('td', { key: 'rec' }, [
|
||||
h('div', { key: 'rec-main' }, offer.recommendation),
|
||||
@@ -2326,11 +2449,11 @@
|
||||
}),
|
||||
h(StatCard, {
|
||||
key: 'scenario-doge',
|
||||
label: 'DOGE pro Tag Neu',
|
||||
label: `${currentCoinCurrency} pro Tag Neu`,
|
||||
value: selectedMinerScenario.scenario_doge_per_day !== null ? fmtNumber(selectedMinerScenario.scenario_doge_per_day, 4) : 'n/a',
|
||||
sub: selectedMinerScenario.scenario_current_doge_per_day !== null
|
||||
? `Aktuell ${fmtNumber(selectedMinerScenario.scenario_current_doge_per_day, 4)}`
|
||||
: 'Keine aktuelle DOGE/Tag-Basis',
|
||||
: `Keine aktuelle ${currentCoinCurrency}/Tag-Basis`,
|
||||
}),
|
||||
h(StatCard, {
|
||||
key: 'scenario-break-even',
|
||||
@@ -2445,7 +2568,7 @@
|
||||
h('form', { key: 'form', className: 'mc-form', onSubmit: submitPayout }, [
|
||||
inputField('Auszahlungszeitpunkt', '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, ['DOGE'].concat(selectableCurrencies.map((currency) => currency.code).filter((code) => code !== 'DOGE')), (value) => setPayoutForm({ ...payoutForm, payout_currency: value })),
|
||||
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'),
|
||||
|
||||
Reference in New Issue
Block a user