From 002d450debaf74b5c2534dfee61f8e7320af806c Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Mon, 4 May 2026 22:22:55 +0200 Subject: [PATCH] yxcyc --- modules/mining-checker/assets/js/app.js | 33 ++++++++++- modules/mining-checker/src/Api/Router.php | 14 +++++ .../mining-checker/src/Domain/OcrService.php | 58 +++++++++++++++---- .../src/Infrastructure/MiningRepository.php | 26 +++++++++ 4 files changed, 120 insertions(+), 11 deletions(-) diff --git a/modules/mining-checker/assets/js/app.js b/modules/mining-checker/assets/js/app.js index 7165af1..e71d284 100644 --- a/modules/mining-checker/assets/js/app.js +++ b/modules/mining-checker/assets/js/app.js @@ -1173,6 +1173,31 @@ } } + 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', + }); + setMessage('Messpunkt geloescht.'); + await loadBootstrap(projectKey); + } catch (err) { + setError(err.message); + } finally { + setSaving(false); + } + } + async function submitMeasurementImport(event) { event.preventDefault(); setSaving(true); @@ -2272,7 +2297,7 @@ panel('Messhistorie', 'Die letzten 10 Uploads inkl. Performance-Werten und OCR-Metadaten.', h('div', { className: 'mc-table-shell' }, [ h('table', { key: 'table', className: 'mc-table' }, [ h('thead', { key: 'thead' }, h('tr', null, [ - 'Zeit', 'Coins', 'Kurs', 'Quelle', 'DOGE/Tag', 'Trend', 'Notiz' + 'Zeit', 'Coins', 'Kurs', 'Quelle', 'DOGE/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 }, [ @@ -2283,6 +2308,12 @@ h('td', { key: 'rate' }, fmtNumber(row.doge_per_day_interval, 4)), 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')), ])) ), ]), diff --git a/modules/mining-checker/src/Api/Router.php b/modules/mining-checker/src/Api/Router.php index be71016..ff0d2a7 100644 --- a/modules/mining-checker/src/Api/Router.php +++ b/modules/mining-checker/src/Api/Router.php @@ -181,6 +181,11 @@ final class Router Http::json(['data' => $this->createMeasurement($projectKey, Http::input())], 201); } + if (preg_match('~^measurements/(\d+)$~', $resource, $matches) && $method === 'DELETE') { + $this->deleteMeasurement($projectKey, (int) $matches[1]); + Http::json(['data' => ['deleted' => true]]); + } + if ($resource === 'measurements-import' && $method === 'POST') { $this->repository()->ensureProject($projectKey); Http::json(['data' => $this->importMeasurements($projectKey, Http::input())], 201); @@ -1454,6 +1459,15 @@ final class Router ]; } + private function deleteMeasurement(string $projectKey, int $measurementId): void + { + if ($measurementId <= 0) { + throw new ApiException('measurement_id ist ungueltig.', 422); + } + + $this->repository()->deleteMeasurement($projectKey, $measurementId); + } + private function syncCurrencyCatalogForMeasurement(array $payload): void { $priceCurrency = strtoupper(trim((string) ($payload['price_currency'] ?? ''))); diff --git a/modules/mining-checker/src/Domain/OcrService.php b/modules/mining-checker/src/Domain/OcrService.php index 456177d..a70e3bd 100644 --- a/modules/mining-checker/src/Domain/OcrService.php +++ b/modules/mining-checker/src/Domain/OcrService.php @@ -299,6 +299,10 @@ final class OcrService $price = null; $currency = null; $normalizedText = preg_replace('/[[:space:]]+/u', ' ', trim($rawText)) ?: ''; + $lines = array_values(array_filter(array_map( + static fn (string $line): string => trim($line), + preg_split('/\R/u', $rawText) ?: [] + ), static fn (string $line): bool => $line !== '')); if ($normalizedText === '') { $flags[] = 'ocr_raw_text_empty'; @@ -338,17 +342,51 @@ final class OcrService $price = round((float) str_replace(',', '.', $priceMatch[1]), 8); } - $coinsCandidates = array_values(array_filter($decimalCandidates, static fn (array $item): bool => $item['value'] > 10 && $item['precision'] >= 4)); - if ($coinsCandidates !== []) { - usort($coinsCandidates, static function (array $a, array $b): int { - return [$b['precision'], $b['value']] <=> [$a['precision'], $a['value']]; - }); - $coinsTotal = round((float) $coinsCandidates[0]['value'], 6); - if (count($coinsCandidates) > 1) { - $flags[] = 'coins_ambiguous'; + if ($coinsTotal === null) { + foreach ($lines as $line) { + if (!preg_match('/MINING[- ]?GUTHABEN|MINING[- ]?BALANCE|GUTHABEN|BALANCE/i', $line)) { + continue; + } + + if (preg_match('/(\d+[.,]\d{4,8})/', $line, $lineCoinsMatch)) { + $coinsTotal = round((float) str_replace(',', '.', $lineCoinsMatch[1]), 6); + $flags[] = 'coins_from_balance_line'; + break; + } + } + } + + if ($coinsTotal === null && preg_match('/(\d+[.,]\d{4,8})\s*(?:DOGE)?\s*(?:MINING[- ]?GUTHABEN|MINING[- ]?BALANCE|GUTHABEN|BALANCE)/i', $normalizedText, $coinsMatch)) { + $coinsTotal = round((float) str_replace(',', '.', $coinsMatch[1]), 6); + $flags[] = 'coins_from_balance_context'; + } + + if ($coinsTotal === null) { + $coinsCandidates = array_values(array_filter($decimalCandidates, static function (array $item) use ($price): bool { + if ($item['precision'] < 4) { + return false; + } + if ($item['value'] <= 0 || $item['value'] >= 1000000) { + return false; + } + if ($price !== null && abs($item['value'] - $price) < 0.0000005) { + return false; + } + + return true; + })); + + if ($coinsCandidates !== []) { + usort($coinsCandidates, static function (array $a, array $b): int { + return [$b['precision'], $b['value']] <=> [$a['precision'], $a['value']]; + }); + $coinsTotal = round((float) $coinsCandidates[0]['value'], 6); + if (count($coinsCandidates) > 1) { + $flags[] = 'coins_ambiguous'; + } + } else { + $flags[] = 'coins_missing'; } - } else { - $flags[] = 'coins_missing'; } $priceCandidates = array_values(array_filter( diff --git a/modules/mining-checker/src/Infrastructure/MiningRepository.php b/modules/mining-checker/src/Infrastructure/MiningRepository.php index 7dfb6fb..d93b23c 100644 --- a/modules/mining-checker/src/Infrastructure/MiningRepository.php +++ b/modules/mining-checker/src/Infrastructure/MiningRepository.php @@ -548,6 +548,32 @@ final class MiningRepository return is_array($row) ? $this->normalizeRow($row) : null; } + public function deleteMeasurement(string $projectKey, int $measurementId): void + { + if ($measurementId <= 0) { + return; + } + + $deleteRates = $this->pdo->prepare( + 'DELETE FROM ' . $this->table('measurement_rates') . ' + WHERE measurement_id = :measurement_id AND owner_sub = :owner_sub' + ); + $deleteRates->execute([ + 'measurement_id' => $measurementId, + 'owner_sub' => $this->ownerSub, + ]); + + $deleteMeasurement = $this->pdo->prepare( + 'DELETE FROM ' . $this->table('measurements') . ' + WHERE id = :id AND project_key = :project_key AND owner_sub = :owner_sub' + ); + $deleteMeasurement->execute([ + 'id' => $measurementId, + 'project_key' => $projectKey, + 'owner_sub' => $this->ownerSub, + ]); + } + public function replaceMeasurementRates(int $measurementId, string $projectKey, array $rates): void { $delete = $this->pdo->prepare('DELETE FROM ' . $this->table('measurement_rates') . ' WHERE measurement_id = :measurement_id AND owner_sub = :owner_sub');