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

This commit is contained in:
2026-05-04 22:22:55 +02:00
parent a9fefa7779
commit 002d450deb
4 changed files with 120 additions and 11 deletions

View File

@@ -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) { async function submitMeasurementImport(event) {
event.preventDefault(); event.preventDefault();
setSaving(true); setSaving(true);
@@ -2272,7 +2297,7 @@
panel('Messhistorie', 'Die letzten 10 Uploads inkl. Performance-Werten und OCR-Metadaten.', h('div', { className: 'mc-table-shell' }, [ 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('table', { key: 'table', className: 'mc-table' }, [
h('thead', { key: 'thead' }, h('tr', null, [ 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)))), ].map((label) => h('th', { key: label }, label)))),
h('tbody', { key: 'tbody' }, h('tbody', { key: 'tbody' },
measurements.slice(-10).reverse().map((row) => h('tr', { key: row.id }, [ 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: 'rate' }, fmtNumber(row.doge_per_day_interval, 4)),
h('td', { key: 'trend' }, row.trend_label), h('td', { key: 'trend' }, row.trend_label),
h('td', { key: 'note' }, row.note || row.ocr_flags.join(', ') || '—'), 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')),
])) ]))
), ),
]), ]),

View File

@@ -181,6 +181,11 @@ final class Router
Http::json(['data' => $this->createMeasurement($projectKey, Http::input())], 201); 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') { if ($resource === 'measurements-import' && $method === 'POST') {
$this->repository()->ensureProject($projectKey); $this->repository()->ensureProject($projectKey);
Http::json(['data' => $this->importMeasurements($projectKey, Http::input())], 201); 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 private function syncCurrencyCatalogForMeasurement(array $payload): void
{ {
$priceCurrency = strtoupper(trim((string) ($payload['price_currency'] ?? ''))); $priceCurrency = strtoupper(trim((string) ($payload['price_currency'] ?? '')));

View File

@@ -299,6 +299,10 @@ final class OcrService
$price = null; $price = null;
$currency = null; $currency = null;
$normalizedText = preg_replace('/[[:space:]]+/u', ' ', trim($rawText)) ?: ''; $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 === '') { if ($normalizedText === '') {
$flags[] = 'ocr_raw_text_empty'; $flags[] = 'ocr_raw_text_empty';
@@ -338,17 +342,51 @@ final class OcrService
$price = round((float) str_replace(',', '.', $priceMatch[1]), 8); $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 ($coinsTotal === null) {
if ($coinsCandidates !== []) { foreach ($lines as $line) {
usort($coinsCandidates, static function (array $a, array $b): int { if (!preg_match('/MINING[- ]?GUTHABEN|MINING[- ]?BALANCE|GUTHABEN|BALANCE/i', $line)) {
return [$b['precision'], $b['value']] <=> [$a['precision'], $a['value']]; continue;
}); }
$coinsTotal = round((float) $coinsCandidates[0]['value'], 6);
if (count($coinsCandidates) > 1) { if (preg_match('/(\d+[.,]\d{4,8})/', $line, $lineCoinsMatch)) {
$flags[] = 'coins_ambiguous'; $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( $priceCandidates = array_values(array_filter(

View File

@@ -548,6 +548,32 @@ final class MiningRepository
return is_array($row) ? $this->normalizeRow($row) : null; 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 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'); $delete = $this->pdo->prepare('DELETE FROM ' . $this->table('measurement_rates') . ' WHERE measurement_id = :measurement_id AND owner_sub = :owner_sub');