Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions js/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,14 +341,14 @@ export function buildReportHTML(profileName, sexLabel, data, flags, notes, supps
.optimal { color: #059669; font-size: 10px; }
.note-item { padding: 6px 0; font-size: 13px; border-bottom: 1px solid #f0f0f0; }
.context-item { padding: 6px 0; font-size: 13px; white-space: pre-line; }
.report-footer { margin-top: 40px; padding-top: 16px; border-top: 1px solid #ddd; font-size: 11px; color: #888; }
.report-footer { margin-top: 40px; padding-top: 16px; border-top: 1px solid #ddd; font-size: 11px; color: #888; break-inside: avoid; page-break-inside: avoid; }
.disclaimer { margin-top: 8px; font-style: italic; }
@media print {
body { padding: 16px; }
h2 { page-break-after: avoid; }
table { page-break-inside: auto; }
tr { page-break-inside: avoid; }
.report-footer { position: fixed; bottom: 0; width: 100%; }
.report-footer { break-inside: avoid; page-break-inside: avoid; }
}
</style></head><body>${body}</body></html>`;
}
Expand Down
91 changes: 80 additions & 11 deletions js/pdf-import-marker-mapping.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,57 @@ function _compactImportLabelVariants(value) {
return [...new Set(variants)];
}

const DIFFERENTIAL_IMPORT_STEMS = new Map([
['neutrofily', 'neutrophils'],
['neutrophils', 'neutrophils'],
['neutrophil', 'neutrophils'],
['lymfocyty', 'lymphocytes'],
['lymphocytes', 'lymphocytes'],
['lymphocyte', 'lymphocytes'],
['monocyty', 'monocytes'],
['monocytes', 'monocytes'],
['monocyte', 'monocytes'],
['eosinofily', 'eosinophils'],
['eosinophils', 'eosinophils'],
['eosinophil', 'eosinophils'],
['basofily', 'basophils'],
['basophils', 'basophils'],
['basophil', 'basophils'],
]);

function _stripDifferentialPercentSuffix(compactBase) {
return String(compactBase || '').replace(/(?:pct|percent|percentage)$/i, '');
}

function _differentialStemFromCompactBase(compactBase) {
return DIFFERENTIAL_IMPORT_STEMS.get(_stripDifferentialPercentSuffix(compactBase)) || null;
}

function _hasImportAbsoluteHint(rawName, unit) {
return /#|\babs\b|absolute/i.test(String(rawName || '')) || String(unit || '').includes('10^9');
}

function _hasImportPercentHint(rawName, unit, compactBase) {
const unitNorm = String(unit || '');
return /%|\bpct\b|percent|percentage/i.test(String(rawName || '')) ||
unitNorm === '%' ||
unitNorm === 'pct' ||
unitNorm === 'percent' ||
unitNorm === 'percentage' ||
/(?:pct|percent|percentage)$/i.test(String(compactBase || ''));
}

function _suggestDifferentialPercentImportKey(marker) {
const rawName = marker?.rawName || marker?.suggestedName || '';
const unit = normalizeUnitStr(marker?.unit || '');
const compactBase = _compactImportLabel(rawName).replace(/#/g, '');
const stem = _differentialStemFromCompactBase(compactBase);
if (!stem) return null;
if (_hasImportAbsoluteHint(rawName, unit)) return null;
if (!_hasImportPercentHint(rawName, unit, compactBase)) return null;
return `differential.${stem}Pct`;
}

export function _cleanImportedMarkerDisplayName(value) {
const cleaned = _stripImportLabelUnits(_stripImportSpecimenPrefix(value))
.trim()
Expand Down Expand Up @@ -274,7 +325,7 @@ function _buildStandardBloodNameLookup() {
return lookup;
}

function _resolveStandardBloodImportKey(marker, refLookup) {
function _resolveStandardBloodImportKey(marker, refLookup, differentialPercentSuggestedKey = undefined) {
const rawName = marker.rawName || marker.suggestedName || '';
const specimen = _getImportSpecimen(rawName);
const unit = normalizeUnitStr(marker.unit || '');
Expand All @@ -289,12 +340,23 @@ function _resolveStandardBloodImportKey(marker, refLookup) {

if (unit === 'arb.j.' || unit.includes('/ul')) return null;

const hasAbsoluteHint = /#|\babs\b|absolute/i.test(String(rawName)) || unit.includes('10^9');
if (compactBase === 'neutrofily') return hasAbsoluteHint ? 'differential.neutrophils' : 'differential.neutrophilsPct';
if (compactBase === 'lymfocyty') return hasAbsoluteHint ? 'differential.lymphocytes' : 'differential.lymphocytesPct';
if (compactBase === 'monocyty') return hasAbsoluteHint ? 'differential.monocytes' : 'differential.monocytesPct';
if (compactBase === 'eosinofily') return hasAbsoluteHint ? 'differential.eosinophils' : null;
if (compactBase === 'basofily') return hasAbsoluteHint ? 'differential.basophils' : null;
const hasAbsoluteHint = _hasImportAbsoluteHint(rawName, unit);
const pctSuggestedKey = differentialPercentSuggestedKey === undefined
? _suggestDifferentialPercentImportKey(marker)
: differentialPercentSuggestedKey;
if (pctSuggestedKey) {
return refLookup[pctSuggestedKey] ? pctSuggestedKey : null;
}
const differentialStem = _differentialStemFromCompactBase(compactBase);
if (differentialStem && hasAbsoluteHint) {
const absoluteKey = `differential.${differentialStem}`;
return refLookup[absoluteKey] ? absoluteKey : null;
}
if (compactBase === 'neutrofily' || compactBase === 'lymfocyty' || compactBase === 'monocyty') {
const pctKey = `differential.${differentialStem}Pct`;
return refLookup[pctKey] ? pctKey : null;
}
if (compactBase === 'eosinofily' || compactBase === 'basofily') return null;

const lookup = _buildStandardBloodNameLookup();
const labels = [marker.rawName, marker.suggestedName];
Expand Down Expand Up @@ -322,18 +384,25 @@ export function reconcileImportMarkerMappings(markers, options = {}) {
const existingNameLookup = options.existingNameLookup || _buildExistingCustomMarkerNameLookup(existingKeys);
for (const marker of markers) {
if (!marker) continue;
const differentialPercentSuggestedKey = testType === 'blood' ? _suggestDifferentialPercentImportKey(marker) : null;
const mappedSpecimenBad = _isSpecimenIncompatibleImportKey(marker, marker.mappedKey, standardCats);
const suggestedSpecimenBad = _isSpecimenIncompatibleImportKey(marker, marker.suggestedKey, standardCats);
const exactMappedKey = mappedSpecimenBad ? null : _knownImportKey(marker.mappedKey, testType, refLookup, existingKeys, standardCats);
const exactSuggestedKey = suggestedSpecimenBad ? null : _knownImportKey(marker.suggestedKey, testType, refLookup, existingKeys, standardCats);
const exactMappedKey = mappedSpecimenBad || differentialPercentSuggestedKey ? null : _knownImportKey(marker.mappedKey, testType, refLookup, existingKeys, standardCats);
const exactSuggestedKey = suggestedSpecimenBad || differentialPercentSuggestedKey ? null : _knownImportKey(marker.suggestedKey, testType, refLookup, existingKeys, standardCats);
const exactKey = exactMappedKey || exactSuggestedKey;
const existingCustomKey = exactKey || _resolveExistingCustomImportKey(marker, existingNameLookup, testType, refLookup, existingKeys, standardCats);
const aliasKey = testType === 'blood' ? _resolveStandardBloodImportKey(marker, refLookup) : null;
const existingCustomKey = exactKey || (differentialPercentSuggestedKey ? null : _resolveExistingCustomImportKey(marker, existingNameLookup, testType, refLookup, existingKeys, standardCats));
const aliasKey = testType === 'blood' ? _resolveStandardBloodImportKey(marker, refLookup, differentialPercentSuggestedKey) : null;
const resolvedKey = aliasKey || existingCustomKey;
if (resolvedKey) {
marker.mappedKey = resolvedKey;
marker.matched = true;
marker.suggestedKey = null;
} else if (differentialPercentSuggestedKey) {
marker.mappedKey = null;
marker.matched = false;
marker.suggestedKey = differentialPercentSuggestedKey;
marker.suggestedName = marker.suggestedName || _cleanImportedMarkerDisplayName(marker.rawName);
marker.suggestedCategoryLabel = marker.suggestedCategoryLabel || 'WBC Differential';
} else if (mappedSpecimenBad || suggestedSpecimenBad) {
_demoteSpecimenIncompatibleImportKey(marker, marker.mappedKey || marker.suggestedKey, standardCats);
} else if (marker.mappedKey && !_knownImportKey(marker.mappedKey, testType, refLookup, existingKeys, standardCats)) {
Expand Down
1 change: 1 addition & 0 deletions js/wearable-adapters.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ export const ADAPTERS = [
// character-for-character.
clientId: '23VBN8',
redirectUris: [
'https://app.getbased.health/app',
'https://app.getbased.health/',
'https://getbased.health/app',
'https://beta.getbased.health/',
Expand Down
5 changes: 5 additions & 0 deletions tests/test-export-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ return (async function() {
assert('Client export has profile dob', exportSrc.includes('dob: p.dob'));
assert('Client export has profile tags', exportSrc.includes('tags: p.tags'));
assert('Client export has profile height', exportSrc.includes('height: p.height'));
assert('PDF report print footer stays in document flow',
!/\.report-footer\s*\{[^}]*position:\s*fixed/i.test(exportSrc),
'fixed print footer overlaps report content in generated PDFs');
assert('PDF report footer avoids splitting across pages',
exportSrc.includes('break-inside: avoid; page-break-inside: avoid;'));

// ═══════════════════════════════════════
// 3. buildAllDataBundle — live call
Expand Down
12 changes: 11 additions & 1 deletion tests/test-unit-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ const settingsSrc = read('js/settings.js');
}],
customMarkers: {
'custom.activeB12': { name: 'Active B12', unit: 'pmol/l' },
'custom.eosinophilsLegacy': { name: 'Eosinophils %', unit: '%' },
'spadiaFA.epaC20_5': { name: 'EPA C20:5', unit: '%' },
'biochemistry.alpUkatL': { name: 'ALP (ukat/l)', unit: 'µkat/l' }
}
Expand All @@ -320,7 +321,10 @@ const settingsSrc = read('js/settings.js');
{ rawName: 'ALP (ukat/l)', value: 1.2, unit: 'µkat/l', matched: false, mappedKey: null, suggestedKey: 'biochemistry.alpUkatL' },
{ rawName: 'ALT [µkat/l]', value: 0.5, unit: 'µkat/l', matched: true, mappedKey: 'biochemistry.altUkatL', suggestedKey: null },
{ rawName: 'USED Leukocyty', value: 4, unit: '/µl', matched: true, mappedKey: 'hematology.wbc', suggestedKey: null },
{ rawName: 'Unknown Marker', value: 42, unit: 'x', matched: true, mappedKey: 'custom.unknownMarker', suggestedKey: null }
{ rawName: 'Unknown Marker', value: 42, unit: 'x', matched: true, mappedKey: 'custom.unknownMarker', suggestedKey: null },
{ rawName: 'Lymphocytes %', value: 36.8, unit: '%', matched: true, mappedKey: 'differential.lymphocytes', suggestedKey: null },
{ rawName: 'Monocytes_PERCENTAGE', value: 7.4, unit: 'PERCENTAGE', matched: true, mappedKey: 'differential.monocytes', suggestedKey: null },
{ rawName: 'Eosinophils %', value: 4.1, unit: '%', matched: true, mappedKey: 'differential.eosinophils', suggestedKey: null }
];
reconcileImportMarkerMappings(importMarkers, { testType: 'blood' });
assert('Czech glucose reconciles to existing schema marker',
Expand Down Expand Up @@ -353,6 +357,12 @@ const settingsSrc = read('js/settings.js');
&& importMarkers[11].suggestedKey === 'urinalysis.leukocytesQualitative');
assert('Unknown invalid mappedKey is demoted so it becomes a real custom marker',
!importMarkers[12].matched && importMarkers[12].mappedKey === null && importMarkers[12].suggestedKey === 'custom.unknownMarker');
assert('Differential lymphocyte percent maps to percentage marker despite AI absolute key',
importMarkers[13].matched && importMarkers[13].mappedKey === 'differential.lymphocytesPct');
assert('Differential monocyte percentage label maps to percentage marker despite AI absolute key',
importMarkers[14].matched && importMarkers[14].mappedKey === 'differential.monocytesPct');
assert('Unsupported differential percent does not overwrite absolute-count or stale custom marker',
!importMarkers[15].matched && importMarkers[15].mappedKey === null && importMarkers[15].suggestedKey === 'differential.eosinophilsPct');
} finally {
state.importedData = originalImportedData;
}
Expand Down
4 changes: 4 additions & 0 deletions tests/test-wearables.js
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,10 @@ assert('Fitbit scopes include temperature + weight (for skin Δ + scale readings
assert('Fitbit adapter scope list matches DEFAULT_FITBIT_SCOPES (no drift)',
JSON.stringify([...fitbitReg.oauth.scopes].sort()) ===
JSON.stringify([...fitbitAuth.DEFAULT_FITBIT_SCOPES].sort()));
assert('Fitbit hosted /app route is registered before origin fallback',
fitbitReg.oauth.redirectUris.includes('https://app.getbased.health/app'));
assert('Fitbit redirect picker keeps hosted /app instead of falling back to origin root',
fitbitAuth.pickRedirectUri(fitbitReg.oauth.redirectUris, { origin: 'https://app.getbased.health', pathname: '/app' }) === 'https://app.getbased.health/app');

const fbUrl = await fitbitAuth.buildAuthorizeUrl({
clientId: 'fb-test-client', redirectUri: 'http://localhost:8000/app',
Expand Down
2 changes: 1 addition & 1 deletion version.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
// Classic script (not ES module) so it works in both browser and service worker.
// Browser: <script src="version.js"> sets window.APP_VERSION
// Service worker: importScripts('/version.js') sets self.APP_VERSION
self.APP_VERSION = '1.8.339';
self.APP_VERSION = '1.8.343';