From 8ee9fb84ba4a33de44fdacb64d9c3c5d5859388b Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 10 Apr 2026 11:25:38 +0200 Subject: [PATCH] fix(metadata): Update extraction logic and enhance circumference detection - Adjusted the extract_value_raw function to return failure for unavailable values in strict mode. - Expanded the circumference detection logic in infer_unit_strict to include additional terms for better accuracy in unit inference. --- backend/placeholder_metadata_enhanced.py | 6 +- .../src/pages/AdminCsvTemplateEditorPage.jsx | 134 ++++++++++++++---- 2 files changed, 114 insertions(+), 26 deletions(-) diff --git a/backend/placeholder_metadata_enhanced.py b/backend/placeholder_metadata_enhanced.py index 400b535..4837f97 100644 --- a/backend/placeholder_metadata_enhanced.py +++ b/backend/placeholder_metadata_enhanced.py @@ -30,7 +30,8 @@ def extract_value_raw(value_display: str, output_type: OutputType, placeholder_t Returns: (raw_value, success) """ if not value_display or value_display in ['nicht verfügbar', 'nicht genug Daten']: - return None, True + # V2 strict mode: missing/unavailable value is not a successful extraction + return None, False # JSON output type if output_type == OutputType.JSON: @@ -111,7 +112,8 @@ def infer_unit_strict(key: str, description: str, output_type: OutputType, place return 'kg' # Circumferences/lengths - if any(x in key_lower for x in ['umfang', 'waist', 'hip', 'chest', 'arm', 'leg', 'delta']) and 'circumference' in desc_lower: + circumference_terms = ['umfang', 'waist', 'hip', 'chest', 'arm', 'leg', 'taill', 'hueft', 'brust', 'oberarm', 'oberschenkel'] + if any(x in key_lower for x in circumference_terms) or any(x in desc_lower for x in ['circumference', 'umfang', 'taill', 'hüft', 'hueft', 'brust', 'oberarm', 'oberschenkel']): return 'cm' # Time durations diff --git a/frontend/src/pages/AdminCsvTemplateEditorPage.jsx b/frontend/src/pages/AdminCsvTemplateEditorPage.jsx index b3ee163..2f40a14 100644 --- a/frontend/src/pages/AdminCsvTemplateEditorPage.jsx +++ b/frontend/src/pages/AdminCsvTemplateEditorPage.jsx @@ -13,6 +13,45 @@ const MODULE_LABEL = { vitals_baseline: 'Vitalwerte (Baseline)', } +/** Erlaubt Eingaben wie 1,03 oder 1.03 während des Tippens; Finale normalisiert bei Blur/Speichern. */ +function normalizeDecimalInputString(raw) { + let s = String(raw).trim().replace(/\s/g, '') + if (s === '') return '' + const lastComma = s.lastIndexOf(',') + const lastDot = s.lastIndexOf('.') + if (lastComma >= 0 && lastDot >= 0) { + if (lastComma > lastDot) { + s = s.replace(/\./g, '').replace(',', '.') + } else { + s = s.replace(/,/g, '') + } + } else if (lastComma >= 0) { + s = s.replace(',', '.') + } + return s +} + +function applyCustomFactorToTcObject(tc, fieldKey, raw) { + const base = + tc[fieldKey] && typeof tc[fieldKey] === 'object' + ? { ...tc[fieldKey] } + : { type: 'float', decimal_separator: 'auto', flexible: true } + const normalized = normalizeDecimalInputString(raw) + if (normalized === '') { + delete base.conversion_factor + } else { + const num = Number(normalized) + if (Number.isNaN(num)) { + return { ok: false, message: `Konvertierungsfaktor (${fieldKey}): keine gültige Zahl.` } + } + base.conversion_factor = num + base.source_unit = 'custom' + } + delete base.target_unit + tc[fieldKey] = base + return { ok: true } +} + function SampleTable({ sampleRows, columns }) { if (!sampleRows?.length || !columns?.length) return null const showCols = columns.slice(0, 8) @@ -82,6 +121,8 @@ export default function AdminCsvTemplateEditorPage() { const [analyzing, setAnalyzing] = useState(false) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) + /** Lokaler Text für Konvertierungsfaktor (nur bei source_unit custom), Commit bei Blur/Speichern — verhindert 1. → 1 beim Tippen. */ + const [customFactorDraftByField, setCustomFactorDraftByField] = useState({}) const modMeta = useMemo(() => modules.find((m) => m.id === module), [modules, module]) const aggregateSleepImport = modMeta?.import_mode === 'apple_sleep_aggregate' @@ -135,6 +176,7 @@ export default function AdminCsvTemplateEditorPage() { setFieldMappings(fm) setColumns(Object.keys(fm)) setTypeConversionsText(JSON.stringify(t.type_conversions || {}, null, 2)) + setCustomFactorDraftByField({}) setSampleRows([]) setSeedHint(null) }) @@ -221,6 +263,11 @@ export default function AdminCsvTemplateEditorPage() { } tc[fieldKey] = base setTypeConversionsText(JSON.stringify(tc, null, 2)) + setCustomFactorDraftByField((prev) => { + const next = { ...prev } + delete next[fieldKey] + return next + }) setError(null) } @@ -236,7 +283,22 @@ export default function AdminCsvTemplateEditorPage() { return String(v) } - const updateCustomConversionFactor = (fieldKey, raw) => { + const getCustomFactorFieldDisplay = (fieldKey) => { + if (Object.prototype.hasOwnProperty.call(customFactorDraftByField, fieldKey)) { + return customFactorDraftByField[fieldKey] + } + return getCustomConversionFactorInputValue(fieldKey) + } + + const commitCustomFactorOnBlur = (fieldKey, raw) => { + if (getSourceUnitSelectValue(fieldKey) !== 'custom') { + setCustomFactorDraftByField((prev) => { + const next = { ...prev } + delete next[fieldKey] + return next + }) + return + } let tc try { tc = JSON.parse(typeConversionsText || '{}') @@ -244,25 +306,17 @@ export default function AdminCsvTemplateEditorPage() { setError('type_conversions: ungültiges JSON (Faktor kann nicht gesetzt werden).') return } - const base = - tc[fieldKey] && typeof tc[fieldKey] === 'object' - ? { ...tc[fieldKey] } - : { type: 'float', decimal_separator: 'auto', flexible: true } - const t = String(raw).trim().replace(',', '.') - if (t === '') { - delete base.conversion_factor - } else { - const n = Number(t) - if (Number.isNaN(n)) { - setError('Konvertierungsfaktor: bitte eine gültige Zahl eingeben.') - return - } - base.conversion_factor = n - base.source_unit = 'custom' + const result = applyCustomFactorToTcObject(tc, fieldKey, raw) + if (!result.ok) { + setError(result.message || 'Konvertierungsfaktor ungültig.') + return } - delete base.target_unit - tc[fieldKey] = base setTypeConversionsText(JSON.stringify(tc, null, 2)) + setCustomFactorDraftByField((prev) => { + const next = { ...prev } + delete next[fieldKey] + return next + }) setError(null) } @@ -285,6 +339,7 @@ export default function AdminCsvTemplateEditorPage() { setDelimiter(res.delimiter || ';') setEncoding(res.encoding || 'utf-8') setSeedHint(res.seed_template || null) + setCustomFactorDraftByField({}) } catch (e) { setError(e.message || 'Analyse fehlgeschlagen') } finally { @@ -298,9 +353,31 @@ export default function AdminCsvTemplateEditorPage() { const handleSave = async () => { setError(null) + let textForTc = typeConversionsText + const pendingFactorDrafts = { ...customFactorDraftByField } + if (Object.keys(pendingFactorDrafts).length > 0) { + try { + const tco = JSON.parse(textForTc || '{}') + for (const [fk, raw] of Object.entries(pendingFactorDrafts)) { + const su = String(tco[fk]?.source_unit || '').toLowerCase() + if (su !== 'custom') continue + const result = applyCustomFactorToTcObject(tco, fk, raw) + if (!result.ok) { + setError(result.message || 'Konvertierungsfaktor ungültig.') + return + } + } + textForTc = JSON.stringify(tco, null, 2) + setTypeConversionsText(textForTc) + setCustomFactorDraftByField({}) + } catch { + setError('type_conversions: ungültiges JSON.') + return + } + } let tc = null try { - tc = JSON.parse(typeConversionsText || '{}') + tc = JSON.parse(textForTc || '{}') if (tc !== null && typeof tc !== 'object') throw new Error() } catch { setError('type_conversions: ungültiges JSON.') @@ -682,12 +759,18 @@ export default function AdminCsvTemplateEditorPage() { type="text" inputMode="decimal" className="form-input" - placeholder="z. B. 1.03 bei ml→g (Dichte ≈ 1,03 g/ml)" - value={getCustomConversionFactorInputValue(fkey)} - onChange={(e) => updateCustomConversionFactor(fkey, e.target.value)} + placeholder="z. B. 1,03 oder 1.03 (ml→g nach Dichte)" + value={getCustomFactorFieldDisplay(fkey)} + onChange={(e) => + setCustomFactorDraftByField((prev) => ({ ...prev, [fkey]: e.target.value })) + } + onBlur={(e) => commitCustomFactorOnBlur(fkey, e.target.value)} style={{ width: '100%', textAlign: 'left' }} /> - {getCustomConversionFactorInputValue(fkey) === '' && ( +

+ Dezimalkomma oder -punkt; mit Tab oder Klick außerhalb übernehmen (oder direkt Speichern). +

+ {getCustomFactorFieldDisplay(fkey) === '' && (

Ohne Faktor gilt keine zusätzliche Skalierung (wie 1:1 nach dem Parsen).

@@ -720,7 +803,10 @@ export default function AdminCsvTemplateEditorPage() { textAlign: 'left', }} value={typeConversionsText} - onChange={(e) => setTypeConversionsText(e.target.value)} + onChange={(e) => { + setTypeConversionsText(e.target.value) + setCustomFactorDraftByField({}) + }} />