(function () { window.initMiningCheckerApp = function initMiningCheckerApp(rootNode, options) { const root = rootNode || document.getElementById('mining-checker-app'); if (!root || !window.React || !window.ReactDOM) { return; } if (root.dataset.moduleInitialized === '1') { return; } root.dataset.moduleInitialized = '1'; const React = window.React; const ReactDOM = window.ReactDOM; const h = React.createElement; const { useEffect, useMemo, useRef, useState } = React; const normalizedOptions = options && typeof options === 'object' ? options : {}; const apiBase = normalizedOptions.apiBase || root.dataset.apiBase || '/api/mining-checker/index.php?path=v1'; const initialProjectKey = normalizedOptions.defaultProjectKey || root.dataset.defaultProjectKey || 'doge-main'; const initialActiveTab = String(normalizedOptions.activeView || root.dataset.activeView || 'overview').trim() || 'overview'; const configuredSections = (() => { const optionSections = Array.isArray(normalizedOptions.sections) ? normalizedOptions.sections : null; if (optionSections) { return optionSections .map((section) => { const key = section && typeof section.key === 'string' ? section.key.trim() : ''; const label = section && typeof section.label === 'string' ? section.label.trim() : ''; return key && label ? [key, label] : null; }) .filter(Boolean); } try { const parsed = JSON.parse(root.dataset.sectionsJson || '[]'); if (!Array.isArray(parsed)) { return []; } return parsed .map((section) => { const key = section && typeof section.key === 'string' ? section.key.trim() : ''; const label = section && typeof section.label === 'string' ? section.label.trim() : ''; return key && label ? [key, label] : null; }) .filter(Boolean); } catch (error) { return []; } })(); const desktopDebugBus = window.__desktopDebugBus && typeof window.__desktopDebugBus === 'object' ? window.__desktopDebugBus : null; const initialDebugMode = (desktopDebugBus && desktopDebugBus.enabled === true) || normalizedOptions.moduleDebugEnabled === true || (document.body && document.body.dataset.moduleDebugEnabled === '1'); const fallbackSections = [ ['overview', 'Übersicht'], ['recovery', 'Nachtragen'], ['upload', 'Upload'], ['measurements', 'Mining-History'], ['wallet', 'Wallet'], ['mining', 'Miner-Daten'], ['dashboards', 'Dashboards'], ['settings', 'Settings'], ]; const sectionEntries = (() => { const ordered = []; const seen = new Set(); const register = (key, label) => { const normalizedKey = String(key || '').trim(); const normalizedLabel = String(label || '').trim(); if (!normalizedKey || !normalizedLabel || seen.has(normalizedKey)) { return; } seen.add(normalizedKey); ordered.push([normalizedKey, normalizedLabel]); }; configuredSections.forEach((entry) => register(entry[0], entry[1])); fallbackSections.forEach((entry) => register(entry[0], entry[1])); return ordered; })(); const sectionMap = new Map(sectionEntries); function getCookie(name) { const pattern = `; ${document.cookie}`; const parts = pattern.split(`; ${name}=`); if (parts.length < 2) { return ''; } return decodeURIComponent(parts.pop().split(';').shift() || ''); } function setCookie(name, value, maxAgeSeconds) { document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAgeSeconds}; samesite=lax`; } const CRYPTO_RUNTIME_CONFIGS = { 3: { discountPercent: 0, bonusPercent: 5 }, 6: { discountPercent: 10, bonusPercent: 10 }, 12: { discountPercent: 35, bonusPercent: 10 }, 18: { discountPercent: 45, bonusPercent: 15 }, 24: { discountPercent: 55, bonusPercent: 20 }, 36: { discountPercent: 65, bonusPercent: 20 }, }; const debugBus = desktopDebugBus || window.__nexusDebugBus || { enabled: initialDebugMode, listener: null, sequence: 0, }; window.__desktopDebugBus = debugBus; window.__nexusDebugBus = debugBus; const browserTimezone = (() => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone || 'Europe/Berlin'; } catch (_error) { return 'Europe/Berlin'; } })(); function emitDebug(entry) { if (typeof debugBus.emit === 'function') { return debugBus.emit(entry); } debugBus.sequence += 1; const payload = { id: debugBus.sequence, time: new Date().toISOString(), ...entry, }; if (typeof debugBus.listener === 'function') { debugBus.listener(payload); } } function emitServerTraceEntries(trace, meta) { if (!Array.isArray(trace) || !trace.length) { emitDebug({ type: meta && meta.type ? meta.type : 'server:trace-empty', source: meta && meta.source ? meta.source : null, message: 'Keine Server-Debug-Eintraege vorhanden.', }); return; } trace.forEach((item) => { emitDebug({ type: `server:${item.event || 'trace'}`, source: meta && meta.source ? meta.source : null, server_time: item.time || null, ...(item.context && typeof item.context === 'object' ? item.context : {}), }); }); } async function loadLatestDebugTrace() { try { const response = await fetch(`${apiBase}/debug/latest`, { headers: { 'X-Mining-Debug': '1' }, }); const payload = await response.json().catch(() => ({})); if (payload && payload.data && Array.isArray(payload.data.entries)) { emitServerTraceEntries(payload.data.entries, { type: 'server:trace-latest', source: payload.data.file || null, }); } } catch (error) { emitDebug({ type: 'server:trace-latest-error', message: error && error.message ? error.message : 'Latest-Debug konnte nicht geladen werden', }); } } function cx() { return Array.from(arguments).filter(Boolean).join(' '); } function fmtNumber(value, digits) { if (value === null || value === undefined || value === '') { return 'n/a'; } return Number(value).toLocaleString('de-DE', { minimumFractionDigits: 0, maximumFractionDigits: digits === undefined ? 6 : digits, }); } function fmtMoney(value, currency) { if (value === null || value === undefined || !currency) { return 'n/a'; } return new Intl.NumberFormat('de-DE', { style: 'currency', currency, maximumFractionDigits: 4, }).format(Number(value)); } function calendarRuntimeEnd(startAt, runtimeMonths) { const start = parseStoredUtcDate(startAt); const months = Number(runtimeMonths); if (!start || !Number.isInteger(months) || months <= 0) { return null; } const targetMonthIndex = (start.getUTCFullYear() * 12) + start.getUTCMonth() + months; const targetYear = Math.floor(targetMonthIndex / 12); const targetMonth = targetMonthIndex % 12; const lastDayOfTargetMonth = new Date(Date.UTC(targetYear, targetMonth + 1, 0)).getUTCDate(); return new Date(Date.UTC( targetYear, targetMonth, Math.min(start.getUTCDate(), lastDayOfTargetMonth), start.getUTCHours(), start.getUTCMinutes(), start.getUTCSeconds(), start.getUTCMilliseconds() )); } function runtimeDaysForEntry(startAt, runtimeMonths) { const start = parseStoredUtcDate(startAt); const end = calendarRuntimeEnd(startAt, runtimeMonths); if (!start || !end) { return null; } return (end.getTime() - start.getTime()) / 86400000; } function deriveDailyCostAmount(totalCostAmount, runtimeMonths, startsAt) { const amount = Number(totalCostAmount); const runtimeDays = runtimeDaysForEntry(startsAt, runtimeMonths); if (!Number.isFinite(amount) || !Number.isFinite(runtimeDays) || runtimeDays <= 0) { return null; } return amount / runtimeDays; } function toKhPerSecond(value, unit) { const numericValue = Number(value); const normalizedUnit = String(unit || '').trim(); if (!Number.isFinite(numericValue) || numericValue <= 0 || !normalizedUnit) { return 0; } if (normalizedUnit === 'MH/s') { return numericValue * 1000; } if (normalizedUnit === 'kH/s') { return numericValue; } return 0; } function recentMeasurementWindow(rows, windowDays) { if (!Array.isArray(rows) || rows.length === 0) { return []; } const normalizedWindowDays = Number(windowDays); if (!Number.isFinite(normalizedWindowDays) || normalizedWindowDays <= 0) { return rows; } const sortedRows = rows .filter((row) => row && row.measured_at) .slice() .sort((left, right) => new Date(left.measured_at).getTime() - new Date(right.measured_at).getTime()); if (sortedRows.length === 0) { return rows; } const latestTs = new Date(sortedRows[sortedRows.length - 1].measured_at).getTime(); if (!Number.isFinite(latestTs)) { return sortedRows; } const minTs = latestTs - (normalizedWindowDays * 86400 * 1000); const filteredRows = sortedRows.filter((row) => { const measuredTs = new Date(row.measured_at).getTime(); return Number.isFinite(measuredTs) && measuredTs >= minTs; }); return filteredRows.length ? filteredRows : [sortedRows[sortedRows.length - 1]]; } function parseStoredUtcDate(value) { const raw = String(value || '').trim(); if (!raw) { return null; } let normalized = raw.replace(' ', 'T'); if (/^\d{4}-\d{2}-\d{2}$/.test(normalized)) { normalized = `${normalized}T00:00:00Z`; } else if (!/[zZ]$|[+-]\d{2}:\d{2}$/.test(normalized)) { normalized = `${normalized}Z`; } const parsed = new Date(normalized); return Number.isNaN(parsed.getTime()) ? null : parsed; } function formatDateByParts(value, includeTime) { const parsed = parseStoredUtcDate(value); if (!parsed) { return value ? String(value).replace('T', ' ').slice(0, includeTime ? 16 : 10) : 'n/a'; } const parts = new Intl.DateTimeFormat('de-DE', { timeZone: browserTimezone, year: 'numeric', month: '2-digit', day: '2-digit', ...(includeTime ? { hour: '2-digit', minute: '2-digit' } : {}), }).formatToParts(parsed); const map = Object.fromEntries(parts.map((part) => [part.type, part.value])); return includeTime ? `${map.day}.${map.month}.${map.year} ${map.hour}:${map.minute}` : `${map.day}.${map.month}.${map.year}`; } function toDateTimeLocalValue(value) { const parsed = parseStoredUtcDate(value); if (!parsed) { return ''; } const parts = new Intl.DateTimeFormat('sv-SE', { timeZone: browserTimezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, }).formatToParts(parsed); const map = Object.fromEntries(parts.map((part) => [part.type, part.value])); return `${map.year}-${map.month}-${map.day}T${map.hour}:${map.minute}`; } function nowDateTimeLocalValue() { const now = new Date(); const parts = new Intl.DateTimeFormat('sv-SE', { timeZone: browserTimezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, }).formatToParts(now); const map = Object.fromEntries(parts.map((part) => [part.type, part.value])); return `${map.year}-${map.month}-${map.day}T${map.hour}:${map.minute}`; } function todayLocalDateValue() { const now = new Date(); const parts = new Intl.DateTimeFormat('sv-SE', { timeZone: browserTimezone, year: 'numeric', month: '2-digit', day: '2-digit', }).formatToParts(now); const map = Object.fromEntries(parts.map((part) => [part.type, part.value])); return `${map.year}-${map.month}-${map.day}`; } function isCryptoCurrencyCode(code) { const normalized = String(code || '').toUpperCase(); return [ 'ADA', 'ARB', 'AVAX', 'BNB', 'BTC', 'CTC', 'DAI', 'DOGE', 'DOT', 'ETH', 'HSH', 'LINK', 'LTC', 'MATIC', 'SOL', 'TRX', 'USDC', 'USDT', 'XMR', 'XRP', ].includes(normalized); } function entryCoverageMeta(startAt, runtimeMonths, autoRenew) { const normalizedRuntime = Number(runtimeMonths); const startDate = parseStoredUtcDate(startAt); if (!startDate || !Number.isFinite(normalizedRuntime) || normalizedRuntime <= 0) { return { endAt: null, isCovered: true }; } const endAtDate = calendarRuntimeEnd(startAt, normalizedRuntime); if (!endAtDate) { return { endAt: null, isCovered: true }; } return { endAt: endAtDate.toISOString(), isCovered: !!autoRenew || Date.now() <= endAtDate.getTime(), }; } function currentSectionLabel(sectionId) { return sectionMap.get(String(sectionId || '').trim()) || 'Bereich'; } function currentSectionSummary(sectionId) { if (sectionId === 'overview') { return 'Kernkennzahlen, ROI, Break-even und Zielmonitor.'; } if (sectionId === 'recovery') { return 'Zusammenhaengende historische Miner-, Transfer-, Wallet- und Mining-Daten nachtragen.'; } if (sectionId === 'upload') { return 'Neue Mining-Daten per OCR, Import oder manueller Eingabe erfassen.'; } if (sectionId === 'measurements') { return 'Historie der letzten Uploads mit Performance- und Trendwerten.'; } if (sectionId === 'wallet') { return 'NC-Wallet, Transfers, echte Wallet-Auszahlungen und erkannte Asset-Bestaende auswerten.'; } if (sectionId === 'mining') { return 'Miner, Angebote und operative Daten verwalten.'; } if (sectionId === 'dashboards') { return 'Gespeicherte Auswertungen und Visualisierungen anzeigen.'; } if (sectionId === 'settings') { return 'Schema, Modulrechte, DB-Checks und Basis-Settings steuern.'; } return 'Bereich des Mining-Checkers'; } function fmtDate(value) { if (!value) { return 'n/a'; } return formatDateByParts(value, true); } function fmtDateTime(value) { if (!value) { return 'n/a'; } return formatDateByParts(value, true); } function normalizeApiUrl(rawUrl) { const url = String(rawUrl || ''); if (!url || !apiBase.includes('?path=')) { return url; } if (!url.startsWith(apiBase)) { return url; } const [baseRoot, basePath = ''] = apiBase.split('?path='); const suffix = url.slice(apiBase.length); const [routeSuffix, extraQuery = ''] = suffix.split('?'); const normalizedPath = [basePath, routeSuffix] .map((part) => String(part || '').replace(/^\/+|\/+$/g, '')) .filter(Boolean) .join('/'); return `${baseRoot}?path=${encodeURIComponent(normalizedPath)}${extraQuery ? `&${extraQuery}` : ''}`; } async function request(path, options) { const requestOptions = options && typeof options === 'object' ? { ...options } : {}; const debugEnabled = !!debugBus.enabled; const timeoutMs = typeof requestOptions.timeoutMs === 'number' ? (debugEnabled ? Math.max(requestOptions.timeoutMs, 30000) : requestOptions.timeoutMs) : (debugEnabled ? 30000 : 20000); delete requestOptions.timeoutMs; const controller = new AbortController(); const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs); const requestUrl = normalizeApiUrl(path); const headers = { ...(requestOptions.headers || {}) }; if (debugEnabled) { headers['X-Mining-Debug'] = '1'; } requestOptions.headers = headers; emitDebug({ type: 'request:start', method: requestOptions.method || 'GET', url: requestUrl, body: typeof requestOptions.body === 'string' ? requestOptions.body.slice(0, 2000) : null, }); let response; try { response = await fetch(requestUrl, { ...requestOptions, credentials: 'same-origin', redirect: 'manual', signal: controller.signal, }); } catch (error) { window.clearTimeout(timeoutId); if (error && error.name === 'AbortError') { emitDebug({ type: 'request:timeout', method: requestOptions.method || 'GET', url: requestUrl, timeout_ms: timeoutMs, }); if (debugEnabled) { loadLatestDebugTrace(); } throw new Error('API request timeout'); } emitDebug({ type: 'request:error', method: requestOptions.method || 'GET', url: requestUrl, page_url: window.location.href, online: window.navigator ? window.navigator.onLine : null, message: error && error.message ? error.message : 'Fetch fehlgeschlagen', }); throw error; } window.clearTimeout(timeoutId); if (response.type === 'opaqueredirect' || (response.status >= 300 && response.status < 400)) { const location = response.headers ? response.headers.get('Location') : ''; emitDebug({ type: 'request:redirect', method: requestOptions.method || 'GET', url: requestUrl, status: response.status, location: location || null, }); throw new Error(`API request wurde weitergeleitet${location ? ` nach ${location}` : ''}. Bitte Login/Session und Proxy-Rewrite pruefen.`); } const payload = await response.json().catch(() => ({})); emitDebug({ type: 'request:response', method: requestOptions.method || 'GET', url: requestUrl, status: response.status, ok: response.ok, has_debug: Array.isArray(payload && payload.debug) && payload.debug.length > 0, }); if (debugEnabled && payload && payload.debug) { emitServerTraceEntries(payload.debug, { type: 'server:trace', source: requestUrl, }); } if (response.status === 401) { emitDebug({ type: 'request:unauthorized', method: requestOptions.method || 'GET', url: requestUrl, redirect_to: '/auth/keycloak', }); window.location.href = '/auth/keycloak'; throw new Error('Nicht autorisiert. Weiterleitung zum Login.'); } if (!response.ok) { const context = payload && payload.context && typeof payload.context === 'object' ? payload.context : null; const detail = context ? [context.message, context.statement ? `SQL: ${String(context.statement).slice(0, 500)}` : null] .filter(Boolean) .join(' ') : ''; throw new Error([payload.error || 'API request failed', detail].filter(Boolean).join(' ')); } if (payload && Object.prototype.hasOwnProperty.call(payload, 'data')) { return payload.data; } return payload; } function normalizeBootstrap(data, projectKey) { const normalized = data && typeof data === 'object' ? data : {}; return { project: normalized.project || { project_key: projectKey }, settings: normalized.settings || { project_key: projectKey, baseline_measured_at: '', baseline_coins_total: '', daily_cost_amount: '', daily_cost_currency: 'EUR', report_currency: 'EUR', crypto_currency: 'DOGE', crypto_base_price_amount: '5.49', crypto_base_price_currency: 'USD', min_offer_runtime_months: 24, target_offer_hashrate_kh: 50, target_offer_runtime_months: 36, preferred_currencies: ['DOGE', 'USD', 'EUR'], cost_plans: [], currencies: [], payouts: [], wallet_withdrawals: [], miner_offers: [], 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 : [], fx_snapshots: normalized.fx_snapshots && typeof normalized.fx_snapshots === 'object' ? normalized.fx_snapshots : {}, summary: normalized.summary || { 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, wallet_balances: {}, wallet_balance_current_asset: null, wallet_balance_current_asset_direct: null, holdings_current_asset: null, }, current_hashrate_mh: null, miner_offers: [], }, }; } function normalizeSchemaStatus(data) { const hasPayload = !!(data && typeof data === 'object'); const normalized = hasPayload ? data : {}; return { loaded: hasPayload, required_tables: Array.isArray(normalized.required_tables) ? normalized.required_tables : [], present_tables: Array.isArray(normalized.present_tables) ? normalized.present_tables : [], missing_tables: Array.isArray(normalized.missing_tables) ? normalized.missing_tables : [], pending_upgrades: Array.isArray(normalized.pending_upgrades) ? normalized.pending_upgrades : [], present_count: typeof normalized.present_count === 'number' ? normalized.present_count : 0, missing_count: typeof normalized.missing_count === 'number' ? normalized.missing_count : 0, pending_upgrade_count: typeof normalized.pending_upgrade_count === 'number' ? normalized.pending_upgrade_count : 0, all_present: !!normalized.all_present, }; } function normalizeOcrPreview(data) { const normalized = data && typeof data === 'object' ? data : {}; const suggested = normalized.suggested && typeof normalized.suggested === 'object' ? normalized.suggested : {}; return { kind: normalized.kind === 'wallet' ? 'wallet' : 'measurement', suggested: { measured_at: suggested.measured_at || '', coins_total: suggested.coins_total ?? '', price_per_coin: suggested.price_per_coin ?? '', price_currency: suggested.price_currency || '', 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 || '', raw_text: normalized.raw_text || '', }; } const OCR_AUTO_SAVE_CONFIDENCE_THRESHOLD = 0.95; function getOcrStatusMessage(preview) { const flags = Array.isArray(preview && preview.flags) ? preview.flags : []; const missingProviders = flags .filter((flag) => typeof flag === 'string' && flag.indexOf('ocr_provider_missing:') === 0) .map((flag) => flag.split(':')[1]) .filter(Boolean); if (flags.includes('ocr_engine_missing') || missingProviders.length) { return { tone: 'error', text: missingProviders.length ? `Auf dem Server ist kein nutzbarer OCR-Provider verfuegbar. Fehlend: ${missingProviders.join(', ')}. Bitte OCR.space oder Tesseract pruefen.` : 'Auf dem Server ist kein nutzbarer OCR-Provider verfuegbar. Bitte OCR.space oder Tesseract pruefen.', }; } const emptyProviders = flags .filter((flag) => typeof flag === 'string' && flag.indexOf('ocr_provider_empty:') === 0) .map((flag) => flag.split(':')[1]) .filter(Boolean); if (emptyProviders.length) { return { tone: 'warn', text: `Der OCR-Provider ${emptyProviders.join(', ')} hat fuer diesen Screenshot keinen verwertbaren Rohtext geliefert.`, }; } if (flags.includes('ocr_raw_text_empty')) { return { tone: 'warn', text: 'Es wurde kein OCR-Rohtext erkannt. Bitte Screenshot pruefen oder optionalen OCR-Hinweistext angeben.', }; } return null; } function StatCard(props) { return h('div', { className: 'mc-stat-card' }, [ h('div', { key: 'label', className: 'mc-kicker' }, props.label), h('div', { key: 'value', className: 'mc-stat-value' }, props.value), props.sub ? h('div', { key: 'sub', className: 'mc-text' }, props.sub) : null, ]); } function Badge(props) { return h('span', { className: cx( 'mc-badge', props.tone === 'warn' ? 'mc-badge--warn' : props.tone === 'danger' ? 'mc-badge--danger' : props.tone === 'success' ? 'mc-badge--success' : 'mc-badge--info' ) }, props.children); } function SectionTitle(props) { return h('div', { className: 'mc-section-head' }, [ h('div', { key: 'copy' }, [ h('h2', { key: 'title', className: 'mc-section-title' }, props.title), props.subtitle ? h('p', { key: 'subtitle', className: 'mc-text' }, props.subtitle) : null, ]), props.action || null, ]); } function SimpleChart(props) { const [hoveredPoint, setHoveredPoint] = useState(null); const touchLike = typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(hover: none), (pointer: coarse)').matches; function pointTooltip(seriesLabel, point) { const label = seriesLabel ? `${seriesLabel} · ` : ''; return `${label}${String(point.x)}: ${point.formatted || fmtNumber(point.y, 6)}`; } function axisLabel(axis) { return axis === 'right' ? (props.rightAxisLabel || 'Rechts') : (props.leftAxisLabel || props.yLabel || 'Wert'); } const series = Array.isArray(props.series) ? props.series .map((item, index) => ({ key: item && item.key ? String(item.key) : `series-${index}`, label: item && item.label ? String(item.label) : `Serie ${index + 1}`, color: item && item.color ? String(item.color) : ['#60a5fa', '#2dd4bf', '#f59e0b', '#f472b6'][index % 4], axis: item && item.axis === 'right' ? 'right' : 'left', formatter: item && typeof item.formatter === 'function' ? item.formatter : null, data: Array.isArray(item && item.data) ? item.data .filter((point) => point && point.y !== null && point.y !== undefined) .map((point) => ({ ...point, formatted: item && typeof item.formatter === 'function' ? item.formatter(Number(point.y), point) : (point.formatted || fmtNumber(point.y, 6)), })) : [], })) .filter((item) => item.data.length > 0) : []; const points = Array.isArray(props.data) ? props.data .filter((point) => point && point.y !== null && point.y !== undefined) .map((point) => ({ ...point, formatted: point.formatted || fmtNumber(point.y, 6), })) : []; const isMultiSeries = series.length > 0; const activeSeries = isMultiSeries ? series : [{ key: 'default', label: props.yLabel || 'Wert', color: props.type === 'area' ? '#2dd4bf' : '#60a5fa', axis: 'left', formatter: null, data: points, }]; const allPoints = activeSeries.flatMap((item) => item.data); if (!allPoints.length) { return h('div', { className: 'mc-empty' }, 'Keine Daten fuer diese Ansicht.'); } if (props.type === 'table') { return h('div', { className: 'mc-table-shell' }, [ h('table', { key: 'table', className: 'mc-table' }, [ h('thead', { key: 'head' }, h('tr', null, [ h('th', { key: 'x' }, props.xLabel || 'X'), h('th', { key: 'y' }, props.yLabel || 'Y'), ])), h('tbody', { key: 'body' }, points.map((point, index) => h('tr', { key: index }, [ h('td', { key: 'x' }, String(point.x)), h('td', { key: 'y' }, fmtNumber(point.y, 6)), ])) ), ]), ]); } const width = 640; const height = 220; const padding = 24; const axisStats = activeSeries.reduce((stats, item) => { const values = item.data.map((point) => Number(point.y)).filter((value) => Number.isFinite(value)); if (!values.length) { return stats; } const axis = item.axis === 'right' ? 'right' : 'left'; const min = Math.min.apply(null, values); const max = Math.max.apply(null, values); const existing = stats[axis]; stats[axis] = existing ? { min: Math.min(existing.min, min), max: Math.max(existing.max, max), } : { min, max }; return stats; }, { left: null, right: null }); const seriesCoords = activeSeries.map((item) => { const axis = item.axis === 'right' ? 'right' : 'left'; const axisMeta = axisStats[axis] || { min: 0, max: 1 }; const range = axisMeta.max - axisMeta.min || 1; const stepX = item.data.length > 1 ? (width - padding * 2) / (item.data.length - 1) : 0; const coords = item.data.map((point, index) => { const x = padding + stepX * index; const y = height - padding - ((Number(point.y) - axisMeta.min) / range) * (height - padding * 2); return [x, y]; }); return { ...item, coords, line: coords.map((coord) => coord.join(',')).join(' '), area: coords.length ? [[coords[0][0], height - padding]].concat(coords, [[coords[coords.length - 1][0], height - padding]]) .map((coord) => coord.join(',')).join(' ') : '', }; }); const leftAxis = axisStats.left || axisStats.right || { min: 0, max: 1 }; const rightAxis = axisStats.right; return h('div', { className: 'mc-chart space-y-3', style: { position: 'relative' } }, [ h('div', { key: 'meta', className: 'mc-flex-split mc-kicker' }, [ h('span', { key: 'left' }, `${axisLabel('left')} · Min ${fmtNumber(leftAxis.min, 4)} · Max ${fmtNumber(leftAxis.max, 4)}`), rightAxis ? h('span', { key: 'right' }, `${axisLabel('right')} · Min ${fmtNumber(rightAxis.min, 4)} · Max ${fmtNumber(rightAxis.max, 4)}`) : null, ]), hoveredPoint ? h('div', { key: 'hover', className: 'mc-mini-card', style: { position: 'absolute', top: 28, right: 0, zIndex: 3, pointerEvents: 'none', maxWidth: '220px', }, }, [ h('div', { key: 'label', className: 'mc-kicker', style: { color: hoveredPoint.color } }, hoveredPoint.label), h('div', { key: 'value' }, `${hoveredPoint.x}: ${hoveredPoint.value}`), ]) : null, h('svg', { key: 'svg', viewBox: `0 0 ${width} ${height}`, className: 'overflow-visible' }, [ h('g', { key: 'grid', stroke: 'rgba(255,255,255,0.08)' }, [ h('line', { key: 'top', x1: padding, x2: width - padding, y1: padding, y2: padding }), h('line', { key: 'mid', x1: padding, x2: width - padding, y1: height / 2, y2: height / 2 }), h('line', { key: 'base', x1: padding, x2: width - padding, y1: height - padding, y2: height - padding }), ]), h('g', { key: 'axis-labels', fill: 'rgba(148,163,184,0.95)', fontSize: 10 }, [ h('text', { key: 'left-top', x: padding, y: 14, textAnchor: 'start' }, axisLabel('left')), rightAxis ? h('text', { key: 'right-top', x: width - padding, y: 14, textAnchor: 'end' }, axisLabel('right')) : null, ]), props.type === 'bar' && !isMultiSeries ? h('g', { key: 'bars' }, seriesCoords[0].coords.map((coord, index) => { const barWidth = Math.max(10, (((width - padding * 2) / Math.max(seriesCoords[0].coords.length - 1, 1)) * 0.6) || 24); return h('rect', { key: index, x: coord[0] - barWidth / 2, y: coord[1], width: barWidth, height: height - padding - coord[1], rx: 8, fill: 'rgba(59, 130, 246, 0.75)', onMouseEnter: () => setHoveredPoint({ label: seriesCoords[0].label, x: String(seriesCoords[0].data[index].x), value: seriesCoords[0].data[index].formatted, color: seriesCoords[0].color, }), onMouseLeave: () => !touchLike && setHoveredPoint(null), onClick: () => setHoveredPoint({ label: seriesCoords[0].label, x: String(seriesCoords[0].data[index].x), value: seriesCoords[0].data[index].formatted, color: seriesCoords[0].color, }), }, [ h('title', { key: 'title' }, pointTooltip(seriesCoords[0].label, seriesCoords[0].data[index])), ]); })) : h('g', { key: 'series' }, seriesCoords.flatMap((item, index) => { const nodes = []; if (props.type === 'area' && !isMultiSeries && item.area) { nodes.push(h('polygon', { key: `${item.key}-area`, points: item.area, fill: 'rgba(45, 212, 191, 0.18)', })); } nodes.push(h('polyline', { key: `${item.key}-line`, points: item.line, fill: 'none', stroke: item.color, strokeWidth: 3, strokeLinecap: 'round', strokeLinejoin: 'round', })); nodes.push(h('g', { key: `${item.key}-dots` }, item.coords.map((coord, pointIndex) => h('circle', { key: `${item.key}-${pointIndex}`, cx: coord[0], cy: coord[1], r: 5, fill: '#f8fafc', stroke: item.color, strokeWidth: 2, style: { cursor: 'pointer' }, onMouseEnter: () => setHoveredPoint({ label: item.label, x: String(item.data[pointIndex].x), value: item.data[pointIndex].formatted, color: item.color, }), onMouseLeave: () => !touchLike && setHoveredPoint(null), onClick: () => setHoveredPoint({ label: item.label, x: String(item.data[pointIndex].x), value: item.data[pointIndex].formatted, color: item.color, }), }, [ h('title', { key: 'title' }, pointTooltip(item.label, item.data[pointIndex])), ])))); return nodes; })), ]), isMultiSeries ? h('div', { key: 'legend', className: 'mc-mini-grid' }, seriesCoords.map((item) => { const latestPoint = item.data[item.data.length - 1] || null; return h('div', { key: item.key, className: 'mc-mini-card' }, [ h('div', { key: 'label', className: 'mc-kicker', style: { color: item.color } }, item.label), h('div', { key: 'value' }, latestPoint ? `${fmtNumber(latestPoint.y, 4)} · ${latestPoint.x}` : 'n/a'), ]); }) ) : h('div', { key: 'labels', className: 'mc-mini-grid' }, points.slice(-3).map((point, index) => h('div', { key: index, className: 'mc-mini-card' }, `${point.x}: ${fmtNumber(point.y, 6)}`)) ), ]); } function DashboardCard(props) { return h('div', { className: 'mc-dashboard-card' }, [ h('div', { key: 'head', className: 'mc-flex-split' }, [ h('div', { key: 'titles' }, [ h('h3', { key: 'name' }, props.definition.name), h('p', { key: 'meta', className: 'mc-kicker' }, `${props.definition.chart_type} · ${props.definition.x_field} → ${props.definition.y_field} · ${props.definition.aggregation}`), ]), h(Badge, { key: 'badge' }, props.definition.is_active ? 'aktiv' : 'inaktiv'), ]), props.loading ? h('div', { key: 'loading', className: 'mc-empty' }, 'Lade Dashboarddaten …') : h(SimpleChart, { key: 'chart', type: props.definition.chart_type, data: props.data || [], xLabel: props.definition.x_field, yLabel: props.definition.y_field, }), ]); } function miningCheckerViewFromHref(href) { try { const url = new URL(href, window.location.origin); if (url.pathname !== '/module/mining-checker') { return null; } return url.searchParams.get('view') || 'overview'; } catch (err) { return null; } } function syncMiningCheckerTabButtons(activeTab) { const buttons = Array.from(document.querySelectorAll('.module-tabs a[href*="/module/mining-checker"]')); for (const button of buttons) { const tab = miningCheckerViewFromHref(button.getAttribute('href') || ''); if (!tab) { continue; } const isActive = tab === activeTab; button.classList.toggle('module-button--tab-active', isActive); button.classList.toggle('module-button--tab', !isActive); } } function App() { const [projectKey, setProjectKey] = useState(initialProjectKey); const [activeTab, setActiveTab] = useState(initialActiveTab); const [payload, setPayload] = useState(() => normalizeBootstrap(null, initialProjectKey)); const [dashboardData, setDashboardData] = useState({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const [message, setMessage] = useState(''); const [schemaStatus, setSchemaStatus] = useState(normalizeSchemaStatus(null)); const bootstrapCacheRef = useRef(new Map()); const [initForm, setInitForm] = useState({ drop_existing: false }); const [sqlImportFile, setSqlImportFile] = useState(null); const [dbCheck, setDbCheck] = useState(null); const [measurementForm, setMeasurementForm] = useState({ measured_at: '', coins_total: '', price_per_coin: '', price_currency: '', note: '', source: 'manual', }); const [recoveryForm, setRecoveryForm] = useState({ miner_at: '2026-09-03T13:18:00', miner_cost: '277.90', doge_usd: '0.087812', usd_eur: '0.86014', transfer_at: '2026-09-08T02:19:00', transfer_coins: '78.495864', wallet_at: '2026-09-12T23:46:05', wallet_coins: '80.90114043', mining_at: '2026-09-12T23:46:05', mining_coins: '81.293679', mining_price_usd: '0.08472', }); const [importForm, setImportForm] = useState({ rows_text: '', default_currency: 'USD', source: 'manual', }); const [importHelpOpen, setImportHelpOpen] = useState(false); const [ocrForm, setOcrForm] = useState({ image: null, date_context: todayLocalDateValue(), ocr_hint_text: '', }); const [ocrPreview, setOcrPreview] = useState(null); const [dashboardForm, setDashboardForm] = useState({ name: 'Neues Dashboard', chart_type: 'line', x_field: 'measured_at', y_field: 'coins_total', aggregation: 'none', filters: { source: '', currency: '' }, }); const [settingsForm, setSettingsForm] = useState({ baseline_measured_at: '', baseline_coins_total: '', report_currency: 'EUR', crypto_currency: 'DOGE', crypto_base_price_amount: '5.49', crypto_base_price_currency: 'USD', min_offer_runtime_months: '24', target_offer_hashrate_kh: '50', target_offer_runtime_months: '36', }); const [offerPreviewForm, setOfferPreviewForm] = useState({ crypto_base_price_amount: '', crypto_base_price_currency: 'USD', fiat_base_price_amount: '', }); const [offerBasisHistory, setOfferBasisHistory] = useState([]); const [previewMinerOffers, setPreviewMinerOffers] = useState([]); const [previewOffersLoading, setPreviewOffersLoading] = useState(false); const [moduleAuthForm, setModuleAuthForm] = useState({ required: true, users: '', groups: '', }); const [fxHistory, setFxHistory] = useState([]); const [fxSelection, setFxSelection] = useState(['DOGE', 'USD', 'EUR']); const [reportCurrencyOverride, setReportCurrencyOverride] = useState(() => { const value = String(getCookie('mining_checker_report_currency') || '').toUpperCase(); return /^[A-Z0-9]{3,10}$/.test(value) ? value : ''; }); const [nowTimestamp, setNowTimestamp] = useState(() => Date.now()); const [targetForm, setTargetForm] = useState({ label: '', target_amount_fiat: '', currency: 'EUR', miner_offer_id: '', is_active: true, sort_order: 0, }); const [targetModalOpen, setTargetModalOpen] = useState(false); const [selectedMinerScenarioId, setSelectedMinerScenarioId] = useState(null); const [minerOfferFilters, setMinerOfferFilters] = useState({ offer_type: 'crypto', speed_min: '', speed_unit: 'auto', price_max: '', max_doge: '', runtime_months: '', }); const [costPlanForm, setCostPlanForm] = useState({ label: '', starts_at: '', runtime_months: 1, mining_speed_value: '', mining_speed_unit: 'MH/s', bonus_percent: '', auto_renew: true, base_price_amount: '', payment_type: 'fiat', total_cost_amount: '', currency: 'EUR', note: '', is_active: true, }); const [costPlanModalOpen, setCostPlanModalOpen] = useState(false); const [payoutForm, setPayoutForm] = useState({ payout_at: '', coins_amount: '', payout_currency: 'DOGE', note: '', }); const [payoutModalOpen, setPayoutModalOpen] = useState(false); const [payoutMode, setPayoutMode] = useState('partial'); const [walletTransferRows, setWalletTransferRows] = useState([]); const [walletWithdrawalForm, setWalletWithdrawalForm] = useState({ withdrawal_at: '', coins_amount: '', withdrawal_currency: 'DOGE', note: '', }); const [walletWithdrawalModalOpen, setWalletWithdrawalModalOpen] = useState(false); const [walletWithdrawalRows, setWalletWithdrawalRows] = useState([]); const [purchaseMinerModalOpen, setPurchaseMinerModalOpen] = useState(false); const [purchaseMinerForm, setPurchaseMinerForm] = useState({ offer_id: '', base_offer_id: '', purchased_at: '', label: '', mining_speed_value: '', mining_speed_unit: '', bonus_percent: '', total_cost_amount: '', currency: '', reference_price_amount: '', reference_price_currency: '', auto_renew: false, note: '', }); useEffect(() => { debugBus.enabled = initialDebugMode; emitDebug({ type: 'debug:mode', enabled: initialDebugMode, }); }, []); const measurements = Array.isArray(payload?.measurements) ? payload.measurements : []; const latest = payload?.summary?.latest_measurement || (measurements.length ? measurements[measurements.length - 1] : null); const currentSettings = payload?.settings || { cost_plans: [], currencies: [], }; const reportCurrency = reportCurrencyOverride || currentSettings.report_currency || 'EUR'; const currentTargets = Array.isArray(payload?.summary?.targets) ? payload.summary.targets : []; const currentDashboards = Array.isArray(payload?.dashboards) ? payload.dashboards : []; const currencies = Array.isArray(currentSettings.currencies) && currentSettings.currencies.length ? currentSettings.currencies : [ { code: 'EUR', name: 'Euro' }, { code: 'USD', name: 'US-Dollar' }, { code: 'DOGE', name: 'Dogecoin' }, { code: 'BTC', name: 'Bitcoin' }, { code: 'ETH', name: 'Ethereum' }, { code: 'CTC', name: 'Creditcoin' }, { code: 'HSH', name: 'HashCoin' }, { code: 'LTC', name: 'Litecoin' }, { code: 'USDT', name: 'Tether' }, { code: 'USDC', name: 'USD Coin' }, ]; const currentCostPlans = Array.isArray(currentSettings.cost_plans) ? currentSettings.cost_plans : []; 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 currentPurchasedMiners = Array.isArray(currentSettings.purchased_miners) ? currentSettings.purchased_miners : []; const currentSummaryMinerOffers = Array.isArray(payload?.summary?.miner_offers) ? payload.summary.miner_offers : []; const currentMiningCurrency = String((latest && latest.coin_currency) || currentSettings.crypto_currency || 'DOGE').toUpperCase(); const minOfferRuntimeMonths = Number(currentSettings.min_offer_runtime_months) > 0 ? Number(currentSettings.min_offer_runtime_months) : 24; const targetOfferHashrateKh = Number(currentSettings.target_offer_hashrate_kh) > 0 ? Number(currentSettings.target_offer_hashrate_kh) : 50; const targetOfferRuntimeMonths = Number(currentSettings.target_offer_runtime_months) > 0 ? Number(currentSettings.target_offer_runtime_months) : 36; const overviewPerDayLabel = `${currentMiningCurrency} pro Tag`; const latestMeasurementDate = latest && latest.measured_at ? parseStoredUtcDate(latest.measured_at) : null; const latestMeasurementAgeMs = latestMeasurementDate ? (Date.now() - latestMeasurementDate.getTime()) : null; const canTransferAll = !!latest && Number(latest?.coins_total_visible ?? latest?.coins_total ?? 0) > 0; const fullTransferCoins = Number(latest?.coins_total_visible ?? latest?.coins_total ?? 0); const transferAllHint = !latestMeasurementDate ? 'Kein Mining-Upload vorhanden.' : latestMeasurementAgeMs !== null && latestMeasurementAgeMs < 0 ? `Letzter Upload ${fmtDate(latest.measured_at)} liegt in der Zukunft. Backend-Pruefung bleibt aktiv.` : latestMeasurementAgeMs !== null && latestMeasurementAgeMs <= 180000 ? `Verwendet den letzten Upload von ${fmtDate(latest.measured_at)} mit ${fmtNumber(fullTransferCoins, 6)} ${currentMiningCurrency}.` : `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 currentWalletPrimaryCurrency = (() => { const snapshotCurrency = String((latestWalletSnapshot && latestWalletSnapshot.wallet_currency) || '').toUpperCase(); if (snapshotCurrency) { return snapshotCurrency; } const latestWithdrawal = currentWalletWithdrawals.length ? currentWalletWithdrawals[currentWalletWithdrawals.length - 1] : null; const latestWalletTransfer = currentPayouts.length ? currentPayouts[currentPayouts.length - 1] : null; const ledgerCurrency = String( (latestWithdrawal && latestWithdrawal.withdrawal_currency) || (latestWalletTransfer && latestWalletTransfer.payout_currency) || '' ).toUpperCase(); return ledgerCurrency || currentMiningCurrency || currentSettings.crypto_currency || 'DOGE'; })(); const preferredCurrencyCodes = Array.isArray(currentSettings.preferred_currencies) ? currentSettings.preferred_currencies.map((code) => String(code || '').toUpperCase()).filter(Boolean) : []; const preferredCurrencySet = new Set(preferredCurrencyCodes); const preferredSelectableCurrencies = preferredCurrencySet.size ? currencies.filter((currency) => preferredCurrencySet.has(String(currency.code || '').toUpperCase())) : currencies; const selectableCurrencies = preferredSelectableCurrencies.length ? preferredSelectableCurrencies : currencies; const evaluatedMinerOffers = Array.isArray(previewMinerOffers) ? previewMinerOffers : []; const selectedOfferType = String(minerOfferFilters.offer_type || 'crypto').toLowerCase() === 'fiat' ? 'fiat' : 'crypto'; const visibleOfferBasisHistory = (Array.isArray(offerBasisHistory) ? offerBasisHistory : []) .filter((entry) => String(entry.offer_type || '').toLowerCase() === selectedOfferType) .slice(0, 6); const hasCryptoOfferBasis = Number(offerPreviewForm.crypto_base_price_amount) > 0; const hasFiatOfferBasis = Number(offerPreviewForm.fiat_base_price_amount) > 0; const maxDogeFilter = minerOfferFilters.max_doge === '' ? null : Number(minerOfferFilters.max_doge); const filteredMinerOffers = evaluatedMinerOffers.filter((offer) => { const paymentType = String(offer.payment_type || '').toLowerCase(); const speedMin = minerOfferFilters.speed_min === '' ? null : Number(minerOfferFilters.speed_min); const speedUnit = String(minerOfferFilters.speed_unit || 'auto'); const priceMax = minerOfferFilters.price_max === '' ? null : Number(minerOfferFilters.price_max); const runtimeMonths = minerOfferFilters.runtime_months === '' ? null : Number(minerOfferFilters.runtime_months); const offerHashrate = Number(offer.offer_hashrate_mh); const comparablePrice = convertCurrencyValue( offer.base_price_amount ?? offer.effective_price_amount, offer.base_price_currency || offer.effective_price_currency, reportCurrency ); const comparableDogePrice = convertCurrencyValue( offer.effective_price_amount ?? offer.base_price_amount, offer.effective_price_currency || offer.base_price_currency, 'DOGE' ); const runtime = Number(offer.runtime_months); const comparableHashrate = speedUnit === 'kh' ? offerHashrate * 1000 : offerHashrate; if (paymentType !== selectedOfferType) { return false; } if ( selectedOfferType === 'crypto' && Number.isFinite(maxDogeFilter) && (!Number.isFinite(comparableDogePrice) || comparableDogePrice > maxDogeFilter) ) { return false; } if (selectedOfferType === 'fiat' && !Number.isFinite(speedMin)) { return false; } if (Number.isFinite(speedMin) && (!Number.isFinite(comparableHashrate) || comparableHashrate < speedMin)) { return false; } if (Number.isFinite(priceMax) && (!Number.isFinite(comparablePrice) || comparablePrice > priceMax)) { return false; } if (Number.isFinite(runtimeMonths) && runtime < runtimeMonths) { return false; } return true; }); const pinnedReferenceMinerOffer = evaluatedMinerOffers.find((offer) => ( String(offer.payment_type || '').toLowerCase() === 'crypto' && Number(offer.runtime_months) === 36 && Math.abs(toKhPerSecond(offer.mining_speed_value, offer.mining_speed_unit) - 50) < 0.0001 )) || null; const visibleMinerOffers = pinnedReferenceMinerOffer && !filteredMinerOffers.some((offer) => String(offer.id) === String(pinnedReferenceMinerOffer.id)) ? [{ ...pinnedReferenceMinerOffer, is_pinned_reference: true }].concat(filteredMinerOffers) : filteredMinerOffers.map((offer) => ({ ...offer, is_pinned_reference: pinnedReferenceMinerOffer !== null && String(offer.id) === String(pinnedReferenceMinerOffer.id), })); const scenarioMinerOffers = visibleMinerOffers; const preferredMinerOffer = currentSummaryMinerOffers .filter((offer) => String(offer.payment_type || '').toLowerCase() === 'crypto') .map((offer) => { const offerHashrateKh = Number(offer.offer_hashrate_mh) * 1000; const runtime = Number(offer.runtime_months); return { ...offer, __runtimeDelta: Math.abs(runtime - targetOfferRuntimeMonths), __hashrateDelta: Math.abs(offerHashrateKh - targetOfferHashrateKh), }; }) .sort((left, right) => left.__runtimeDelta - right.__runtimeDelta || left.__hashrateDelta - right.__hashrateDelta)[0] || null; const syntheticPreferredMinerOffer = (() => { const baseUsdAmount = Number(currentSettings.crypto_base_price_amount || 5.49); const runtimeConfig = CRYPTO_RUNTIME_CONFIGS[targetOfferRuntimeMonths]; if (!Number.isFinite(baseUsdAmount) || baseUsdAmount <= 0 || !runtimeConfig || targetOfferHashrateKh <= 0) { return null; } const speedFactor = targetOfferHashrateKh / 50; const runtimeFactor = targetOfferRuntimeMonths / 3; const discountFactor = Math.max(0, 1 - (Number(runtimeConfig.discountPercent) / 100)); const usdAmount = baseUsdAmount * speedFactor * runtimeFactor * discountFactor; const effectiveAmount = convertCurrencyValue(usdAmount, 'USD', currentMiningCurrency); return { label: `Crypto Server · ${fmtNumber(targetOfferHashrateKh, 0)} kH/s · ${targetOfferRuntimeMonths} Monate`, usd_display_price_amount: usdAmount, usd_display_price_currency: 'USD', effective_price_amount: effectiveAmount, effective_price_currency: currentMiningCurrency, runtime_months: targetOfferRuntimeMonths, offer_hashrate_mh: targetOfferHashrateKh / 1000, }; })(); const resolvedPreferredMinerOffer = pinnedReferenceMinerOffer || preferredMinerOffer || syntheticPreferredMinerOffer; const preferredOfferRequiredCoins = resolvedPreferredMinerOffer ? (() => { const directCryptoAmount = Number(resolvedPreferredMinerOffer.crypto_display_price_amount); const directCryptoCurrency = String(resolvedPreferredMinerOffer.crypto_display_price_currency || '').toUpperCase(); if (Number.isFinite(directCryptoAmount) && directCryptoAmount > 0 && directCryptoCurrency === currentMiningCurrency) { return directCryptoAmount; } return convertCurrencyValue( resolvedPreferredMinerOffer.effective_price_amount ?? resolvedPreferredMinerOffer.crypto_display_price_amount ?? resolvedPreferredMinerOffer.usd_display_price_amount, resolvedPreferredMinerOffer.effective_price_currency || resolvedPreferredMinerOffer.crypto_display_price_currency || resolvedPreferredMinerOffer.usd_display_price_currency || currentMiningCurrency, currentMiningCurrency ); })() : null; const preferredOfferCurrentCoins = payload?.summary?.payouts?.holdings_current_asset !== null && payload?.summary?.payouts?.holdings_current_asset !== undefined ? Number(payload.summary.payouts.holdings_current_asset) : (latest ? Number(latest.coins_total_visible ?? latest.coins_total) : null); const preferredOfferRemainingCoins = Number.isFinite(preferredOfferRequiredCoins) && Number.isFinite(preferredOfferCurrentCoins) ? Math.max(0, preferredOfferRequiredCoins - preferredOfferCurrentCoins) : null; const preferredOfferCurrentFiatValue = Number.isFinite(preferredOfferCurrentCoins) ? convertCurrencyValue(preferredOfferCurrentCoins, currentMiningCurrency, reportCurrency) : null; const preferredOfferRequiredFiatValue = Number.isFinite(preferredOfferRequiredCoins) ? convertCurrencyValue(preferredOfferRequiredCoins, currentMiningCurrency, reportCurrency) : null; const preferredOfferRemainingDaysAtUpload = preferredOfferRemainingCoins !== null && Number(latest?.doge_per_day_since_last_payout) > 0 ? (preferredOfferRemainingCoins / Number(latest.doge_per_day_since_last_payout)) : null; const preferredOfferUploadTimestamp = latest?.measured_at ? Date.parse(latest.measured_at) : NaN; const preferredOfferEtaTimestamp = preferredOfferRemainingDaysAtUpload !== null && Number.isFinite(preferredOfferUploadTimestamp) ? preferredOfferUploadTimestamp + (preferredOfferRemainingDaysAtUpload * 86400000) : NaN; const preferredOfferRemainingDays = Number.isFinite(preferredOfferEtaTimestamp) ? Math.max(0, (preferredOfferEtaTimestamp - nowTimestamp) / 86400000) : null; const preferredOfferEta = Number.isFinite(preferredOfferEtaTimestamp) ? new Date(preferredOfferEtaTimestamp).toISOString() : null; const remainingDaysUntil = (timestamp) => { const etaTimestamp = timestamp ? new Date(timestamp).getTime() : NaN; return Number.isFinite(etaTimestamp) ? Math.max(0, (etaTimestamp - nowTimestamp) / 86400000) : null; }; const speedUnits = ['kH/s', 'MH/s']; const fiatCurrencies = currencies.filter((currency) => !currency.is_crypto); const cryptoCurrencies = currencies.filter((currency) => !!currency.is_crypto); const preferredSelectableFiatCurrencies = preferredCurrencySet.size ? fiatCurrencies.filter((currency) => preferredCurrencySet.has(String(currency.code || '').toUpperCase())) : fiatCurrencies; const preferredSelectableCryptoCurrencies = preferredCurrencySet.size ? cryptoCurrencies.filter((currency) => preferredCurrencySet.has(String(currency.code || '').toUpperCase())) : cryptoCurrencies; const selectableFiatCurrencies = preferredSelectableFiatCurrencies.length ? preferredSelectableFiatCurrencies : fiatCurrencies; const selectableCryptoCurrencies = preferredSelectableCryptoCurrencies.length ? preferredSelectableCryptoCurrencies : cryptoCurrencies; const selectedMinerScenario = scenarioMinerOffers.find((offer) => String(offer.id) === String(selectedMinerScenarioId)) || null; const allMinerRows = currentPurchasedMiners.map((miner) => { const effectiveCurrency = String(miner.currency || '').toUpperCase(); const paymentType = isCryptoCurrencyCode(effectiveCurrency) ? 'crypto' : 'fiat'; const effectiveAutoRenew = paymentType === 'crypto' ? false : !!miner.auto_renew; const coverage = entryCoverageMeta(miner.purchased_at, miner.runtime_months, effectiveAutoRenew); const referenceCurrency = String(miner.reference_price_currency || '').toUpperCase(); const referenceAmount = Number(miner.reference_price_amount); const hasFiatReference = referenceCurrency && !isCryptoCurrencyCode(referenceCurrency); const fallbackUsdReference = Number(miner.usd_reference_amount); let baseAmount = null; let baseCurrency = null; if (paymentType === 'crypto') { if (Number.isFinite(referenceAmount) && referenceAmount > 0 && hasFiatReference) { const convertedReference = referenceCurrency === reportCurrency ? referenceAmount : convertCurrencyValue(referenceAmount, referenceCurrency, reportCurrency); if (Number.isFinite(convertedReference) && convertedReference > 0) { baseAmount = convertedReference; baseCurrency = reportCurrency; } else { baseAmount = referenceAmount; baseCurrency = referenceCurrency; } } else if (Number.isFinite(fallbackUsdReference) && fallbackUsdReference > 0) { const convertedUsd = reportCurrency === 'USD' ? fallbackUsdReference : convertCurrencyValue(fallbackUsdReference, 'USD', reportCurrency); if (Number.isFinite(convertedUsd) && convertedUsd > 0) { baseAmount = convertedUsd; baseCurrency = reportCurrency; } else { baseAmount = fallbackUsdReference; baseCurrency = 'USD'; } } else { const convertedEffective = convertCurrencyValue(miner.total_cost_amount, effectiveCurrency, reportCurrency); if (Number.isFinite(convertedEffective) && convertedEffective > 0) { baseAmount = convertedEffective; baseCurrency = reportCurrency; } } } else if (Number.isFinite(referenceAmount) && referenceAmount > 0 && hasFiatReference) { baseAmount = referenceAmount; baseCurrency = referenceCurrency; } else if (Number.isFinite(fallbackUsdReference) && fallbackUsdReference > 0) { baseAmount = fallbackUsdReference; baseCurrency = 'USD'; } const dailyAmount = deriveDailyCostAmount(miner.total_cost_amount, miner.runtime_months, miner.purchased_at); const dailyCurrency = String(miner.daily_cost_currency || miner.currency || '').toUpperCase(); const dailyReportAmount = Number.isFinite(dailyAmount) && dailyCurrency ? (dailyCurrency === reportCurrency ? dailyAmount : convertCurrencyValue(dailyAmount, dailyCurrency, reportCurrency)) : null; const totalHashrateKh = toKhPerSecond(miner.mining_speed_value, miner.mining_speed_unit) + toKhPerSecond(miner.bonus_speed_value, miner.bonus_speed_unit); const settledAmount = Number(miner.settled_value_amount); const settledCurrency = String(miner.settled_value_currency || '').toUpperCase(); const hasHistoricalSettlement = Number.isFinite(settledAmount) && settledAmount > 0 && settledCurrency; const runtimeDays = runtimeDaysForEntry(miner.purchased_at, miner.runtime_months); const costPerKhAmount = totalHashrateKh > 0 && Number.isFinite(runtimeDays) && runtimeDays > 0 ? ((paymentType === 'crypto' && hasHistoricalSettlement ? settledAmount : Number(miner.total_cost_amount)) / totalHashrateKh / runtimeDays) : null; const costPerKhCurrency = paymentType === 'crypto' && hasHistoricalSettlement ? settledCurrency : effectiveCurrency; return { id: `purchase-${miner.id}`, source: 'miete', starts_at: miner.purchased_at, label: miner.label, runtime_months: miner.runtime_months, auto_renew: effectiveAutoRenew, effective_amount: miner.total_cost_amount, effective_currency: miner.currency, daily_cost_amount: dailyAmount, daily_cost_currency: dailyCurrency, daily_cost_report_amount: Number.isFinite(dailyReportAmount) ? dailyReportAmount : null, total_hashrate_kh: totalHashrateKh > 0 ? totalHashrateKh : null, runtime_days: Number.isFinite(runtimeDays) && runtimeDays > 0 ? runtimeDays : null, cost_per_kh_amount: Number.isFinite(costPerKhAmount) && costPerKhAmount > 0 ? costPerKhAmount : null, cost_per_kh_currency: costPerKhCurrency, base_amount: paymentType === 'crypto' && hasHistoricalSettlement ? settledAmount : baseAmount, base_currency: paymentType === 'crypto' && hasHistoricalSettlement ? settledCurrency : baseCurrency, base_label: paymentType === 'crypto' && hasHistoricalSettlement ? 'Fix zum Mietzeitpunkt' : 'Basis', miner_id: miner.id, miner_offer_id: miner.miner_offer_id, payment_type: paymentType, is_active: miner.is_active !== false && coverage.isCovered, end_at: coverage.endAt, can_toggle_auto_renew: paymentType !== 'crypto' && Number(miner.runtime_months) > 0, toggle_resource: 'purchased-miners', toggle_id: miner.id, 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) => { const coverage = entryCoverageMeta(plan.starts_at, plan.runtime_months, plan.auto_renew); const dailyAmount = deriveDailyCostAmount(plan.total_cost_amount, plan.runtime_months, plan.starts_at); const dailyCurrency = String(plan.daily_cost_currency || plan.currency || '').toUpperCase(); const dailyReportAmount = Number.isFinite(dailyAmount) && dailyCurrency ? (dailyCurrency === reportCurrency ? dailyAmount : convertCurrencyValue(dailyAmount, dailyCurrency, reportCurrency)) : null; const totalHashrateKh = toKhPerSecond(plan.mining_speed_value, plan.mining_speed_unit) + toKhPerSecond(plan.bonus_speed_value, plan.bonus_speed_unit); const runtimeDays = runtimeDaysForEntry(plan.starts_at, plan.runtime_months); const costPerKhAmount = totalHashrateKh > 0 && Number.isFinite(runtimeDays) && runtimeDays > 0 ? Number(plan.total_cost_amount) / totalHashrateKh / runtimeDays : null; return { id: `plan-${plan.id}`, source: 'manual', starts_at: plan.starts_at, label: plan.label, runtime_months: plan.runtime_months, auto_renew: !!plan.auto_renew, effective_amount: plan.total_cost_amount, effective_currency: plan.currency, daily_cost_amount: dailyAmount, daily_cost_currency: dailyCurrency, daily_cost_report_amount: Number.isFinite(dailyReportAmount) ? dailyReportAmount : null, total_hashrate_kh: totalHashrateKh > 0 ? totalHashrateKh : null, runtime_days: Number.isFinite(runtimeDays) && runtimeDays > 0 ? runtimeDays : null, cost_per_kh_amount: Number.isFinite(costPerKhAmount) && costPerKhAmount > 0 ? costPerKhAmount : null, cost_per_kh_currency: plan.currency, base_amount: plan.base_price_amount, base_currency: currentSettings.report_currency || 'EUR', payment_type: plan.payment_type, is_active: !!plan.is_active && coverage.isCovered, end_at: coverage.endAt, can_toggle_auto_renew: Number(plan.runtime_months) > 0, toggle_resource: 'cost-plans', toggle_id: plan.id, hashrate_text: formatHashrateWithBonus(plan.mining_speed_value, plan.mining_speed_unit, plan.bonus_speed_value, plan.bonus_speed_unit), type_label: 'Manuell eingetragen', }; })).sort((left, right) => String(right.starts_at || '').localeCompare(String(left.starts_at || ''))); const activeMinerRows = allMinerRows.filter((row) => row.is_active); const inactiveMinerRows = allMinerRows.filter((row) => !row.is_active); useEffect(() => { if (reportCurrencyOverride) { setCookie('mining_checker_report_currency', reportCurrencyOverride, 60 * 60 * 24 * 30); } }, [reportCurrencyOverride]); useEffect(() => { const intervalId = window.setInterval(() => setNowTimestamp(Date.now()), 60000); return () => window.clearInterval(intervalId); }, []); useEffect(() => { if (selectedMinerScenarioId === null) { return; } const exists = scenarioMinerOffers.some((offer) => String(offer.id) === String(selectedMinerScenarioId)); if (!exists) { setSelectedMinerScenarioId(null); } }, [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); } }, selectedOfferType === 'crypto' ? 1000 : 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; } const selectedOffer = scenarioMinerOffers.find((offer) => String(purchaseMinerForm.offer_id) === String(offer.id)) || scenarioMinerOffers[0] || null; if (!selectedOffer) { return; } setPurchaseMinerForm((current) => ({ ...current, offer_id: current.offer_id || String(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 || ''), bonus_percent: current.bonus_percent || (selectedOffer.bonus_percent !== null && selectedOffer.bonus_percent !== undefined ? String(selectedOffer.bonus_percent) : ''), 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, scenarioMinerOffers, purchaseMinerForm.offer_id, currentSettings.crypto_currency]); function measurementFxRate(measurementId, fromCurrency, toCurrency) { const from = String(fromCurrency || '').toUpperCase(); const to = String(toCurrency || '').toUpperCase(); if (!from || !to) { return null; } if (from === to) { return 1; } const measurement = measurements.find((row) => Number(row.id) === Number(measurementId)); const fetchId = measurement && measurement.fx_fetch_id !== null && measurement.fx_fetch_id !== undefined ? String(measurement.fx_fetch_id) : ''; const snapshots = payload && payload.fx_snapshots && typeof payload.fx_snapshots === 'object' ? payload.fx_snapshots : {}; const snapshot = fetchId && snapshots[fetchId] && typeof snapshots[fetchId] === 'object' ? snapshots[fetchId] : null; if (snapshot) { const baseCurrency = String(snapshot.base_currency || '').toUpperCase(); const rates = snapshot.rates && typeof snapshot.rates === 'object' ? snapshot.rates : {}; const directRate = from === baseCurrency ? rates[to] : null; if (Number.isFinite(Number(directRate)) && Number(directRate) > 0) { return Number(directRate); } const inverseRate = to === baseCurrency ? rates[from] : null; if (Number.isFinite(Number(inverseRate)) && Number(inverseRate) > 0) { return 1 / Number(inverseRate); } const fromRate = from === baseCurrency ? 1 : Number(rates[from]); const toRate = to === baseCurrency ? 1 : Number(rates[to]); if (Number.isFinite(fromRate) && Number.isFinite(toRate) && fromRate > 0 && toRate > 0) { return toRate / fromRate; } } const priceQuotes = measurement && measurement.price_quotes && typeof measurement.price_quotes === 'object' ? measurement.price_quotes : null; if (priceQuotes && from === 'DOGE') { const directQuote = Number(priceQuotes[to]); if (Number.isFinite(directQuote) && directQuote > 0) { return directQuote; } } if (priceQuotes && to === 'DOGE') { const inverseQuote = Number(priceQuotes[from]); if (Number.isFinite(inverseQuote) && inverseQuote > 0) { return 1 / inverseQuote; } } return null; } function convertMeasurementMoney(measurement, value, targetCurrency) { if (!measurement || value === null || value === undefined) { return null; } const sourceCurrency = String(measurement.effective_price_currency || measurement.price_currency || '').toUpperCase(); const target = String(targetCurrency || '').toUpperCase(); const numericValue = Number(value); if (!sourceCurrency || !target || !Number.isFinite(numericValue)) { return null; } if (sourceCurrency === target) { return numericValue; } const rate = measurementFxRate(measurement.id, sourceCurrency, target) ?? latestFxHistoryRate(sourceCurrency, target); return rate === null ? null : numericValue * rate; } function convertCurrencyValue(value, sourceCurrency, targetCurrency) { const from = String(sourceCurrency || '').toUpperCase(); const to = String(targetCurrency || '').toUpperCase(); const numericValue = Number(value); if (!from || !to || !Number.isFinite(numericValue)) { return null; } if (from === to) { return numericValue; } const rate = latest && latest.id ? (measurementFxRate(latest.id, from, to) ?? latestFxHistoryRate(from, to)) : latestFxHistoryRate(from, to); return rate === null ? null : numericValue * rate; } function latestFxHistoryRate(fromCurrency, toCurrency) { const from = String(fromCurrency || '').toUpperCase(); const to = String(toCurrency || '').toUpperCase(); if (!from || !to) { return null; } if (from === to) { return 1; } const rows = Array.isArray(fxHistory) ? fxHistory : []; for (const row of rows) { const rowBase = String(row.base_currency || '').toUpperCase(); const rowTarget = String(row.target_currency || row.currency_code || '').toUpperCase(); const rowRate = Number(row.rate); if (!Number.isFinite(rowRate) || rowRate <= 0) { continue; } if (rowBase === from && rowTarget === to) { return rowRate; } if (rowBase === to && rowTarget === from) { return 1 / rowRate; } } return null; } async function loadSchemaStatus(key) { try { const schema = await request(`${apiBase}/projects/${encodeURIComponent(key)}/schema-status`, { timeoutMs: 4000 }); const normalized = normalizeSchemaStatus(schema); setSchemaStatus(normalized); return normalized; } catch (err) { setSchemaStatus(normalizeSchemaStatus(null)); setError((previous) => previous || `Schema-Status konnte nicht geladen werden: ${err.message}`); return null; } } async function loadBootstrap(key, options) { const suppressError = !!(options && options.suppressError); const schemaStatusOverride = options && options.schemaStatusOverride ? options.schemaStatusOverride : null; const cacheKey = `${key}:${activeTab || 'overview'}`; const cachedPayload = bootstrapCacheRef.current.get(cacheKey) || null; setLoading(!cachedPayload); setError(''); if (cachedPayload) { setPayload(cachedPayload); } else { setPayload((previous) => previous || normalizeBootstrap(null, key)); } let loadGuardTriggered = false; const loadGuard = window.setTimeout(() => { loadGuardTriggered = true; setLoading(false); setPayload((previous) => previous || cachedPayload || normalizeBootstrap(null, key)); if (!suppressError) { setError((previous) => previous || 'Bootstrap-Request haengt oder braucht zu lange.'); } }, 30000); try { const effectiveSchemaStatus = schemaStatusOverride || schemaStatus; if (effectiveSchemaStatus.missing_count > 0 || effectiveSchemaStatus.pending_upgrade_count > 0) { setPayload(normalizeBootstrap(null, key)); setError('Mining-Checker Schema ist noch nicht initialisiert. Bitte im Tab Settings die Datenbank initialisieren.'); return false; } const params = new URLSearchParams({ view: activeTab || 'overview' }); const data = await request(`${apiBase}/projects/${encodeURIComponent(key)}/bootstrap?${params.toString()}`, { timeoutMs: 25000 }); const normalized = normalizeBootstrap(data, key); bootstrapCacheRef.current.set(cacheKey, normalized); setPayload(normalized); setSettingsForm({ baseline_measured_at: normalized.settings.baseline_measured_at || '', baseline_coins_total: normalized.settings.baseline_coins_total || '', report_currency: normalized.settings.report_currency || 'EUR', crypto_currency: normalized.settings.crypto_currency || 'DOGE', crypto_base_price_amount: normalized.settings.crypto_base_price_amount !== null && normalized.settings.crypto_base_price_amount !== undefined ? String(normalized.settings.crypto_base_price_amount) : '5.49', crypto_base_price_currency: normalized.settings.crypto_base_price_currency || 'USD', min_offer_runtime_months: normalized.settings.min_offer_runtime_months !== null && normalized.settings.min_offer_runtime_months !== undefined ? String(normalized.settings.min_offer_runtime_months) : '24', target_offer_hashrate_kh: normalized.settings.target_offer_hashrate_kh !== null && normalized.settings.target_offer_hashrate_kh !== undefined ? String(normalized.settings.target_offer_hashrate_kh) : '50', target_offer_runtime_months: normalized.settings.target_offer_runtime_months !== null && normalized.settings.target_offer_runtime_months !== undefined ? String(normalized.settings.target_offer_runtime_months) : '36', }); setOfferPreviewForm((current) => ({ crypto_base_price_amount: normalized.settings.crypto_base_price_amount !== null && normalized.settings.crypto_base_price_amount !== undefined ? String(normalized.settings.crypto_base_price_amount) : (current.crypto_base_price_amount || '5.49'), crypto_base_price_currency: normalized.settings.crypto_base_price_currency || current.crypto_base_price_currency || 'USD', fiat_base_price_amount: current.fiat_base_price_amount || '', })); setMinerOfferFilters((previous) => ({ ...previous, runtime_months: previous.runtime_months || String(normalized.settings.min_offer_runtime_months ?? 24), })); setFxSelection(Array.isArray(normalized.settings.preferred_currencies) && normalized.settings.preferred_currencies.length ? normalized.settings.preferred_currencies : ['DOGE', 'USD', 'EUR']); setTargetForm((previous) => ({ ...previous, currency: normalized.settings.currencies?.[0]?.code || previous.currency || 'EUR', })); setCostPlanForm((previous) => ({ ...previous, currency: normalized.settings.currencies?.[0]?.code || previous.currency || 'EUR', })); return true; } catch (err) { if (!suppressError) { setError(err.message); } setPayload(normalizeBootstrap(null, key)); return false; } finally { window.clearTimeout(loadGuard); if (!loadGuardTriggered) { setLoading(false); } } } async function reloadBootstrapAfterMutation(successMessage) { const refreshed = await loadBootstrap(projectKey, { suppressError: true }); if (!refreshed) { setError('Speichern war erfolgreich, aber die Ansicht konnte nicht automatisch aktualisiert werden.'); if (successMessage) { setMessage(successMessage); } } return refreshed; } function invalidateProjectBootstrapCache(key) { const prefix = `${key}:`; Array.from(bootstrapCacheRef.current.keys()).forEach((cacheKey) => { if (String(cacheKey).startsWith(prefix)) { bootstrapCacheRef.current.delete(cacheKey); } }); } function applySavedPayout(savedPayout) { if (!savedPayout || typeof savedPayout !== 'object') { return; } setPayload((previous) => { const current = previous || normalizeBootstrap(null, projectKey); const previousPayouts = Array.isArray(current.settings?.payouts) ? current.settings.payouts : []; const savedId = String(savedPayout.id ?? ''); const payouts = previousPayouts .filter((row) => savedId === '' || String(row?.id ?? '') !== savedId) .concat(savedPayout) .sort((left, right) => String(left?.payout_at || '').localeCompare(String(right?.payout_at || ''))); const totalCoins = payouts.reduce((sum, row) => { const amount = Number(row?.coins_amount); return Number.isFinite(amount) ? sum + amount : sum; }, 0); return { ...current, settings: { ...(current.settings || {}), payouts, }, summary: { ...(current.summary || {}), payouts: { ...(current.summary?.payouts || {}), total_count: payouts.length, total_coins: totalCoins, }, }, }; }); } useEffect(() => { loadBootstrap(projectKey); }, [projectKey, activeTab]); useEffect(() => { if (activeTab !== 'mining') { return; } loadFxHistory(projectKey); loadOfferBasisHistory(projectKey); }, [activeTab, projectKey]); useEffect(() => { if (!Array.isArray(offerBasisHistory) || offerBasisHistory.length === 0) { return; } const latestCrypto = offerBasisHistory.find((entry) => String(entry.offer_type || '').toLowerCase() === 'crypto') || null; const latestFiat = offerBasisHistory.find((entry) => String(entry.offer_type || '').toLowerCase() === 'fiat') || null; setOfferPreviewForm((current) => ({ crypto_base_price_amount: current.crypto_base_price_amount || (latestCrypto && latestCrypto.base_price_amount !== null && latestCrypto.base_price_amount !== undefined ? String(latestCrypto.base_price_amount) : ''), crypto_base_price_currency: current.crypto_base_price_currency || latestCrypto?.base_price_currency || 'USD', fiat_base_price_amount: current.fiat_base_price_amount || (latestFiat && latestFiat.base_price_amount !== null && latestFiat.base_price_amount !== undefined ? String(latestFiat.base_price_amount) : ''), })); }, [offerBasisHistory]); useEffect(() => { if (activeTab !== 'wallet') { return; } loadWalletTransfers(projectKey); loadWalletWithdrawals(projectKey); }, [activeTab, projectKey]); useEffect(() => { syncMiningCheckerTabButtons(activeTab); function handleNavigationClick(event) { const link = event.target instanceof Element ? event.target.closest('.module-tabs a[href*="/module/mining-checker"]') : null; if (!link) { return; } const nextTab = miningCheckerViewFromHref(link.getAttribute('href') || ''); if (!nextTab || nextTab === activeTab) { return; } event.preventDefault(); window.history.pushState({ miningCheckerView: nextTab }, '', `/module/mining-checker?view=${encodeURIComponent(nextTab)}`); setActiveTab(nextTab); } function handlePopState() { const nextTab = miningCheckerViewFromHref(window.location.href) || 'overview'; setActiveTab(nextTab); } document.addEventListener('click', handleNavigationClick); window.addEventListener('popstate', handlePopState); return () => { document.removeEventListener('click', handleNavigationClick); window.removeEventListener('popstate', handlePopState); }; }, [activeTab]); useEffect(() => { loadSchemaStatus(projectKey); loadModuleAuth(); }, [projectKey]); useEffect(() => { async function loadSavedDashboards() { if (!payload || !currentDashboards.length) { return; } const next = {}; for (const definition of currentDashboards) { const params = new URLSearchParams({ x_field: definition.x_field, y_field: definition.y_field, aggregation: definition.aggregation || 'none', }); if (definition.filters && definition.filters.source) { params.set('source', definition.filters.source); } if (definition.filters && definition.filters.currency) { params.set('currency', definition.filters.currency); } try { next[definition.id] = await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/dashboard-data?${params.toString()}`); } catch (err) { next[definition.id] = []; } } setDashboardData(next); } loadSavedDashboards(); }, [payload, projectKey]); const overviewCharts = useMemo(() => { const overviewWindowDays = Number(payload?.bootstrap_meta?.overview_window_days || 15); const overviewRows = recentMeasurementWindow(measurements, overviewWindowDays); const performanceByDay = new Map(); const pricingByDay = new Map(); overviewRows.forEach((row) => { const rate = Number(row.doge_per_day_interval); const hashrate = Number(row.active_hashrate_mh); const dayKey = String(row.measured_date || '').trim() || String(row.measured_at || '').slice(0, 10); if (dayKey) { if (Number.isFinite(rate) && rate >= 0) { const existing = performanceByDay.get(dayKey) || { rateSum: 0, rateCount: 0, hashrateSum: 0, hashrateCount: 0, label: fmtDate(dayKey) }; existing.rateSum += rate; existing.rateCount += 1; if (Number.isFinite(hashrate) && hashrate >= 0) { existing.hashrateSum += hashrate; existing.hashrateCount += 1; } performanceByDay.set(dayKey, existing); } else if (Number.isFinite(hashrate) && hashrate >= 0) { const existing = performanceByDay.get(dayKey) || { rateSum: 0, rateCount: 0, hashrateSum: 0, hashrateCount: 0, label: fmtDate(dayKey) }; existing.hashrateSum += hashrate; existing.hashrateCount += 1; performanceByDay.set(dayKey, existing); } } const rawPrice = row.effective_price_per_coin !== null && row.effective_price_per_coin !== undefined ? row.effective_price_per_coin : row.price_per_coin; const sourcePrice = Number(rawPrice); const convertedPrice = Number.isFinite(sourcePrice) && sourcePrice > 0 ? convertMeasurementMoney(row, sourcePrice, reportCurrency) : null; if (!dayKey || !Number.isFinite(Number(convertedPrice))) { return; } const priceEntry = pricingByDay.get(dayKey) || { sum: 0, count: 0, label: fmtDate(dayKey) }; priceEntry.sum += Number(convertedPrice); priceEntry.count += 1; pricingByDay.set(dayKey, priceEntry); }); return { performance: { series: [ { key: 'doge-per-day', label: overviewPerDayLabel, color: '#2dd4bf', axis: 'left', formatter: (value) => `${fmtNumber(value, 4)} ${currentMiningCurrency}`, data: Array.from(performanceByDay.entries()) .sort((left, right) => String(left[0]).localeCompare(String(right[0]))) .map(([, entry]) => ({ x: entry.label, y: entry.rateCount > 0 ? (entry.rateSum / entry.rateCount) : null, })) .filter((point) => Number.isFinite(Number(point.y))), }, { key: 'hashrate', label: 'Aktive Hashrate', color: '#60a5fa', axis: 'right', formatter: (value) => `${fmtNumber(value, 4)} MH/s`, data: Array.from(performanceByDay.entries()) .sort((left, right) => String(left[0]).localeCompare(String(right[0]))) .map(([, entry]) => ({ x: entry.label, y: entry.hashrateCount > 0 ? (entry.hashrateSum / entry.hashrateCount) : null, })) .filter((point) => Number.isFinite(Number(point.y))), }, ], }, pricing: Array.from(pricingByDay.entries()) .sort((left, right) => String(left[0]).localeCompare(String(right[0]))) .map(([, entry]) => ({ x: entry.label, y: entry.count > 0 ? (entry.sum / entry.count) : null, formatted: entry.count > 0 ? fmtMoney(entry.sum / entry.count, reportCurrency) : null, })) .filter((point) => Number.isFinite(Number(point.y))), }; }, [currentMiningCurrency, measurements, overviewPerDayLabel, payload?.bootstrap_meta?.overview_window_days, reportCurrency]); async function saveMeasurement(raw, successMessage) { setSaving(true); setError(''); setMessage(''); try { await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/measurements`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(raw), }); invalidateProjectBootstrapCache(projectKey); setMessage(successMessage); setMeasurementForm({ measured_at: '', coins_total: '', price_per_coin: '', price_currency: '', note: '', source: 'manual', }); setOcrPreview(null); await reloadBootstrapAfterMutation(successMessage); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function submitMeasurement(fromPreview, previewOverride) { const preview = normalizeOcrPreview(previewOverride || ocrPreview); const raw = fromPreview ? { ...preview.suggested, coin_currency: currentMiningCurrency, image_path: preview.image_path, ocr_raw_text: preview.raw_text, ocr_confidence: preview.confidence, ocr_flags: preview.flags, } : { ...measurementForm, coin_currency: currentMiningCurrency, }; return saveMeasurement(raw, fromPreview ? 'OCR-Vorschlag bestaetigt und gespeichert.' : 'Messpunkt gespeichert.'); } async function saveWalletSnapshot(raw, successMessage) { 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(successMessage); setOcrPreview(null); await reloadBootstrapAfterMutation(successMessage); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function submitHistoricalRecovery(event) { event.preventDefault(); const offer = scenarioMinerOffers.find((item) => String(item.payment_type || '').toLowerCase() === 'crypto' && Number(item.runtime_months) === 36 && Math.abs(toKhPerSecond(item.mining_speed_value, item.mining_speed_unit) - 50) < 0.0001); if (!offer) { setError('Das Referenzangebot 50 kH/s fuer 36 Monate konnte nicht geladen werden. Bitte Miner-Angebote pruefen und erneut versuchen.'); return; } setSaving(true); setError(''); setMessage(''); try { await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/historical-recovery-fx`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ measured_at: recoveryForm.miner_at, doge_usd: recoveryForm.doge_usd, usd_eur: recoveryForm.usd_eur, }), }); await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/miner-offers/${encodeURIComponent(offer.id)}/purchase`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ computed_offer: offer, purchased_at: recoveryForm.miner_at, label: 'Crypto Server · 50 kH/s · 36 Monate', mining_speed_value: '50', mining_speed_unit: 'kH/s', bonus_percent: '20', total_cost_amount: recoveryForm.miner_cost, currency: currentMiningCurrency, auto_renew: false, note: 'Historischer Nachtrag: +10 kH/s Bonus; zuvor HSH in DOGE gewandelt.', }), }); await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/payouts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ payout_at: recoveryForm.transfer_at, coins_amount: recoveryForm.transfer_coins, payout_currency: currentMiningCurrency, note: 'Historischer Nachtrag: Transfer nach HSH-zu-DOGE-Umwandlung.', }), }); await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/wallet-snapshots`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ measured_at: recoveryForm.wallet_at, wallet_balance: recoveryForm.wallet_coins, wallet_currency: currentMiningCurrency, balances_json: { [currentMiningCurrency]: { balance: Number(recoveryForm.wallet_coins) } }, source: 'manual', note: 'Historischer Nachtrag: autoritativer Wallet-Bestand.', }), }); await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/measurements`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ measured_at: recoveryForm.mining_at, coins_total: recoveryForm.mining_coins, coin_currency: currentMiningCurrency, price_per_coin: recoveryForm.mining_price_usd, price_currency: 'USD', source: 'manual', note: 'Historischer Nachtrag: CT-Pool-Screenshot.', }), }); await loadBootstrap(projectKey); setMessage('Historische Daten vollstaendig nachgetragen.'); } catch (err) { setError(`Nachtrag wurde nicht vollstaendig gespeichert: ${err.message}`); } finally { setSaving(false); } } async function repairHistoricalRecoveryFx() { setSaving(true); setError(''); setMessage(''); try { await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/historical-recovery-fx`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ measured_at: recoveryForm.miner_at, doge_usd: recoveryForm.doge_usd, usd_eur: recoveryForm.usd_eur, }), }); await loadBootstrap(projectKey); setMessage('Historischer FX-Snapshot und zugehoeriger Miner wurden korrigiert.'); } catch (err) { setError(`Historischer FX-Snapshot konnte nicht korrigiert werden: ${err.message}`); } finally { setSaving(false); } } async function submitWalletSnapshotFromPreview(previewOverride) { const preview = normalizeOcrPreview(previewOverride || 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, }; return saveWalletSnapshot(raw, 'Wallet-Snapshot gespeichert.'); } async function deleteMeasurement(id) { if (!id) { return; } if (!window.confirm('Diesen Messpunkt wirklich loeschen?')) { return; } setSaving(true); setError(''); setMessage(''); try { await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/measurements/${encodeURIComponent(id)}`, { method: 'DELETE', }); invalidateProjectBootstrapCache(projectKey); setMessage('Messpunkt geloescht.'); await reloadBootstrapAfterMutation('Messpunkt geloescht.'); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function submitMeasurementImport(event) { event.preventDefault(); setSaving(true); setError(''); setMessage(''); try { const result = await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/measurements-import`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(importForm), timeoutMs: 20000, }); invalidateProjectBootstrapCache(projectKey); const summary = [ `${result.imported || 0} importiert`, `${result.duplicates_ignored || 0} Duplikate ignoriert`, `${result.error_count || 0} Fehler`, ].join(', '); setMessage(`Import abgeschlossen: ${summary}.`); if (!result.error_count) { setImportForm((previous) => ({ ...previous, rows_text: '' })); } await reloadBootstrapAfterMutation(`Import abgeschlossen: ${summary}.`); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function loadOcrPreview(file, overrides) { const nextForm = { ...ocrForm, ...(overrides || {}), image: file || null, }; if (!nextForm.image) { setOcrPreview(null); setError('Bitte ein Bild auswaehlen.'); return; } setOcrForm(nextForm); setSaving(true); setError(''); setMessage(''); try { const body = new FormData(); 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', currentWalletPrimaryCurrency); body.append('mining_currency_hint', currentMiningCurrency); const data = await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/ocr-preview`, { method: 'POST', body, timeoutMs: 45000, }); const preview = normalizeOcrPreview(data); const isWalletPreview = preview.kind === 'wallet'; const hasMiningSuggestion = preview.suggested.coins_total !== '' && preview.suggested.coins_total !== null; const hasWalletSuggestion = preview.suggested_wallet.wallet_balance !== '' && preview.suggested_wallet.wallet_balance !== null; const hasUsableOcrSuggestion = isWalletPreview ? hasWalletSuggestion : hasMiningSuggestion; setOcrPreview(preview); if (hasUsableOcrSuggestion && preview.confidence >= OCR_AUTO_SAVE_CONFIDENCE_THRESHOLD) { if (isWalletPreview) { await submitWalletSnapshotFromPreview(preview); } else { await submitMeasurement(true, preview); } return; } setMessage('OCR-Ergebnis geladen. Bei Bedarf direkt speichern.'); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function submitDashboard(event) { event.preventDefault(); setSaving(true); setError(''); setMessage(''); try { await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/dashboards`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...dashboardForm, is_active: true, filters: dashboardForm.filters, }), }); setMessage('Dashboard gespeichert.'); await reloadBootstrapAfterMutation('Dashboard gespeichert.'); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function submitSettings(event) { event.preventDefault(); setSaving(true); setError(''); try { await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/settings`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ baseline_measured_at: settingsForm.baseline_measured_at, baseline_coins_total: settingsForm.baseline_coins_total, daily_cost_amount: currentSettings.daily_cost_amount, daily_cost_currency: currentSettings.daily_cost_currency, report_currency: settingsForm.report_currency || 'EUR', crypto_currency: settingsForm.crypto_currency || 'DOGE', crypto_base_price_amount: settingsForm.crypto_base_price_amount || '5.49', crypto_base_price_currency: settingsForm.crypto_base_price_currency || 'USD', min_offer_runtime_months: Number(settingsForm.min_offer_runtime_months) || 24, target_offer_hashrate_kh: Number(settingsForm.target_offer_hashrate_kh) || 50, target_offer_runtime_months: Number(settingsForm.target_offer_runtime_months) || 36, preferred_currencies: Array.isArray(currentSettings.preferred_currencies) ? currentSettings.preferred_currencies : fxSelection, }), }); setMessage('Settings gespeichert.'); await reloadBootstrapAfterMutation('Settings gespeichert.'); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function loadModuleAuth() { try { const auth = await request('/api/module-auth/mining-checker/index.php', { timeoutMs: 5000 }); setModuleAuthForm({ required: !!auth.required, users: Array.isArray(auth.users) ? auth.users.join(', ') : '', groups: Array.isArray(auth.groups) ? auth.groups.join(', ') : '', }); } catch (err) { setError(err.message); } } async function submitModuleAuth(event) { event.preventDefault(); setSaving(true); setError(''); try { await request('/api/module-auth/mining-checker/index.php', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ required: !!moduleAuthForm.required, users: moduleAuthForm.users, groups: moduleAuthForm.groups, }), }); setMessage('Modulrechte gespeichert.'); await loadModuleAuth(); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function submitTarget(event) { event.preventDefault(); setSaving(true); setError(''); try { await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/targets`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(targetForm), }); setMessage('Ziel gespeichert.'); setTargetForm({ label: '', target_amount_fiat: '', currency: currencies[0]?.code || 'EUR', miner_offer_id: '', is_active: true, sort_order: 0 }); setTargetModalOpen(false); await reloadBootstrapAfterMutation('Ziel gespeichert.'); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function deleteTarget(target) { const label = target?.label || 'dieses Ziel'; if (!window.confirm(`Soll ${label} wirklich geloescht werden?`)) { return; } setSaving(true); setError(''); try { await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/targets/${encodeURIComponent(target.id)}`, { method: 'DELETE', }); setMessage('Ziel geloescht.'); await reloadBootstrapAfterMutation('Ziel geloescht.'); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function toggleMinerAutoRenew(row) { if (!row || !row.can_toggle_auto_renew || !row.toggle_resource || !row.toggle_id) { return; } setSaving(true); setError(''); try { await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/${encodeURIComponent(row.toggle_resource)}/${encodeURIComponent(row.toggle_id)}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ auto_renew: !row.auto_renew }), }); setMessage(`Automatische Verlängerung ${row.auto_renew ? 'deaktiviert' : 'aktiviert'}.`); await reloadBootstrapAfterMutation(`Automatische Verlaengerung ${row.auto_renew ? 'deaktiviert' : 'aktiviert'}.`); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function submitCostPlan(event) { event.preventDefault(); setSaving(true); setError(''); try { await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/cost-plans`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(costPlanForm), }); setMessage('Miner gespeichert.'); setCostPlanForm({ label: '', starts_at: '', runtime_months: 1, mining_speed_value: '', mining_speed_unit: 'MH/s', bonus_percent: '', auto_renew: true, base_price_amount: '', payment_type: 'fiat', total_cost_amount: '', currency: currencies[0]?.code || 'EUR', note: '', is_active: true, }); setCostPlanModalOpen(false); await reloadBootstrapAfterMutation('Miner gespeichert.'); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function submitPayout(event) { event.preventDefault(); setSaving(true); setError(''); setMessage(''); try { const savedPayout = await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/payouts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payoutForm), timeoutMs: 8000, }); applySavedPayout(savedPayout); setWalletTransferRows((previous) => previous.filter((row) => String(row.id) !== String(savedPayout.id)).concat(savedPayout).sort((left, right) => String(left?.payout_at || '').localeCompare(String(right?.payout_at || '')))); invalidateProjectBootstrapCache(projectKey); setPayoutForm({ payout_at: '', coins_amount: '', payout_currency: currentMiningCurrency, note: '' }); setPayoutModalOpen(false); await reloadBootstrapAfterMutation('Wallet-Transfer gespeichert.'); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function submitFullPayout() { if (!canTransferAll) { setError(transferAllHint); return; } setSaving(true); setError(''); setMessage(''); try { const savedPayout = await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/payouts`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ transfer_mode: 'full_latest', note: 'Kompletttransfer aus letztem Upload', }), timeoutMs: 8000, }); applySavedPayout(savedPayout); setWalletTransferRows((previous) => previous.filter((row) => String(row.id) !== String(savedPayout.id)).concat(savedPayout).sort((left, right) => String(left?.payout_at || '').localeCompare(String(right?.payout_at || '')))); invalidateProjectBootstrapCache(projectKey); await reloadBootstrapAfterMutation('Gesamter aktueller Miner-Bestand wurde ins NC Wallet uebertragen.'); } catch (err) { setError(err.message); } finally { setSaving(false); } } 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', }); setWalletTransferRows((previous) => previous.filter((row) => String(row.id) !== String(transfer.id))); invalidateProjectBootstrapCache(projectKey); await reloadBootstrapAfterMutation('Wallet-Transfer geloescht.'); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function loadWalletTransfers(key) { try { const result = await request(`${apiBase}/projects/${encodeURIComponent(key)}/payouts`, { timeoutMs: 6000 }); setWalletTransferRows(Array.isArray(result) ? result : []); } catch (_err) { setWalletTransferRows([]); } } async function submitWalletWithdrawal(event) { event.preventDefault(); setSaving(true); setError(''); setMessage(''); try { await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/wallet-withdrawals`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(walletWithdrawalForm), timeoutMs: 8000, }); invalidateProjectBootstrapCache(projectKey); await loadWalletWithdrawals(projectKey); setMessage('Wallet-Auszahlung gespeichert.'); setWalletWithdrawalForm({ withdrawal_at: '', coins_amount: '', withdrawal_currency: currentWalletPrimaryCurrency, note: '', }); setWalletWithdrawalModalOpen(false); await reloadBootstrapAfterMutation('Wallet-Auszahlung gespeichert.'); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function loadWalletWithdrawals(key) { try { const result = await request(`${apiBase}/projects/${encodeURIComponent(key)}/wallet-withdrawals`, { timeoutMs: 6000 }); setWalletWithdrawalRows(Array.isArray(result) ? result : []); } catch (_err) { setWalletWithdrawalRows([]); } } async function deleteWalletWithdrawal(withdrawal) { if (!withdrawal || !withdrawal.id) { return; } const label = `${fmtNumber(withdrawal.coins_amount, 6)} ${withdrawal.withdrawal_currency || ''}`.trim(); if (!window.confirm(`Diese Wallet-Auszahlung (${label}) wirklich loeschen?`)) { return; } setSaving(true); setError(''); setMessage(''); try { await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/wallet-withdrawals/${encodeURIComponent(withdrawal.id)}`, { method: 'DELETE', }); setWalletWithdrawalRows((previous) => previous.filter((row) => String(row.id) !== String(withdrawal.id))); invalidateProjectBootstrapCache(projectKey); await reloadBootstrapAfterMutation('Wallet-Auszahlung geloescht.'); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function purchaseMinerOffer(offerId, overrides) { setSaving(true); setError(''); try { await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/miner-offers/${offerId}/purchase`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(overrides || { purchased_at: nowDateTimeLocalValue() }), }); setMessage('Miner als gemietet erfasst.'); setPurchaseMinerForm({ offer_id: '', base_offer_id: '', purchased_at: '', label: '', mining_speed_value: '', mining_speed_unit: '', bonus_percent: '', total_cost_amount: '', currency: '', reference_price_amount: '', reference_price_currency: '', auto_renew: false, note: '', }); setPurchaseMinerModalOpen(false); await loadBootstrap(projectKey); } catch (err) { setError(err.message); } finally { setSaving(false); } } 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) { setError('Bitte ein Miner-Angebot auswaehlen.'); return; } const selectedOffer = scenarioMinerOffers.find((offer) => String(offer.id) === String(purchaseMinerForm.offer_id)); if (!selectedOffer) { setError('Bitte ein gueltiges Miner-Angebot auswaehlen.'); return; } 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, bonus_percent: purchaseMinerForm.bonus_percent || null, purchased_at: purchaseMinerForm.purchased_at || nowDateTimeLocalValue(), total_cost_amount: purchaseMinerForm.total_cost_amount || null, currency: purchaseMinerForm.currency || null, reference_price_amount: purchaseMinerForm.reference_price_amount || null, reference_price_currency: purchaseMinerForm.reference_price_currency || null, auto_renew: !!purchaseMinerForm.auto_renew, note: purchaseMinerForm.note || '', }); } async function initializeModule(event) { event.preventDefault(); setSaving(true); setError(''); setMessage(''); try { const result = await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/initialize`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(initForm), }); const nextStatus = normalizeSchemaStatus(result.after); setSchemaStatus(nextStatus); setMessage( `${result.message} Vorhanden: ${nextStatus.present_count}/${nextStatus.required_tables.length}. ` + (Array.isArray(result.dropped_tables) && result.dropped_tables.length ? `Geloeschte Tabellen: ${result.dropped_tables.join(', ')}.` : 'Keine Tabellen geloescht.') ); try { await loadBootstrap(projectKey); } catch (bootstrapError) { setError(`Schema wurde initialisiert, aber Bootstrap-Daten konnten nicht geladen werden: ${bootstrapError.message}`); } } catch (err) { setError(err.message); } finally { setSaving(false); } } async function upgradeDatabaseSchema() { setSaving(true); setError(''); setMessage(''); try { const result = await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/upgrade`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, }); const nextStatus = normalizeSchemaStatus(result.after); setSchemaStatus(nextStatus); setMessage( `${result.message} ` + (Array.isArray(result.upgraded) && result.upgraded.length ? `Angewendete Upgrades: ${result.upgraded.join(', ')}.` : 'Keine Upgrades erforderlich.') ); const refreshedStatus = await loadSchemaStatus(projectKey); await loadBootstrap(projectKey, { schemaStatusOverride: refreshedStatus || nextStatus }); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function importOldData() { if (!window.confirm('Alte Mining-Checker Daten ueber alle Tabellen sichern, Schema neu aufbauen und danach importieren?')) { return; } setSaving(true); setError(''); setMessage(''); try { const result = await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/rebuild-preserve-core`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, }); const restored = result.restored && typeof result.restored === 'object' ? result.restored : {}; const restoredParts = Object.keys(restored) .filter((key) => Number(restored[key]) > 0) .map((key) => `${key}: ${restored[key]}`); setMessage( `${result.message || 'Alte Daten wurden importiert.'} ` + (restoredParts.length ? `Importiert: ${restoredParts.join(', ')}.` : 'Keine Altdaten gefunden.') ); await loadSchemaStatus(projectKey); await loadBootstrap(projectKey); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function migrateLegacyFxData() { if (!window.confirm('Legacy-FX-Rates aus dem Mining-Checker nach fx-rates migrieren und Messpunkte auf die neuen fetch_id-Verweise aktualisieren?')) { return; } setSaving(true); setError(''); setMessage(''); try { const result = await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/legacy-fx-migrate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, timeoutMs: 30000, }); setMessage( `${result.message || 'Legacy-FX-Rates wurden migriert.'} ` + `Fetches gefunden: ${Number(result.legacy_fetches_found || 0)}, ` + `neu importiert: ${Number(result.fx_fetches_imported || 0)}, ` + `wiederverwendet: ${Number(result.fx_fetches_reused || 0)}, ` + `Messpunkte aktualisiert: ${Number(result.measurements_updated || 0)}, ` + `offen: ${Number(result.measurements_unresolved || 0)}.` ); await loadBootstrap(projectKey); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function importSqlFile() { if (!sqlImportFile) { setError('Bitte zuerst eine SQL-Datei auswaehlen.'); setMessage(''); return; } setSaving(true); setError(''); setMessage(''); try { const body = new FormData(); body.append('sql_file', sqlImportFile); const result = await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/sql-import`, { method: 'POST', body, timeoutMs: 30000, }); setSqlImportFile(null); setMessage( `${result.message || 'SQL-Datei wurde importiert.'} ` + `${result.statement_count || 0} Statements aus ${result.file || 'der Datei'} ausgefuehrt.` ); await loadSchemaStatus(projectKey); await loadBootstrap(projectKey); } catch (err) { setError(err.message); } finally { setSaving(false); } } async function testDatabaseConnection() { setSaving(true); setError(''); setMessage(''); try { const result = await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/connection-test`); setDbCheck(result); setMessage(`DB-Verbindung erfolgreich. Driver: ${result.driver}, Datenbank: ${result.database}.`); } catch (err) { setDbCheck(null); setError(err.message); } finally { setSaving(false); } } async function loadFxHistory(key) { try { const result = await request(`${apiBase}/projects/${encodeURIComponent(key)}/fx-history`, { timeoutMs: 6000 }); setFxHistory(Array.isArray(result) ? result : []); } catch (err) { setFxHistory([]); } } async function loadOfferBasisHistory(key) { try { const result = await request(`${apiBase}/projects/${encodeURIComponent(key)}/offer-basis-history`, { timeoutMs: 6000 }); setOfferBasisHistory(Array.isArray(result) ? result : []); } catch (err) { setOfferBasisHistory([]); } } async function rememberOfferBasisInput(offerType, amount, currency) { const numericAmount = Number(amount); if (!Number.isFinite(numericAmount) || numericAmount <= 0) { return; } try { await request(`${apiBase}/projects/${encodeURIComponent(projectKey)}/offer-basis-history`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ offer_type: offerType, base_price_amount: numericAmount, base_price_currency: offerType === 'crypto' ? (currency || 'USD') : 'EUR', }), }); await loadOfferBasisHistory(projectKey); } catch (err) { setError(err.message); } } function resetReportCurrencyOverride() { setReportCurrencyOverride(''); setCookie('mining_checker_report_currency', '', 0); } function renderSharedOcrPanel() { const preview = normalizeOcrPreview(ocrPreview); const isWalletPreview = preview.kind === 'wallet'; const hasMiningSuggestion = preview.suggested.coins_total !== '' && preview.suggested.coins_total !== null; const hasWalletSuggestion = preview.suggested_wallet.wallet_balance !== '' && preview.suggested_wallet.wallet_balance !== null; const hasUsableOcrSuggestion = isWalletPreview ? hasWalletSuggestion : hasMiningSuggestion; 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('Wallet-Bestand', `${fmtNumber(preview.suggested_wallet.wallet_balance, 8)} ${preview.suggested_wallet.wallet_currency || currentWalletPrimaryCurrency}`), 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, asset]) => { const balance = asset && typeof asset === 'object' ? asset.balance : asset; const priceAmount = asset && typeof asset === 'object' ? asset.price_amount : null; const priceCurrency = asset && typeof asset === 'object' ? asset.price_currency : null; return `${fmtNumber(balance, 8)} ${code}${priceAmount ? ` @ ${fmtNumber(priceAmount, 6)} ${priceCurrency || ''}`.trim() : ''}`; }) .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'), isWalletPreview && hasMiningSuggestion ? h('button', { key: 'force-mining', type: 'button', className: 'mc-button mc-button--ghost', onClick: () => submitMeasurement(true), disabled: saving, }, 'Als Mining speichern') : null, !isWalletPreview && hasWalletSuggestion ? h('button', { key: 'force-wallet', type: 'button', className: 'mc-button mc-button--ghost', onClick: () => submitWalletSnapshotFromPreview(), disabled: saving, }, 'Als Wallet speichern') : null, ]) : h('div', { key: 'ocr-empty', className: 'mc-empty' }, 'Noch kein Screenshot ausgewaehlt.'), ]); } return h('div', { className: 'mc-grid-bg', }, [ h('div', { key: 'shell', className: 'window-app-shell mc-shell' }, [ h('div', { key: 'frame', className: 'window-app-frame' }, [ h('aside', { key: 'sidebar', className: 'window-app-sidebar mc-sidebar' }, [ h('div', { key: 'brand', className: 'window-app-brand mc-sidebar-brand' }, [ h('div', { key: 'kicker', className: 'window-app-kicker mc-kicker' }, 'Modul'), h('h1', { key: 'title', className: 'mc-sidebar-title' }, 'Mining-Checker'), h('p', { key: 'copy', className: 'mc-text' }, 'Erfassung, OCR-Auswertung und Analyse von Mining-Messwerten in einer gemeinsamen Modulansicht.'), ]), h('div', { key: 'nav', className: 'window-app-nav-list mc-nav-list' }, sectionEntries.map(([key, label]) => h('button', { key, type: 'button', className: cx('window-app-nav-button mc-nav-button', activeTab === key && 'is-active'), onClick: () => { if (key === activeTab) { return; } window.history.pushState({ miningCheckerView: key }, '', `/module/mining-checker?view=${encodeURIComponent(key)}`); setActiveTab(key); }, }, [ h('strong', { key: 'label' }, label), h('span', { key: 'meta', className: 'window-app-meta mc-nav-meta' }, currentSectionSummary(key)), ])) ), ]), h('main', { key: 'main', className: 'window-app-main mc-main' }, [ h('section', { key: 'hero', className: 'window-app-hero mc-hero-panel' }, [ h('div', { key: 'hero-copy', className: 'mc-hero-copy' }, [ h('div', { key: 'hero-kicker', className: 'window-app-kicker mc-kicker' }, 'Mining-Checker'), h('h2', { key: 'hero-title', className: 'window-app-title mc-section-title' }, currentSectionLabel(activeTab)), h('p', { key: 'hero-text', className: 'mc-text' }, currentSectionSummary(activeTab)), ]), h('div', { key: 'hero-badges', className: 'mc-inline-row mc-inline-row--wrap' }, [ h(Badge, { key: 'project', tone: 'info' }, `Projekt ${projectKey}`), h(Badge, { key: 'report-currency', tone: 'info' }, `Report ${reportCurrency}`), latest && latest.measured_at ? h(Badge, { key: 'latest', tone: 'success' }, `Letzter Upload ${fmtDate(latest.measured_at)}`) : h(Badge, { key: 'latest-empty', tone: 'warn' }, 'Noch kein Upload'), ]), ]), h('div', { key: 'content', className: 'mc-stack' }, [ error ? h('div', { key: 'error', className: 'mc-alert mc-alert--error' }, error) : null, message ? h('div', { key: 'message', className: 'mc-alert mc-alert--success' }, message) : null, loading ? h('div', { key: 'loading', className: 'mc-alert mc-alert--warning' }, 'Mining-Checker Daten werden aktualisiert …') : null, renderTab(), renderWalletModals(), ]), ]), ]), ]), ]); function renderTab() { const currentCoinCurrency = currentMiningCurrency; const perDayLabel = `${currentCoinCurrency} pro Tag`; if (activeTab === 'overview') { const latestPriceSource = latest && latest.effective_price_per_coin !== null && latest.effective_price_per_coin !== undefined ? latest.effective_price_per_coin : (latest ? latest.price_per_coin : null); const latestCoinPriceUsd = latest && latestPriceSource !== null && latestPriceSource !== undefined ? convertMeasurementMoney(latest, latestPriceSource, 'USD') : null; const dailyRevenue = latest ? convertMeasurementMoney(latest, latest.theoretical_daily_revenue, reportCurrency) : null; const dailyProfit = latest ? convertMeasurementMoney(latest, latest.theoretical_daily_profit, reportCurrency) : null; const dailyCost = latest ? convertMeasurementMoney(latest, latest.effective_daily_cost, reportCurrency) : null; const breakEvenPrice = latest && latest.break_even_price_per_coin !== null && latest.break_even_price_per_coin !== undefined ? convertMeasurementMoney(latest, latest.break_even_price_per_coin, reportCurrency) : null; const breakEvenRemainingAmount = latest ? convertMeasurementMoney(latest, latest.break_even_remaining_amount, reportCurrency) : null; 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.cash_invested_capital ?? latest.invested_capital, reportCurrency) : null; const reinvestedCapital = latest ? convertMeasurementMoney(latest, latest.reinvested_capital, reportCurrency) : null; const totalHoldingsValue = latest ? convertMeasurementMoney(latest, latest.total_holdings_value, reportCurrency) : null; const earnedValue = latest ? convertMeasurementMoney(latest, latest.earned_value, reportCurrency) : null; const settledCryptoSpendValue = latest ? convertMeasurementMoney(latest, latest.settled_crypto_spend_value, reportCurrency) : null; const fixedFiatSpendValue = investedCapital; const totalSpendValue = (settledCryptoSpendValue !== null || fixedFiatSpendValue !== null) ? (Number(settledCryptoSpendValue) || 0) + (Number(fixedFiatSpendValue) || 0) : null; const fixedCryptoSpendCurrentAsset = latest && latest.fixed_crypto_spend_current_asset !== null && latest.fixed_crypto_spend_current_asset !== undefined ? Number(latest.fixed_crypto_spend_current_asset) : null; const breakEvenReached = breakEvenRemainingAmount !== null && breakEvenRemainingAmount <= 0; const breakEvenEta = latest && latest.break_even_eta_at ? fmtDate(latest.break_even_eta_at) : null; const walletBalanceCurrentAssetDirect = payload?.summary?.payouts?.wallet_balance_current_asset_direct; const minerVisibleCoins = latest ? Number(latest.coins_total_visible ?? latest.coins_total) : null; const walletCurrentAssetCoins = walletBalanceCurrentAssetDirect !== null && walletBalanceCurrentAssetDirect !== undefined ? Number(walletBalanceCurrentAssetDirect) : null; const totalCurrentAssetCoins = Number.isFinite(walletCurrentAssetCoins) && Number.isFinite(minerVisibleCoins) ? walletCurrentAssetCoins + minerVisibleCoins : null; const minerVisibleValue = latest && latest.current_value !== null && latest.current_value !== undefined ? Number(latest.current_value) : ( latest && minerVisibleCoins !== null && latest.effective_price_per_coin !== null && latest.effective_price_per_coin !== undefined ? Number(minerVisibleCoins) * Number(latest.effective_price_per_coin) : null ); const minerValueUsd = latest && Number.isFinite(minerVisibleValue) ? convertMeasurementMoney(latest, minerVisibleValue, 'USD') : null; const minerValueReport = latest && Number.isFinite(minerVisibleValue) ? convertMeasurementMoney(latest, minerVisibleValue, reportCurrency) : null; 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.', [ h('div', { className: 'mc-inline-fields' }, [ selectField( 'Aktueller Besuch', reportCurrency, selectableCurrencies.map((currency) => currency.code), (value) => setReportCurrencyOverride(String(value || '').toUpperCase()) ), h('button', { type: 'button', className: 'mc-button mc-button--ghost', onClick: resetReportCurrencyOverride, disabled: !reportCurrencyOverride, }, 'Standard verwenden'), ]), ]), h('div', { key: 'stats', className: 'mc-stats-grid' }, [ h(StatCard, { key: 'coins', label: `${currentCoinCurrency} im Miner`, value: latest ? fmtNumber(minerVisibleCoins, 6) : 'n/a', sub: latest ? [ minerValueUsd !== null ? `USD ${fmtMoney(minerValueUsd, 'USD')}` : null, reportCurrency !== 'USD' && minerValueReport !== null ? `${reportCurrency} ${fmtMoney(minerValueReport, reportCurrency)}` : null, `Stand ${fmtDate(latest.measured_at)}`, ].filter(Boolean).join(' · ') : '', }), h(StatCard, { key: 'earned-overall', label: `Bisher verdient gesamt ${reportCurrency}`, value: earnedValue !== null ? fmtMoney(earnedValue, reportCurrency) : 'n/a', sub: [ totalHoldingsValue !== null ? `Variabel ${fmtMoney(totalHoldingsValue, reportCurrency)}` : null, currentSettings?.baseline_measured_at ? `Seit Baseline ${fmtDate(currentSettings.baseline_measured_at)}` : 'Benötigt Baseline', ].filter(Boolean).join(' · '), }), h(StatCard, { key: 'spent-overall', label: `Ausgaben gesamt ${reportCurrency}`, value: totalSpendValue !== null ? fmtMoney(totalSpendValue, reportCurrency) : 'n/a', sub: [ settledCryptoSpendValue !== null ? `Krypto fix ${fmtMoney(settledCryptoSpendValue, reportCurrency)}` : null, fixedFiatSpendValue !== null ? `FIAT ${fmtMoney(fixedFiatSpendValue, reportCurrency)}` : null, fixedCryptoSpendCurrentAsset !== null ? `Entspricht ${fmtNumber(fixedCryptoSpendCurrentAsset, 6)} ${currentCoinCurrency}` : null, settledCryptoSpendValue !== null ? 'Krypto-Kurs aus gespeichertem fx-rates-Snapshot' : null, ].filter(Boolean).join(' · '), }), h(StatCard, { key: 'perday', 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}` : ''), }), h(StatCard, { key: 'perday-since-payout', 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 kein Transfer vor dem letzten Upload', }), h(StatCard, { key: 'current-asset-balance', label: `${currentCoinCurrency} Bestand`, value: totalCurrentAssetCoins !== null ? `${fmtNumber(totalCurrentAssetCoins, 6)} ${currentCoinCurrency}` : 'n/a', sub: [ walletCurrentAssetCoins !== null && Number.isFinite(walletCurrentAssetCoins) ? `Wallet ${fmtNumber(walletCurrentAssetCoins, 6)} ${currentCoinCurrency}` : null, Number.isFinite(minerVisibleCoins) ? `Miner ${fmtNumber(minerVisibleCoins, 6)} ${currentCoinCurrency}` : null, totalCurrentAssetCoins !== null ? `Gesamt ${fmtNumber(totalCurrentAssetCoins, 6)} ${currentCoinCurrency}` : null, latestCoinPriceUsd !== null ? `Letzter ${currentCoinCurrency}-Kurs ${fmtNumber(latestCoinPriceUsd, 6)} USD${latest && latest.price_is_fallback ? ' · Fallback aus letztem Kurs' : ''}` : null, ].filter(Boolean).join(' · ') || 'Kein Wallet- oder Mining-Bestand vorhanden', }), h(StatCard, { key: 'preferred-offer-target', label: `Bis Zielminer ${fmtNumber(targetOfferHashrateKh, 0)} kH/s · ${targetOfferRuntimeMonths} Monate`, value: preferredOfferRemainingDays !== null ? (preferredOfferRemainingDays <= 0 ? 'Erreicht' : `${fmtNumber(preferredOfferRemainingDays, 2)} Tage`) : 'n/a', sub: [ preferredOfferRequiredCoins !== null ? `Preis ${fmtNumber(preferredOfferRequiredCoins, 4)} ${currentMiningCurrency}${preferredOfferRequiredFiatValue !== null ? ` · ≈ ${fmtMoney(preferredOfferRequiredFiatValue, reportCurrency)}` : ''}` : null, preferredOfferCurrentCoins !== null ? `Bestand ${fmtNumber(preferredOfferCurrentCoins, 6)} ${currentMiningCurrency}${preferredOfferCurrentFiatValue !== null ? ` · ≈ ${fmtMoney(preferredOfferCurrentFiatValue, reportCurrency)}` : ''}` : null, preferredOfferRemainingCoins !== null ? `Rest ${fmtNumber(preferredOfferRemainingCoins, 4)} ${currentMiningCurrency}` : null, latest?.doge_per_day_since_last_payout !== null && latest?.doge_per_day_since_last_payout !== undefined ? `Basis ${fmtNumber(latest.doge_per_day_since_last_payout, 4)} ${currentMiningCurrency}/Tag seit letztem Transfer` : null, preferredOfferEta ? `ETA ${fmtDate(preferredOfferEta)}` : null, ].filter(Boolean).join(' · '), }), h(StatCard, { key: 'profit', label: 'Theoretischer Tagesgewinn', value: dailyProfit !== null ? fmtMoney(dailyProfit, reportCurrency) : 'n/a', sub: dailyCost !== null ? `Tageskosten ${fmtMoney(dailyCost, reportCurrency)}` : 'Kein aktiver Miner fuer diese Waehrung', }), h(StatCard, { key: 'break-even-point', label: 'Gesamt-Break-even', value: breakEvenReached ? 'Erreicht' : breakEvenDaysOverall !== null ? `${fmtNumber(breakEvenDaysOverall, 2)} Tage` : ((investedCapital === null && reinvestedCapital === null) ? 'Keine Mietbasis' : 'Nicht erreichbar'), sub: (investedCapital !== null || reinvestedCapital !== null) ? `${breakEvenEta ? `ETA ${breakEvenEta} · ` : ''}Cash ${fmtMoney(investedCapital, reportCurrency)}${reinvestedCapital !== null ? ` · Reinvest fix ${fmtMoney(reinvestedCapital, reportCurrency)}` : ''}` : (breakEvenPrice !== null ? `Break-even-Kurs ${fmtNumber(breakEvenPrice, 6)} ${reportCurrency}` : (investedCapital === null && reinvestedCapital === null ? 'Noch keine Miner als Mietbasis hinterlegt' : 'Keine belastbare Break-even-Basis')), }), ]), h('div', { key: 'charts', className: 'mc-overview-grid' }, [ panel('Performance-Verlauf', `${perDayLabel}-Raten der letzten 15 Tage als Tagesdurchschnitt ohne negative Ausreißer plus aktive MH/s pro Tag.`, h(SimpleChart, { type: 'line', series: overviewCharts.performance.series, leftAxisLabel: perDayLabel, rightAxisLabel: 'Hashrate MH/s', })), panel('Kurs-Verlauf', `Durchschnittlicher Kurs pro Tag der letzten 15 Tage in ${reportCurrency}.`, h(SimpleChart, { type: 'line', data: overviewCharts.pricing, yLabel: `Kurs ${reportCurrency}`, })), ]), panel('Zielmonitor', `Die ETA wird ab dem letzten Upload mit dem Durchschnitt seit dem letzten Transfer berechnet; die Restzeit laeuft bis zum naechsten Upload weiter.`, h('div', { className: 'mc-target-grid' }, currentTargets.map((target, index) => h('div', { key: index, className: 'mc-target-card' }, [ h('div', { key: 'head', className: 'mc-flex-split' }, [ h('h3', { key: 'title' }, target.label), h(Badge, { key: 'status', tone: target.status === 'reached' ? 'success' : 'info' }, target.status), ]), 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 ${currentCoinCurrency}: ${fmtNumber(target.required_doge, 6)}`), h('div', { key: 'remaining' }, `Rest-${currentCoinCurrency}: ${fmtNumber(target.remaining_doge, 6)}`), h('div', { key: 'days' }, `Resttage: ${fmtNumber(remainingDaysUntil(target.target_eta_at), 4)}`), ]), ])) ) ), ]); } if (activeTab === 'recovery') { return h('div', { className: 'mc-stack' }, [ panel('Historische Daten nachtragen', 'Speichert die bekannten Ereignisse chronologisch: Miner-Miete, Transfer ins Wallet, autoritativer Wallet-Snapshot und Mining-Stand. Vor dem Speichern wird ein historischer FX-Snapshot fuer die Krypto-Miete benoetigt.', h('form', { className: 'mc-form', onSubmit: submitHistoricalRecovery, }, [ inputField('Miner-Miete', 'datetime-local', recoveryForm.miner_at, (value) => setRecoveryForm({ ...recoveryForm, miner_at: value }), '1'), inputField('Miner-Preis DOGE', 'number', recoveryForm.miner_cost, (value) => setRecoveryForm({ ...recoveryForm, miner_cost: value }), '0.000001'), inputField('Historisch DOGE/USD', 'number', recoveryForm.doge_usd, (value) => setRecoveryForm({ ...recoveryForm, doge_usd: value }), '0.00000001'), inputField('Historisch USD/EUR', 'number', recoveryForm.usd_eur, (value) => setRecoveryForm({ ...recoveryForm, usd_eur: value }), '0.00000001'), inputField('Transfer Mining -> Wallet', 'datetime-local', recoveryForm.transfer_at, (value) => setRecoveryForm({ ...recoveryForm, transfer_at: value }), '1'), inputField('Transfer DOGE', 'number', recoveryForm.transfer_coins, (value) => setRecoveryForm({ ...recoveryForm, transfer_coins: value }), '0.000001'), inputField('Wallet-Snapshot', 'datetime-local', recoveryForm.wallet_at, (value) => setRecoveryForm({ ...recoveryForm, wallet_at: value }), '1'), inputField('Wallet DOGE', 'number', recoveryForm.wallet_coins, (value) => setRecoveryForm({ ...recoveryForm, wallet_coins: value }), '0.00000001'), inputField('Mining-Screenshot', 'datetime-local', recoveryForm.mining_at, (value) => setRecoveryForm({ ...recoveryForm, mining_at: value }), '1'), inputField('Mining DOGE', 'number', recoveryForm.mining_coins, (value) => setRecoveryForm({ ...recoveryForm, mining_coins: value }), '0.000001'), inputField('DOGE/USD im Screenshot', 'number', recoveryForm.mining_price_usd, (value) => setRecoveryForm({ ...recoveryForm, mining_price_usd: value }), '0.000001'), h('button', { type: 'submit', className: 'mc-button mc-button--primary', disabled: saving, }, saving ? 'Speichert …' : 'Alle Daten nachtragen'), h('button', { type: 'button', className: 'mc-button mc-button--ghost', onClick: repairHistoricalRecoveryFx, disabled: saving, }, 'Nur historischen FX/Miner korrigieren'), ])), ]); } if (activeTab === 'measurements') { return h('div', { className: 'mc-stack' }, [ 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 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 }, [ h('td', { key: 'measured' }, fmtDate(row.measured_at)), 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)), h('td', { key: 'rate-payout' }, row.doge_per_day_since_last_payout !== null && row.doge_per_day_since_last_payout !== undefined ? `${fmtNumber(row.doge_per_day_since_last_payout, 4)}${row.last_payout_at ? ` seit ${fmtDate(row.last_payout_at)}` : ''}` : 'n/a'), h('td', { key: 'trend' }, row.trend_label), h('td', { key: 'note' }, row.note || row.ocr_flags.join(', ') || '—'), h('td', { key: 'action' }, h('button', { type: 'button', className: 'mc-button mc-button--ghost', onClick: () => deleteMeasurement(row.id), disabled: saving, }, 'Loeschen')), ])) ), ]), ])), ]); } if (activeTab === 'upload') { return h('div', { className: 'mc-stack' }, [ h('div', { className: 'mc-two-col' }, [ renderSharedOcrPanel(), panel('Mining manuell erfassen', 'Direkte Eingabe eines einzelnen Mining-Messpunkts. Ein belegter historischer Zeitpunkt wird unveraendert gespeichert.', h('form', { className: 'mc-form', onSubmit: function (event) { event.preventDefault(); submitMeasurement(false); }, }, [ inputField('Zeitpunkt', 'datetime-local', measurementForm.measured_at, (value) => setMeasurementForm({ ...measurementForm, measured_at: value }), '1'), inputField('Coins total', 'number', measurementForm.coins_total, (value) => setMeasurementForm({ ...measurementForm, coins_total: value }), '0.000001'), inputField('Kurs', 'number', measurementForm.price_per_coin, (value) => setMeasurementForm({ ...measurementForm, price_per_coin: value }), '0.000001'), selectField('Waehrung', measurementForm.price_currency, [''].concat(selectableCurrencies.map((currency) => currency.code)), (value) => setMeasurementForm({ ...measurementForm, price_currency: value })), textareaField('Notiz', measurementForm.note, (value) => setMeasurementForm({ ...measurementForm, note: value })), h('button', { type: 'submit', className: 'mc-button mc-button--secondary', disabled: saving, }, saving ? 'Speichert …' : 'Messpunkt speichern'), ])), importHelpOpen ? h('div', { className: 'mc-modal-backdrop', onClick: () => setImportHelpOpen(false) }, [ h('div', { key: 'modal', className: 'mc-modal', onClick: (event) => event.stopPropagation(), }, [ h('div', { key: 'head', className: 'mc-flex-split' }, [ h('h3', { key: 'title' }, 'Import-Hilfe'), h('button', { key: 'close', type: 'button', className: 'mc-button mc-button--ghost', onClick: () => setImportHelpOpen(false), }, 'Schliessen'), ]), h('div', { key: 'body', className: 'mc-form' }, [ displayField('Format', 'DD.MM.YYYY HH:MM | Coins | Kurs | Waehrung | Notiz'), h('div', { key: 'rules', className: 'mc-display-field' }, [ h('div', { key: 'rules-label', className: 'mc-field-label' }, 'Hinweise'), h('div', { key: 'rules-text', className: 'mc-text' }, [ 'Leere Zeilen sind erlaubt. ', 'Zeilen mit # oder // am Anfang werden ignoriert. ', 'Wenn ein Kurs gesetzt ist, muss auch eine Waehrung gesetzt sein. ', 'Duplikate werden automatisch ignoriert.' ]), ]), h('div', { key: 'example-wrap', className: 'mc-display-field' }, [ h('div', { key: 'example-label', className: 'mc-field-label' }, 'Beispiel'), h('pre', { key: 'example', className: 'mc-code-block' }, [ '21.03.2026 23:48 | 50.988525 | 0.09316 | USD | Screenshot importiert\n', '22.03.2026 08:10 | 51.402100 | 0.09420 | USD | Morgens\n', '22.03.2026 14:30 | 51.998700 | | | ohne Kurs' ]), ]), ]), ]), ]) : null, ]), panel('Mining-Import per Copy & Paste', 'Mehrere historische Mining-Messpunkte auf einmal einfuegen. Doppelte Eintraege werden ignoriert.', h('form', { className: 'mc-form', onSubmit: submitMeasurementImport, }, [ h('div', { className: 'mc-inline-row' }, [ h('button', { key: 'help', type: 'button', className: 'mc-button mc-button--ghost', onClick: () => setImportHelpOpen(true), }, 'Import-Hilfe'), ]), displayField('Format', 'DD.MM.YYYY HH:MM | Coins | Kurs | Waehrung | Notiz'), textareaField('Importdaten', importForm.rows_text, (value) => setImportForm({ ...importForm, rows_text: value })), selectField('Standard-Waehrung', importForm.default_currency, [''].concat(selectableCurrencies.map((currency) => currency.code)), (value) => setImportForm({ ...importForm, default_currency: value })), selectField('Import-Quelle', importForm.source, ['manual', 'seed_import'], (value) => setImportForm({ ...importForm, source: value })), h('button', { type: 'submit', className: 'mc-button mc-button--secondary', disabled: saving, }, saving ? 'Importiert …' : 'Import ausfuehren'), ])), ]); } if (activeTab === 'wallet') { const latestWalletSnapshot = currentWalletSnapshots.length ? currentWalletSnapshots[0] : null; const latestWalletAssets = latestWalletSnapshot && latestWalletSnapshot.balances_json && typeof latestWalletSnapshot.balances_json === 'object' ? Object.entries(latestWalletSnapshot.balances_json) : []; return h('div', { className: 'mc-stack' }, [ panel('NC Wallet-Bestaende', 'Der letzte Snapshot zeigt alle erkannten Assets des internen NC Wallet separat.', latestWalletAssets.length ? h('div', { className: 'mc-asset-grid' }, latestWalletAssets.map(([code, asset]) => { const balance = asset && typeof asset === 'object' ? asset.balance : asset; const priceAmount = asset && typeof asset === 'object' ? asset.price_amount : null; const priceCurrency = asset && typeof asset === 'object' ? asset.price_currency : null; return h('div', { key: code, className: 'mc-display-field mc-asset-card' }, [ h('div', { key: 'code', className: 'mc-field-label' }, code), h('div', { key: 'balance', className: 'mc-asset-balance' }, `${fmtNumber(balance, 8)} ${code}`), h('div', { key: 'price', className: 'mc-text' }, priceAmount !== null && priceAmount !== undefined ? `1 ${code} = ${fmtNumber(priceAmount, 6)} ${priceCurrency || ''}`.trim() : 'Kein Screenshot-Kurs erkannt'), ]); })) : h('div', { className: 'mc-empty' }, 'Noch keine Wallet-Assets erkannt.')), panel('Wallet-Transfers', 'Auszahlungen aus dem Mining-Tool erhoehen den Wallet-Bestand automatisch. Ein Screenshot setzt den Bestand zum Screenshot-Zeitpunkt verbindlich. Krypto-Minerkaeufe ziehen Coins wieder ab und bleiben zum Kaufkurs festgeschrieben.', [ h('div', { key: 'actions', className: 'mc-inline-row' }, [ h('button', { key: 'transfer-all', type: 'button', className: 'mc-button mc-button--secondary', onClick: submitFullPayout, disabled: saving || !canTransferAll, title: transferAllHint, }, 'Alles uebertragen'), h('button', { key: 'add-transfer', type: 'button', className: 'mc-button mc-button--secondary', onClick: () => { setPayoutMode('partial'); setPayoutForm((previous) => ({ payout_at: previous.payout_at || nowDateTimeLocalValue(), coins_amount: previous.coins_amount || '', payout_currency: currentMiningCurrency, note: previous.note || '', })); setPayoutModalOpen(true); }, }, 'Teiluebertragung'), ]), h('div', { key: 'transfer-last-import', className: 'mc-text' }, latest ? `Letzter Mining-Import: ${fmtDate(latest.measured_at)}` : 'Kein Mining-Upload vorhanden.'), h('div', { key: 'transfer-help', className: 'mc-text' }, transferAllHint), 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.'))] ), ]), ]), ]), panel('Wallet-Auszahlungen', 'Echte Auszahlungen vom NC Wallet an ein externes, hier nicht getracktes Wallet reduzieren den NC-Wallet-Bestand.', [ h('div', { key: 'actions', className: 'mc-inline-row' }, [ h('button', { key: 'add-wallet-withdrawal', type: 'button', className: 'mc-button mc-button--secondary', onClick: () => { setWalletWithdrawalForm((previous) => ({ withdrawal_at: previous.withdrawal_at || nowDateTimeLocalValue(), coins_amount: previous.coins_amount || '', withdrawal_currency: currentWalletPrimaryCurrency, note: previous.note || '', })); setWalletWithdrawalModalOpen(true); }, }, 'Aus NC Wallet auszahlen'), ]), h('div', { key: 'withdrawal-list', className: 'mc-table-shell' }, [ h('table', { key: 'withdrawal-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' }, currentWalletWithdrawals.length ? currentWalletWithdrawals.slice().reverse().map((withdrawal) => h('tr', { key: withdrawal.id }, [ h('td', { key: 'time' }, fmtDate(withdrawal.withdrawal_at)), h('td', { key: 'coins' }, fmtNumber(withdrawal.coins_amount, 6)), h('td', { key: 'currency' }, withdrawal.withdrawal_currency), h('td', { key: 'note' }, withdrawal.note || '—'), h('td', { key: 'action' }, h('button', { type: 'button', className: 'mc-button mc-button--ghost', onClick: () => deleteWalletWithdrawal(withdrawal), disabled: saving, }, 'Loeschen')), ])) : [h('tr', { key: 'empty' }, h('td', { colSpan: 5 }, 'Noch keine Wallet-Auszahlungen hinterlegt.'))] ), ]), ]), ]), panel('NC Wallet-Historie', 'Die letzten 10 NC-Wallet-Uploads mit allen aus dem Screenshot gelesenen Assets.', h('div', { className: 'mc-history-stack' }, currentWalletSnapshots.length ? currentWalletSnapshots.slice(0, 10).map((row) => h('article', { key: row.id, className: 'mc-history-card' }, [ h('div', { key: 'head', className: 'mc-flex-split' }, [ h('div', { key: 'time' }, [ h('h3', { key: 'timestamp' }, fmtDate(row.measured_at)), h('div', { key: 'source', className: 'mc-kicker' }, row.source || 'manual'), ]), ]), h('div', { key: 'assets', className: 'mc-history-asset-grid' }, Object.entries(row.balances_json || {}).map(([code, asset]) => { const balance = asset && typeof asset === 'object' ? asset.balance : asset; const priceAmount = asset && typeof asset === 'object' ? asset.price_amount : null; const priceCurrency = asset && typeof asset === 'object' ? asset.price_currency : null; return h('div', { key: code, className: 'mc-history-asset-card' }, [ h('strong', { key: 'code' }, code), h('span', { key: 'balance' }, `${fmtNumber(balance, 8)} ${code}`), priceAmount !== null && priceAmount !== undefined ? h('span', { key: 'price', className: 'mc-text' }, `1 ${code} = ${fmtNumber(priceAmount, 6)} ${priceCurrency || ''}`.trim()) : h('span', { key: 'price-empty', className: 'mc-text' }, 'Kein Kurs erkannt'), ]); }) ), ])) : h('div', { className: 'mc-empty' }, 'Noch keine NC-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', { className: 'mc-form', onSubmit: submitDashboard, }, [ inputField('Name', 'text', dashboardForm.name, (value) => setDashboardForm({ ...dashboardForm, name: value })), selectField('Chart-Typ', dashboardForm.chart_type, ['line', 'bar', 'area', 'table'], (value) => setDashboardForm({ ...dashboardForm, chart_type: value })), selectField('X-Feld', dashboardForm.x_field, ['measured_at', 'measured_date', 'source', 'price_currency', 'trend_label'], (value) => setDashboardForm({ ...dashboardForm, x_field: value })), selectField('Y-Feld', dashboardForm.y_field, ['coins_total', 'price_per_coin', 'growth_since_baseline', 'doge_per_hour_since_baseline', 'doge_per_day_since_baseline', 'doge_per_hour_interval', 'doge_per_day_interval', 'current_value', 'theoretical_daily_revenue', 'theoretical_daily_profit'], (value) => setDashboardForm({ ...dashboardForm, y_field: value })), selectField('Aggregation', dashboardForm.aggregation, ['none', 'sum', 'avg', 'min', 'max', 'count', 'latest'], (value) => setDashboardForm({ ...dashboardForm, aggregation: value })), selectField('Filter Quelle', dashboardForm.filters.source, ['', 'manual', 'image_ocr', 'seed_import'], (value) => setDashboardForm({ ...dashboardForm, filters: { ...dashboardForm.filters, source: value } })), selectField('Filter Waehrung', dashboardForm.filters.currency, [''].concat(selectableCurrencies.map((currency) => currency.code)), (value) => setDashboardForm({ ...dashboardForm, filters: { ...dashboardForm.filters, currency: value } })), h('button', { type: 'submit', className: 'mc-button mc-button--secondary', disabled: saving, }, saving ? 'Speichert …' : 'Dashboard speichern'), ])), h('div', { className: 'mc-stack' }, currentDashboards.map((definition) => h(DashboardCard, { key: definition.id, definition, data: dashboardData[definition.id], loading: !dashboardData[definition.id], }))), ]); } if (activeTab === 'mining') { const scenarioCurrency = selectedMinerScenario?.scenario_currency || reportCurrency; const scenarioCurrentDailyProfit = selectedMinerScenario ? convertMeasurementMoney(latest, selectedMinerScenario.scenario_current_daily_profit, reportCurrency) : null; const scenarioDailyProfit = selectedMinerScenario ? convertMeasurementMoney(latest, selectedMinerScenario.scenario_daily_profit, reportCurrency) : null; const scenarioDailyProfitDelta = selectedMinerScenario ? convertMeasurementMoney(latest, selectedMinerScenario.scenario_daily_profit_delta, reportCurrency) : null; const scenarioInvestedCapital = selectedMinerScenario ? convertMeasurementMoney(latest, selectedMinerScenario.scenario_invested_capital, reportCurrency) : null; const scenarioOfferCost = selectedMinerScenario ? convertMeasurementMoney(latest, selectedMinerScenario.scenario_offer_cost, reportCurrency) : null; const scenarioBreakEvenRemaining = selectedMinerScenario ? convertMeasurementMoney(latest, selectedMinerScenario.scenario_break_even_remaining_amount, reportCurrency) : null; const renderMinerTable = (rows, emptyText) => ( h('div', { className: 'mc-table-shell' }, [ h('table', { key: 'table', className: 'mc-table' }, [ h('thead', { key: 'head' }, h('tr', null, ['Label', 'Start', 'Laufzeit', 'Auto', 'Kosten Server', 'Kosten/kH/s/Tag', 'Waehrung', 'Aktiv', 'Aktion'].map((label) => h('th', { key: label }, label)))), h('tbody', { key: 'body' }, rows.length ? rows.map((row) => h('tr', { key: row.id }, [ h('td', { key: 'label' }, [ h('div', { key: 'main' }, row.label), h('div', { key: 'type', className: 'mc-kicker' }, row.type_label), ]), h('td', { key: 'start' }, fmtDateTime(row.starts_at)), h('td', { key: 'runtime' }, [ h('div', { key: 'months' }, `${row.runtime_months} Monate`), h('div', { key: 'hash', className: 'mc-kicker' }, row.hashrate_text), row.end_at ? h('div', { key: 'end', className: 'mc-kicker' }, `Ende ${fmtDate(row.end_at)}`) : null, ]), h('td', { key: 'renew' }, row.payment_type === 'crypto' ? 'nein' : (row.auto_renew ? 'ja' : 'nein')), h('td', { key: 'cost-total' }, [ h('div', { key: 'effective' }, fmtNumber(row.effective_amount, 6)), row.daily_cost_amount !== null && row.daily_cost_amount !== undefined && row.daily_cost_currency ? h('div', { key: 'daily', className: 'mc-kicker' }, [ `Pro Tag ${fmtNumber(row.daily_cost_amount, 6)} ${row.daily_cost_currency}`, row.daily_cost_report_amount !== null && row.daily_cost_report_amount !== undefined && row.daily_cost_currency !== reportCurrency ? ` · ${fmtMoney(row.daily_cost_report_amount, reportCurrency)}` : '', ].join('')) : null, row.base_amount !== null && row.base_amount !== undefined && row.base_currency ? h('div', { key: 'base', className: 'mc-kicker' }, `${row.base_label || 'Basis'} ${fmtNumber(row.base_amount, 6)} ${row.base_currency}`) : null, ]), h('td', { key: 'cost-kh' }, row.cost_per_kh_amount !== null && row.cost_per_kh_amount !== undefined ? [ h('div', { key: 'amount' }, fmtMoney(row.cost_per_kh_amount, row.cost_per_kh_currency || reportCurrency)), row.total_hashrate_kh !== null && row.total_hashrate_kh !== undefined ? h('div', { key: 'hash', className: 'mc-kicker' }, `bei ${fmtNumber(row.total_hashrate_kh, 4)} kH/s${row.runtime_days !== null && row.runtime_days !== undefined ? ` · ${fmtNumber(row.runtime_days, 2)} Tage` : ''}`) : null, ] : 'n/a'), h('td', { key: 'currency' }, [ h('div', { key: 'currency-main' }, row.effective_currency), row.payment_type ? h('div', { key: 'currency-mode', className: 'mc-kicker' }, row.payment_type === 'crypto' ? 'Zahlung Krypto' : 'Zahlung FIAT') : null, ]), h('td', { key: 'active' }, row.is_active ? 'ja' : 'nein'), h('td', { key: 'action' }, 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))] ), ]), ]) ); return h('div', { className: 'mc-stack' }, [ panel('Aktive Miner', 'Alle bereits gemieteten oder manuell eingetragenen Miner in einer gemeinsamen Liste.', [ h('div', { key: 'actions', className: 'mc-inline-row' }, [ h('button', { key: 'add-server', type: 'button', className: 'mc-button mc-button--secondary', onClick: () => setCostPlanModalOpen(true), }, 'Miner eintragen'), h('button', { key: 'rent-miner', type: 'button', className: 'mc-button mc-button--ghost', onClick: () => { setPurchaseMinerForm({ offer_id: '', base_offer_id: '', purchased_at: '', label: '', mining_speed_value: '', mining_speed_unit: '', bonus_percent: '', total_cost_amount: '', currency: '', reference_price_amount: '', reference_price_currency: '', auto_renew: false, note: '', }); setPurchaseMinerModalOpen(true); }, disabled: !scenarioMinerOffers.length, }, 'Neuen Miner mieten'), ]), renderMinerTable(activeMinerRows, 'Noch keine aktiven Miner hinterlegt.'), ]), panel('Inaktive Miner', 'Abgelaufene oder deaktivierte Miner werden separat gefuehrt.', [ renderMinerTable(inactiveMinerRows, 'Noch keine inaktiven Miner hinterlegt.'), ]), panel('Miner-Angebote', selectedOfferType === 'crypto' ? `Crypto-Angebote sind aktiv. Standardmäßig werden Laufzeiten ab ${fmtNumber(minOfferRuntimeMonths, 0)} Monaten berücksichtigt. Der 50-kH/s-Server mit 36 Monaten bleibt als Referenz immer sichtbar; der Wallet-Bestand begrenzt weder Anzeige noch Mieten.` : `Fiat-Angebote sind aktiv. Standardmäßig werden Laufzeiten ab ${fmtNumber(minOfferRuntimeMonths, 0)} Monaten berücksichtigt. Die Berechnung startet erst, wenn du eine Geschwindigkeit eingibst.`, [ h('div', { key: 'filters', className: 'mc-filter-grid' }, [ selectedOfferType === 'crypto' ? fieldWrapper('Basispreis fuer 50 kH/s auf 3 Monate', h('input', { className: 'mc-input', type: 'number', step: '0.000001', value: offerPreviewForm.crypto_base_price_amount, onChange: (event) => setOfferPreviewForm({ ...offerPreviewForm, crypto_base_price_amount: event.target.value }), onBlur: () => rememberOfferBasisInput('crypto', offerPreviewForm.crypto_base_price_amount, offerPreviewForm.crypto_base_price_currency || 'USD'), })) : fieldWrapper('Basispreis fuer 100 kH/s auf 3 Monate in EUR', h('input', { className: 'mc-input', type: 'number', step: '0.000001', value: offerPreviewForm.fiat_base_price_amount, onChange: (event) => setOfferPreviewForm({ ...offerPreviewForm, fiat_base_price_amount: event.target.value }), onBlur: () => rememberOfferBasisInput('fiat', offerPreviewForm.fiat_base_price_amount, 'EUR'), })), selectedOfferType === 'crypto' ? fieldWrapper('Basiswaehrung', h('select', { className: 'mc-select', value: offerPreviewForm.crypto_base_price_currency || 'USD', onChange: async (event) => { const nextCurrency = event.target.value || 'USD'; setOfferPreviewForm({ ...offerPreviewForm, crypto_base_price_currency: nextCurrency }); await rememberOfferBasisInput('crypto', offerPreviewForm.crypto_base_price_amount, nextCurrency); }, }, Array.from(new Set(['USD', String(currentSettings.crypto_currency || 'DOGE').toUpperCase()])).map((value) => h('option', { key: value, value }, value)))) : displayField('Basiswaehrung', 'EUR'), selectField('Angebotsart', minerOfferFilters.offer_type, [ { value: 'crypto', label: 'Crypto' }, { value: 'fiat', label: 'Fiat' }, ], (value) => setMinerOfferFilters({ ...minerOfferFilters, offer_type: value || 'crypto' })), inputField(`${selectedOfferType === 'fiat' ? 'Geschwindigkeit' : 'Min. Geschwindigkeit'} (${minerOfferFilters.speed_unit === 'kh' ? 'kH/s' : 'MH/s'})`, 'number', minerOfferFilters.speed_min, (value) => setMinerOfferFilters({ ...minerOfferFilters, speed_min: value }), '0.0001'), selectField('Geschwindigkeitseinheit', minerOfferFilters.speed_unit, [ { value: 'auto', label: 'MH/s' }, { value: 'kh', label: 'kH/s' }, ], (value) => setMinerOfferFilters({ ...minerOfferFilters, speed_unit: value || 'auto' })), selectedOfferType === 'crypto' ? inputField('Max. DOGE', 'number', minerOfferFilters.max_doge, (value) => setMinerOfferFilters({ ...minerOfferFilters, max_doge: value }), '0.0001') : null, inputField(`Max. Basispreis (${reportCurrency})`, 'number', minerOfferFilters.price_max, (value) => setMinerOfferFilters({ ...minerOfferFilters, price_max: value }), '0.0001'), selectField('Min. Laufzeit', minerOfferFilters.runtime_months, [{ value: '', label: 'Alle Laufzeiten' }].concat(Array.from(new Set(evaluatedMinerOffers.map((offer) => String(offer.runtime_months || '')).filter(Boolean))).sort((a, b) => Number(a) - Number(b)).map((value) => ({ value, label: `ab ${value} Monate`, }))), (value) => setMinerOfferFilters({ ...minerOfferFilters, runtime_months: value })), ]), visibleOfferBasisHistory.length ? h('div', { key: 'basis-history', className: 'mc-mini-grid' }, visibleOfferBasisHistory.map((entry) => h('div', { key: `basis-${entry.id}`, className: 'mc-mini-card', }, [ h('div', { key: 'label', className: 'mc-field-label' }, selectedOfferType === 'crypto' ? 'Crypto-Basis' : 'Fiat-Basis'), h('div', { key: 'value' }, `${fmtNumber(entry.base_price_amount, 6)} ${entry.base_price_currency}`), h('div', { key: 'sub', className: 'mc-kicker' }, entry.created_at ? fmtDate(entry.created_at) : 'Zeitpunkt unbekannt'), ]))) : null, h('div', { key: 'offers-table', className: 'mc-table-shell' }, [ h('table', { key: 'table', className: 'mc-table' }, [ h('thead', { key: 'head' }, h('tr', null, ['Label', 'Hashrate', 'Preis', 'Erwartet/Tag', 'Break-even', 'Empfehlung', 'Aktion'].map((label) => h('th', { key: label }, label)))), h('tbody', { key: 'body' }, visibleMinerOffers.length ? visibleMinerOffers.map((offer) => h('tr', { key: offer.id }, [ h('td', { key: 'label' }, [ h('div', { key: 'name' }, offer.label), offer.is_pinned_reference ? h('div', { key: 'reference', className: 'mc-kicker' }, 'Referenzangebot · 50 kH/s · 36 Monate') : null, ]), h('td', { key: 'hashrate' }, [ h('div', { key: 'base' }, formatSpeed(offer.mining_speed_value, offer.mining_speed_unit, 'Basis') || 'n/a'), Number(offer.bonus_speed_value) > 0 ? h('div', { key: 'bonus', className: 'mc-kicker' }, formatSpeed(offer.bonus_speed_value, offer.bonus_speed_unit, 'Bonus')) : h('div', { key: 'bonus', className: 'mc-kicker' }, 'Bonus 0'), h('div', { key: 'total', className: 'mc-kicker' }, `Gesamt ${formatAdaptiveSpeed(offer.offer_hashrate_mh)}`), ]), h('td', { key: 'price' }, [ offer.payment_type === 'crypto' ? [ offer.crypto_display_price_amount !== null && offer.crypto_display_price_currency ? h('div', { key: 'price-crypto' }, [ h('div', { key: 'amount' }, `${fmtNumber(offer.crypto_display_price_amount, 6)} ${offer.crypto_display_price_currency}`), (() => { const offerDailyAmount = deriveDailyCostAmount(offer.crypto_display_price_amount, offer.runtime_months); return offerDailyAmount !== null ? h('div', { key: 'daily', className: 'mc-kicker' }, `Pro Tag ${fmtNumber(offerDailyAmount, 6)} ${offer.crypto_display_price_currency}`) : null; })(), h('div', { key: 'label', className: 'mc-kicker' }, 'Zu zahlen in Krypto'), ]) : null, offer.usd_display_price_amount !== null && offer.usd_display_price_currency ? h('div', { key: 'price-usd' }, [ h('div', { key: 'amount' }, `${fmtNumber(offer.usd_display_price_amount, 6)} ${offer.usd_display_price_currency}`), (() => { const offerDailyAmount = deriveDailyCostAmount(offer.usd_display_price_amount, offer.runtime_months); return offerDailyAmount !== null ? h('div', { key: 'daily', className: 'mc-kicker' }, `Pro Tag ${fmtNumber(offerDailyAmount, 6)} ${offer.usd_display_price_currency}`) : null; })(), h('div', { key: 'label', className: 'mc-kicker' }, 'Zu zahlen in USD'), ]) : null, ] : [ h('div', { key: 'price-main' }, [ h('div', { key: 'amount' }, `${fmtNumber(offer.effective_price_amount, 6)} ${offer.effective_price_currency}`), (() => { const offerDailyAmount = deriveDailyCostAmount(offer.effective_price_amount, offer.runtime_months); return offerDailyAmount !== null ? h('div', { key: 'daily', className: 'mc-kicker' }, `Pro Tag ${fmtNumber(offerDailyAmount, 6)} ${offer.effective_price_currency}`) : null; })(), h('div', { key: 'label', className: 'mc-kicker' }, 'Zu zahlen'), ]), offer.base_price_amount !== null && offer.base_price_currency ? h('div', { key: 'price-base' }, [ h('div', { key: 'amount' }, `${fmtNumber(offer.base_price_amount, 6)} ${offer.base_price_currency}`), h('div', { key: 'label', className: 'mc-kicker' }, 'Gegenwert'), ]) : null, ], ]), 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), offer.base_price_amount !== null && offer.base_price_currency ? h('div', { key: 'rec-ref', className: 'mc-kicker' }, `Basis ${fmtNumber(offer.base_price_amount, 6)} ${offer.base_price_currency}`) : null, offer.payment_type ? h('div', { key: 'paytype', className: 'mc-kicker' }, offer.payment_type === 'crypto' ? `Zahlung in Krypto (${currentSettings.crypto_currency || 'DOGE'})` : `Zahlung in FIAT (${offer.base_price_currency || 'EUR'})`) : null, h('div', { key: 'renew', className: 'mc-kicker' }, offer.auto_renew ? 'Automatische Verlängerung' : 'Laeuft aus'), ]), h('td', { key: 'action' }, [ h('button', { key: 'scenario', type: 'button', className: cx('mc-button', String(selectedMinerScenario?.id) === String(offer.id) ? 'mc-button--secondary' : 'mc-button--ghost'), onClick: () => setSelectedMinerScenarioId(offer.id), }, 'Szenario'), h('button', { key: 'target', type: 'button', className: 'mc-button mc-button--ghost', onClick: () => { setTargetForm({ label: offer.label, target_amount_fiat: String(offer.base_price_amount ?? offer.effective_price_amount ?? ''), currency: offer.base_price_currency || offer.effective_price_currency || 'EUR', miner_offer_id: '', is_active: true, sort_order: 0, }); setTargetModalOpen(true); }, }, 'Als Ziel'), h('button', { key: 'buy', type: 'button', className: 'mc-button mc-button--ghost', onClick: () => { setPurchaseMinerForm({ offer_id: String(offer.id), base_offer_id: String(offer.id || ''), purchased_at: nowDateTimeLocalValue(), label: String(offer.label || ''), mining_speed_value: String(offer.mining_speed_value || ''), mining_speed_unit: String(offer.mining_speed_unit || ''), bonus_percent: offer.bonus_percent !== null && offer.bonus_percent !== undefined ? String(offer.bonus_percent) : '', total_cost_amount: offer.effective_price_amount !== null && offer.effective_price_amount !== undefined ? String(offer.effective_price_amount) : '', currency: offer.effective_price_currency || offer.base_price_currency || 'USD', reference_price_amount: offer.base_price_amount !== null && offer.base_price_amount !== undefined ? String(offer.base_price_amount) : '', reference_price_currency: offer.base_price_currency || '', auto_renew: !!offer.auto_renew, note: '', }); setPurchaseMinerModalOpen(true); }, disabled: saving, }, 'Mieten'), ]), ])) : [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.'))] ), ]), ]), selectedMinerScenario ? panel( `Szenario: ${selectedMinerScenario.label}`, 'Zeigt, wie sich Kennzahlen veraendern wuerden, wenn dieser Miner jetzt zusaetzlich gemietet wird.', [ h('div', { key: 'scenario-stats', className: 'mc-stats-grid' }, [ h(StatCard, { key: 'scenario-profit', label: 'Tagesgewinn Neu', value: scenarioDailyProfit !== null ? fmtMoney(scenarioDailyProfit, reportCurrency) : 'n/a', sub: scenarioDailyProfitDelta !== null ? `Aenderung pro Tag ${fmtMoney(scenarioDailyProfitDelta, reportCurrency)}` : 'Keine belastbare Gewinnprognose', }), h(StatCard, { key: 'scenario-doge', 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 ${currentCoinCurrency}/Tag-Basis`, }), h(StatCard, { key: 'scenario-break-even', label: 'Break-even Neu', value: selectedMinerScenario.scenario_break_even_days !== null ? `${fmtNumber(selectedMinerScenario.scenario_break_even_days, 2)} Tage` : 'n/a', sub: selectedMinerScenario.scenario_break_even_date ? `Theoretisch ${fmtDate(selectedMinerScenario.scenario_break_even_date)}` : 'Kein belastbares Break-even-Datum', }), h(StatCard, { key: 'scenario-capital', label: 'Kosten inkl. Miete', value: scenarioInvestedCapital !== null ? fmtMoney(scenarioInvestedCapital, reportCurrency) : 'n/a', sub: scenarioOfferCost !== null ? `Neue Miete ${fmtMoney(scenarioOfferCost, reportCurrency)}` : `Mietpreis in ${scenarioCurrency}`, }), h(StatCard, { key: 'scenario-two-year', label: '2 Jahre Ergebnis Neu', value: selectedMinerScenario.scenario_two_year_profit !== null ? fmtMoney(convertMeasurementMoney(latest, selectedMinerScenario.scenario_two_year_profit, reportCurrency), reportCurrency) : 'n/a', sub: selectedMinerScenario.scenario_two_year_profit_delta !== null ? `Aenderung ggü. heute ${fmtMoney(convertMeasurementMoney(latest, selectedMinerScenario.scenario_two_year_profit_delta, reportCurrency), reportCurrency)}` : 'Laufzeit und Verlaengerung beruecksichtigt', }), ]), h('div', { key: 'scenario-meta', className: 'mc-mini-grid' }, [ h('div', { key: 'hashrate', className: 'mc-mini-card' }, [ h('div', { key: 'label', className: 'mc-field-label' }, 'Hashrate'), h('div', { key: 'value' }, selectedMinerScenario.scenario_hashrate_mh !== null ? `${fmtNumber(selectedMinerScenario.scenario_hashrate_mh, 4)} MH/s` : 'n/a'), h('div', { key: 'sub', className: 'mc-kicker' }, selectedMinerScenario.scenario_current_hashrate_mh !== null ? `Aktuell ${fmtNumber(selectedMinerScenario.scenario_current_hashrate_mh, 4)} MH/s` : 'Aktuell n/a'), ]), h('div', { key: 'remaining', className: 'mc-mini-card' }, [ h('div', { key: 'label', className: 'mc-field-label' }, 'Offen bis Break-even'), h('div', { key: 'value' }, scenarioBreakEvenRemaining !== null ? fmtMoney(scenarioBreakEvenRemaining, reportCurrency) : 'n/a'), h('div', { key: 'sub', className: 'mc-kicker' }, scenarioCurrentDailyProfit !== null ? `Aktueller Tagesgewinn ${fmtMoney(scenarioCurrentDailyProfit, reportCurrency)}` : 'Aktueller Tagesgewinn n/a'), ]), ]), ] ) : h('div', { key: 'scenario-empty', className: 'mc-empty' }, 'Waehle bei einem Angebot "Szenario", um die Auswirkung hier anzuzeigen.'), ]), panel('Ziele', 'Ziele koennen direkt oder aus einem Miner-Angebot heraus angelegt werden.', [ h('div', { key: 'actions', className: 'mc-inline-row' }, [ h('button', { key: 'add-target', type: 'button', className: 'mc-button mc-button--secondary', onClick: () => setTargetModalOpen(true), }, 'Ziel anlegen'), ]), h('div', { key: 'target-list', className: 'mc-table-shell' }, [ h('table', { key: 'target-table', className: 'mc-table' }, [ h('thead', { key: 'head' }, h('tr', null, ['Label', 'Betrag', 'Waehrung', 'Resttage', 'Ziel erreicht ca.', 'Sortierung', 'Aktiv', 'Aktion'].map((label) => h('th', { key: label }, label)))), h('tbody', { key: 'body' }, currentTargets.length ? currentTargets.map((target) => h('tr', { key: target.id || target.label }, [ h('td', { key: 'label' }, target.label), h('td', { key: 'amount' }, fmtNumber(target.effective_target_amount_fiat ?? target.target_amount_fiat, 2)), h('td', { key: 'currency' }, [ h('div', { key: 'currency-main' }, target.effective_currency || target.currency), target.linked_offer_label ? h('div', { key: 'currency-offer', className: 'mc-kicker' }, `Angebot ${target.linked_offer_label}`) : null, ]), h('td', { key: 'days' }, remainingDaysUntil(target.target_eta_at) !== null ? fmtNumber(remainingDaysUntil(target.target_eta_at), 2) : 'n/a'), h('td', { key: 'eta' }, target.target_eta_at ? fmtDateTime(target.target_eta_at) : 'n/a'), h('td', { key: 'sort' }, String(target.sort_order ?? 0)), h('td', { key: 'active' }, target.is_active ? 'ja' : 'nein'), h('td', { key: 'action' }, h('button', { type: 'button', className: 'mc-button mc-button--ghost', onClick: () => deleteTarget(target), disabled: saving, }, 'Loeschen') ), ])) : [h('tr', { key: 'empty' }, h('td', { colSpan: 8 }, 'Noch keine Ziele hinterlegt.'))] ), ]), ]), ]), costPlanModalOpen ? renderModal('Miner eintragen', [ h('form', { key: 'form', className: 'mc-form', onSubmit: submitCostPlan }, [ inputField('Label', 'text', costPlanForm.label, (value) => setCostPlanForm({ ...costPlanForm, label: value })), inputField('Startdatum', 'datetime-local', costPlanForm.starts_at, (value) => setCostPlanForm({ ...costPlanForm, starts_at: value })), inputField('Laufzeit in Monaten', 'number', String(costPlanForm.runtime_months), (value) => setCostPlanForm({ ...costPlanForm, runtime_months: Number(value) || 0 })), inputField('Mining-Geschwindigkeit', 'number', costPlanForm.mining_speed_value, (value) => setCostPlanForm({ ...costPlanForm, mining_speed_value: value }), '0.0001'), selectField('Mining-Einheit', costPlanForm.mining_speed_unit, speedUnits, (value) => setCostPlanForm({ ...costPlanForm, mining_speed_unit: value })), inputField('Bonus-Hashrate in %', 'number', costPlanForm.bonus_percent, (value) => setCostPlanForm({ ...costPlanForm, bonus_percent: value }), '0.01'), inputField(`Basispreis in ${settingsForm.report_currency || 'EUR'}`, 'number', costPlanForm.base_price_amount, (value) => setCostPlanForm({ ...costPlanForm, base_price_amount: value }), '0.000001'), selectField('Zahlungsart', costPlanForm.payment_type, [{ value: 'fiat', label: 'FIAT' }, { value: 'crypto', label: 'Krypto' }], (value) => setCostPlanForm({ ...costPlanForm, payment_type: value })), textareaField('Notiz', costPlanForm.note, (value) => setCostPlanForm({ ...costPlanForm, note: value })), h('label', { className: 'mc-checkbox' }, [ h('input', { type: 'checkbox', checked: !!costPlanForm.auto_renew, onChange: (event) => setCostPlanForm({ ...costPlanForm, auto_renew: event.target.checked }) }), 'Automatisch verlaengernd', ]), h('label', { className: 'mc-checkbox' }, [ h('input', { type: 'checkbox', checked: !!costPlanForm.is_active, onChange: (event) => setCostPlanForm({ ...costPlanForm, is_active: event.target.checked }) }), 'Aktiv', ]), h('div', { className: 'mc-inline-row' }, [ h('button', { type: 'button', className: 'mc-button mc-button--ghost', onClick: () => setCostPlanModalOpen(false) }, 'Abbrechen'), h('button', { type: 'submit', className: 'mc-button mc-button--secondary', disabled: saving }, saving ? 'Speichert …' : 'Miner speichern'), ]), ]), ], () => setCostPlanModalOpen(false)) : null, payoutModalOpen ? renderModal(payoutMode === 'partial' ? 'Teiluebertragung ins NC Wallet' : 'Zu Wallet transferieren', [ h('form', { key: 'form', className: 'mc-form', onSubmit: submitPayout }, [ h('div', { key: 'hint', className: 'mc-text' }, 'Hier kannst du einen Teilbetrag vom aktuellen Miner-Bestand in das NC Wallet uebertragen.'), 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, [currentMiningCurrency].concat(selectableCurrencies.map((currency) => currency.code).filter((code) => code !== currentMiningCurrency)), (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 …' : 'Transfer speichern'), ]), ]), ], () => setPayoutModalOpen(false)) : null, walletWithdrawalModalOpen ? renderModal('Aus NC Wallet auszahlen', [ h('form', { key: 'form', className: 'mc-form', onSubmit: submitWalletWithdrawal }, [ inputField('Auszahlungszeitpunkt', 'datetime-local', walletWithdrawalForm.withdrawal_at, (value) => setWalletWithdrawalForm({ ...walletWithdrawalForm, withdrawal_at: value })), inputField('Coins', 'number', walletWithdrawalForm.coins_amount, (value) => setWalletWithdrawalForm({ ...walletWithdrawalForm, coins_amount: value }), '0.000001'), selectField('Waehrung', walletWithdrawalForm.withdrawal_currency, [currentWalletPrimaryCurrency].concat(selectableCurrencies.map((currency) => currency.code).filter((code) => code !== currentWalletPrimaryCurrency)), (value) => setWalletWithdrawalForm({ ...walletWithdrawalForm, withdrawal_currency: value })), textareaField('Notiz', walletWithdrawalForm.note, (value) => setWalletWithdrawalForm({ ...walletWithdrawalForm, note: value })), h('div', { className: 'mc-inline-row' }, [ h('button', { type: 'button', className: 'mc-button mc-button--ghost', onClick: () => setWalletWithdrawalModalOpen(false) }, 'Abbrechen'), h('button', { type: 'submit', className: 'mc-button mc-button--secondary', disabled: saving }, saving ? 'Speichert …' : 'Wallet-Auszahlung speichern'), ]), ]), ], () => setWalletWithdrawalModalOpen(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) => ({ value: String(offer.id), label: `${offer.label} · ${fmtNumber(offer.effective_price_amount, 6)} ${offer.effective_price_currency}`, }))), (value) => { const offer = scenarioMinerOffers.find((item) => String(item.id) === String(value)); setPurchaseMinerForm({ offer_id: value, 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) : '', mining_speed_unit: offer?.mining_speed_unit || '', bonus_percent: offer && offer.bonus_percent !== null && offer.bonus_percent !== undefined ? String(offer.bonus_percent) : '', total_cost_amount: offer && offer.effective_price_amount !== null && offer.effective_price_amount !== undefined ? String(offer.effective_price_amount) : '', currency: offer?.effective_price_currency || offer?.base_price_currency || 'USD', reference_price_amount: offer && offer.reference_price_amount !== null && offer.reference_price_amount !== undefined ? String(offer.reference_price_amount) : '', reference_price_currency: offer?.reference_price_currency || '', auto_renew: !!offer?.auto_renew, note: purchaseMinerForm.note || '', }); }), inputField('Mietdatum/-zeit', 'datetime-local', purchaseMinerForm.purchased_at, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, purchased_at: value })), inputField('Label', 'text', purchaseMinerForm.label, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, label: value })), inputField('Mining-Geschwindigkeit', 'number', purchaseMinerForm.mining_speed_value, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, mining_speed_value: value }), '0.0001'), selectField('Mining-Einheit', purchaseMinerForm.mining_speed_unit, speedUnits, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, mining_speed_unit: value })), inputField('Bonus-Hashrate in %', 'number', purchaseMinerForm.bonus_percent, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, bonus_percent: value }), '0.01'), inputField('Exakter Mietpreis', 'number', purchaseMinerForm.total_cost_amount, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, total_cost_amount: value }), '0.000001'), selectField('Mietwährung', purchaseMinerForm.currency, selectableCurrencies.map((currency) => currency.code), (value) => setPurchaseMinerForm({ ...purchaseMinerForm, currency: value })), inputField('Referenzpreis', 'number', purchaseMinerForm.reference_price_amount, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, reference_price_amount: value }), '0.000001'), selectField('Referenzwährung', purchaseMinerForm.reference_price_currency, [''].concat(selectableCurrencies.map((currency) => currency.code)), (value) => setPurchaseMinerForm({ ...purchaseMinerForm, reference_price_currency: value })), h('label', { className: 'mc-checkbox' }, [ h('input', { type: 'checkbox', checked: !!purchaseMinerForm.auto_renew, onChange: (event) => setPurchaseMinerForm({ ...purchaseMinerForm, auto_renew: event.target.checked }) }), 'Automatische Verlängerung', ]), textareaField('Notiz', purchaseMinerForm.note, (value) => setPurchaseMinerForm({ ...purchaseMinerForm, note: value })), h('div', { className: 'mc-inline-row' }, [ h('button', { type: 'button', className: 'mc-button mc-button--ghost', onClick: () => setPurchaseMinerModalOpen(false) }, 'Abbrechen'), h('button', { type: 'submit', className: 'mc-button mc-button--secondary', disabled: saving }, saving ? 'Speichert …' : 'Miner mieten'), ]), ]), ], () => setPurchaseMinerModalOpen(false)) : null, targetModalOpen ? renderModal('Ziel anlegen', [ h('form', { key: 'form', className: 'mc-form', onSubmit: submitTarget }, [ inputField('Label', 'text', targetForm.label, (value) => setTargetForm({ ...targetForm, label: value })), 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 })), h('label', { className: 'mc-checkbox' }, [ h('input', { type: 'checkbox', checked: !!targetForm.is_active, onChange: (event) => setTargetForm({ ...targetForm, is_active: event.target.checked }) }), 'Aktiv', ]), h('div', { className: 'mc-inline-row' }, [ h('button', { type: 'button', className: 'mc-button mc-button--ghost', onClick: () => setTargetModalOpen(false) }, 'Abbrechen'), h('button', { type: 'submit', className: 'mc-button mc-button--secondary', disabled: saving }, saving ? 'Speichert …' : 'Ziel speichern'), ]), ]), ], () => setTargetModalOpen(false)) : null, ]); } return h('div', { className: 'mc-two-col' }, [ h('div', { className: 'mc-stack' }, [ panel('Initialisierung', 'Prueft den Tabellenstatus und kann das Mining-Checker Schema neu anlegen. Reset loescht bestehende miningcheck_ Tabellen inklusive Daten.', [ h('div', { key: 'status', className: 'mc-form' }, [ displayField('Status', !schemaStatus.loaded ? 'Status unbekannt' : (schemaStatus.all_present ? 'Schema vollstaendig vorhanden' : 'Schema unvollstaendig')), displayField('Vorhandene Tabellen', schemaStatus.loaded ? `${schemaStatus.present_count}/${schemaStatus.required_tables.length}` : 'Status konnte nicht geladen werden'), displayField('Fehlende Tabellen', schemaStatus.loaded ? (schemaStatus.missing_tables.length ? schemaStatus.missing_tables.join(', ') : 'keine') : 'Status konnte nicht geladen werden'), displayField('Ausstehende Upgrades', schemaStatus.loaded ? (schemaStatus.pending_upgrades.length ? schemaStatus.pending_upgrades.join(', ') : 'keine') : 'Status konnte nicht geladen werden'), ]), h('form', { key: 'form', className: 'mc-form', onSubmit: initializeModule }, [ h('label', { className: 'mc-checkbox' }, [ h('input', { key: 'drop-existing', type: 'checkbox', checked: !!initForm.drop_existing, onChange: (event) => setInitForm({ drop_existing: event.target.checked }), }), 'Bestehende Mining-Checker Tabellen inkl. Daten loeschen und neu anlegen', ]), h('button', { type: 'submit', className: initForm.drop_existing ? 'mc-button mc-button--danger' : 'mc-button mc-button--primary', disabled: saving, }, saving ? 'Initialisiert …' : (initForm.drop_existing ? 'Reset + Schema neu anlegen' : 'Schema initialisieren')), ]), h('button', { key: 'upgrade', type: 'button', className: 'mc-button mc-button--ghost', onClick: upgradeDatabaseSchema, disabled: saving, }, saving ? 'Upgradet …' : 'DB auf neueste Version upgraden'), h('button', { key: 'old-data-import', type: 'button', className: 'mc-button mc-button--secondary', onClick: importOldData, disabled: saving, }, saving ? 'Importiert …' : 'Alte Daten importieren'), h('button', { key: 'legacy-fx-migrate', type: 'button', className: 'mc-button mc-button--secondary', onClick: migrateLegacyFxData, disabled: saving, }, saving ? 'Migriert …' : 'Legacy FX zu fx-rates migrieren'), h('div', { key: 'sql-import', className: 'mc-form' }, [ h('label', { className: 'mc-field' }, [ h('span', { className: 'mc-field-label' }, 'SQL-Datei importieren'), h('input', { type: 'file', accept: '.sql,text/sql,application/sql', onChange: (event) => setSqlImportFile(event.target.files && event.target.files[0] ? event.target.files[0] : null), }), ]), h('div', { className: 'mc-text' }, sqlImportFile ? `Ausgewaehlt: ${sqlImportFile.name}` : 'Fuehrt die ausgewaehlte SQL-Datei direkt in der aktuellen Projekt-Datenbank aus. Bestehende Daten werden dabei nicht automatisch geloescht.' ), h('button', { type: 'button', className: 'mc-button mc-button--secondary', onClick: importSqlFile, disabled: saving || !sqlImportFile, }, saving ? 'Importiert …' : 'SQL-Datei einspielen'), ]), ]), panel('Datenbank-Test', 'Prueft, ob das Modul die Projekt-Datenbank erreichen und eine einfache Anfrage ausfuehren kann.', [ dbCheck ? h('div', { key: 'dbcheck-result', className: 'mc-form' }, [ displayField('Status', dbCheck.ok ? 'Verbindung erfolgreich' : 'Verbindung fehlgeschlagen'), displayField('Driver', dbCheck.driver || 'n/a'), displayField('Datenbank', dbCheck.database || 'n/a'), displayField('Tabellenpraefix', dbCheck.table_prefix || 'n/a'), ]) : h('div', { key: 'dbcheck-empty', className: 'mc-empty' }, 'Noch kein Verbindungstest ausgefuehrt.'), h('button', { key: 'dbcheck-button', type: 'button', className: 'mc-button mc-button--ghost', onClick: testDatabaseConnection, disabled: saving, }, saving ? 'Prueft …' : 'DB-Verbindung testen'), ]), ]), h('div', { className: 'mc-stack' }, [ panel('Basis-Settings', 'Baseline bleibt als Referenzwert mit Datum und Uhrzeit bestehen. Angebotsbasis und Zielminer steuern die automatische Miner-Kalkulation auf der Übersicht.', h('form', { className: 'mc-form', onSubmit: submitSettings, }, [ inputField('Baseline Zeitpunkt', 'datetime-local', toDateTimeLocalValue(settingsForm.baseline_measured_at), (value) => setSettingsForm({ ...settingsForm, baseline_measured_at: value })), inputField('Baseline Coins', 'number', settingsForm.baseline_coins_total, (value) => setSettingsForm({ ...settingsForm, baseline_coins_total: value }), '0.000001'), selectField('Standard-FIAT-Währung', settingsForm.report_currency || 'EUR', selectableFiatCurrencies.map((currency) => currency.code), (value) => setSettingsForm({ ...settingsForm, report_currency: value })), selectField('Standard-Krypto-Währung', settingsForm.crypto_currency || 'DOGE', selectableCryptoCurrencies.map((currency) => currency.code), (value) => setSettingsForm({ ...settingsForm, crypto_currency: value })), inputField('Krypto-Basispreis 50 kH/s · 3 Monate', 'number', settingsForm.crypto_base_price_amount, (value) => setSettingsForm({ ...settingsForm, crypto_base_price_amount: value }), '0.000001'), selectField('Basiswaehrung Krypto', settingsForm.crypto_base_price_currency || 'USD', Array.from(new Set(['USD', String(settingsForm.crypto_currency || 'DOGE').toUpperCase()])), (value) => setSettingsForm({ ...settingsForm, crypto_base_price_currency: value || 'USD' })), inputField('Min. Mietlaufzeit in Monaten', 'number', settingsForm.min_offer_runtime_months, (value) => setSettingsForm({ ...settingsForm, min_offer_runtime_months: value }), '1'), inputField('Zielminer Hashrate in kH/s', 'number', settingsForm.target_offer_hashrate_kh, (value) => setSettingsForm({ ...settingsForm, target_offer_hashrate_kh: value }), '1'), inputField('Zielminer Laufzeit in Monaten', 'number', settingsForm.target_offer_runtime_months, (value) => setSettingsForm({ ...settingsForm, target_offer_runtime_months: value }), '1'), h('button', { type: 'submit', className: 'mc-button mc-button--primary', disabled: saving, }, saving ? 'Speichert …' : 'Settings speichern'), ])), panel('Modulrechte', 'Steuert, wer den Mining-Checker auf der Startseite sieht und direkt aufrufen darf.', h('form', { className: 'mc-form', onSubmit: submitModuleAuth, }, [ h('label', { className: 'mc-checkbox' }, [ h('input', { key: 'required', type: 'checkbox', checked: !!moduleAuthForm.required, onChange: (event) => setModuleAuthForm({ ...moduleAuthForm, required: event.target.checked }), }), 'Login fuer dieses Modul erforderlich', ]), inputField('Erlaubte Benutzer / Subs', 'text', moduleAuthForm.users, (value) => setModuleAuthForm({ ...moduleAuthForm, users: value })), inputField('Erlaubte Gruppen', 'text', moduleAuthForm.groups, (value) => setModuleAuthForm({ ...moduleAuthForm, groups: value })), h('div', { className: 'mc-text' }, 'Mehrere Werte mit Komma trennen. Benutzerfeld akzeptiert Keycloak-Sub, Benutzername oder E-Mail. Leer bedeutet: jeder eingeloggte Benutzer darf das Modul nutzen.'), h('button', { type: 'submit', className: 'mc-button mc-button--primary', disabled: saving, }, saving ? 'Speichert …' : 'Modulrechte speichern'), ])), ]), ]); } function renderWalletModals() { if (activeTab !== 'wallet') { return null; } return [ payoutModalOpen ? renderModal('Teiluebertragung ins NC Wallet', [ h('form', { key: 'form', className: 'mc-form', onSubmit: submitPayout }, [ h('div', { key: 'hint', className: 'mc-text' }, 'Hier kannst du einen Teilbetrag vom aktuellen Miner-Bestand in das NC Wallet uebertragen.'), 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, [currentMiningCurrency].concat(selectableCurrencies.map((currency) => currency.code).filter((code) => code !== currentMiningCurrency)), (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 …' : 'Transfer speichern'), ]), ]), ], () => setPayoutModalOpen(false)) : null, walletWithdrawalModalOpen ? renderModal('Aus NC Wallet auszahlen', [ h('form', { key: 'form', className: 'mc-form', onSubmit: submitWalletWithdrawal }, [ inputField('Auszahlungszeitpunkt', 'datetime-local', walletWithdrawalForm.withdrawal_at, (value) => setWalletWithdrawalForm({ ...walletWithdrawalForm, withdrawal_at: value })), inputField('Coins', 'number', walletWithdrawalForm.coins_amount, (value) => setWalletWithdrawalForm({ ...walletWithdrawalForm, coins_amount: value }), '0.000001'), selectField('Waehrung', walletWithdrawalForm.withdrawal_currency, [currentWalletPrimaryCurrency].concat(selectableCurrencies.map((currency) => currency.code).filter((code) => code !== currentWalletPrimaryCurrency)), (value) => setWalletWithdrawalForm({ ...walletWithdrawalForm, withdrawal_currency: value })), textareaField('Notiz', walletWithdrawalForm.note, (value) => setWalletWithdrawalForm({ ...walletWithdrawalForm, note: value })), h('div', { className: 'mc-inline-row' }, [ h('button', { type: 'button', className: 'mc-button mc-button--ghost', onClick: () => setWalletWithdrawalModalOpen(false) }, 'Abbrechen'), h('button', { type: 'submit', className: 'mc-button mc-button--secondary', disabled: saving }, saving ? 'Speichert …' : 'Wallet-Auszahlung speichern'), ]), ]), ], () => setWalletWithdrawalModalOpen(false)) : null, ]; } function panel(title, subtitle, content) { return h('section', { className: 'mc-panel' }, [ h(SectionTitle, { key: 'title', title, subtitle }), h('div', { key: 'body', className: 'mc-panel-body' }, content), ]); } function fieldWrapper(label, child) { return h('label', { className: 'mc-field' }, [ h('span', { key: 'label', className: 'mc-field-label' }, label), child, ]); } function inputField(label, type, value, onChange, step) { return fieldWrapper(label, h('input', { className: 'mc-input', type, step: step || undefined, value: value, onChange: (event) => onChange(event.target.value), })); } function selectField(label, value, options, onChange) { return fieldWrapper(label, h('select', { className: 'mc-select', value, onChange: (event) => onChange(event.target.value), }, options.map((option) => { const normalized = option && typeof option === 'object' ? option : { value: option, label: option || 'alle' }; return h('option', { key: normalized.value || 'empty', value: normalized.value, }, normalized.label || 'alle'); }))); } function textareaField(label, value, onChange) { return fieldWrapper(label, h('textarea', { className: 'mc-textarea', value, onChange: (event) => onChange(event.target.value), })); } function fileField(label, onChange) { return fieldWrapper(label, h('input', { className: 'mc-file', type: 'file', accept: 'image/png,image/jpeg,image/webp', onChange: (event) => onChange(event.target.files && event.target.files[0] ? event.target.files[0] : null), })); } function displayField(label, value) { return h('div', { className: 'mc-display-field' }, [ h('div', { key: 'label', className: 'mc-field-label' }, label), h('div', { key: 'value', className: 'mc-text' }, value || 'n/a'), ]); } function renderModal(title, content, onClose) { return h('div', { className: 'mc-modal-backdrop', onClick: onClose, }, [ h('div', { key: 'modal', className: 'mc-modal', onClick: (event) => event.stopPropagation(), }, [ h('div', { key: 'head', className: 'mc-section-head' }, [ h('div', { key: 'title-wrap' }, [ h('h3', { key: 'title' }, title), ]), h('button', { key: 'close', type: 'button', className: 'mc-button mc-button--ghost', onClick: onClose, }, 'Schliessen'), ]), h('div', { key: 'body', className: 'mc-panel-body' }, content), ]), ]); } function formatSpeed(value, unit, label) { if (value === null || value === undefined || value === '' || !unit) { return ''; } return `${label ? label + ' ' : ''}${fmtNumber(value, 4)} ${unit}`; } function formatHashrateWithBonus(speedValue, speedUnit, bonusValue, bonusUnit) { const parts = [ formatSpeed(speedValue, speedUnit, 'Basis'), Number(bonusValue) > 0 ? formatSpeed(bonusValue, bonusUnit, 'Bonus') : '', ].filter(Boolean); return parts.length ? parts.join(' · ') : 'n/a'; } function formatAdaptiveSpeed(valueMh) { const numericValue = Number(valueMh); if (!Number.isFinite(numericValue)) { return 'n/a'; } if (numericValue > 0 && numericValue < 1) { return `${fmtNumber(numericValue * 1000, 2)} kH/s`; } return `${fmtNumber(numericValue, 4)} MH/s`; } } ReactDOM.createRoot(root).render(h(App)); }; const standaloneRoot = document.getElementById('mining-checker-app'); if (standaloneRoot) { window.initMiningCheckerApp(standaloneRoot); } })();