From b254d7012df9b409fc1653fafa67fa4dc44eb31b Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 29 Mar 2026 08:43:24 +1300 Subject: [PATCH 01/25] add plan-fixes skill for processing user bug reports into work plans --- .claude/skills/plan-fixes/SKILL.md | 117 +++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 .claude/skills/plan-fixes/SKILL.md diff --git a/.claude/skills/plan-fixes/SKILL.md b/.claude/skills/plan-fixes/SKILL.md new file mode 100644 index 0000000..386a0d4 --- /dev/null +++ b/.claude/skills/plan-fixes/SKILL.md @@ -0,0 +1,117 @@ +--- +name: plan-fixes +description: Reads user bug reports from .claude/issues/, builds a prioritized work plan for generate and evaluate agents +--- + +You are a Planning agent. Your job is to read bug reports from real users, analyze them, and produce a structured work plan that other agents (generate, evaluate) can execute. + +## Input + +`$ARGUMENTS` — paths to issue directories or files to process. If empty, read all directories in `.claude/issues/`. + +## Steps + +### 1. Read all bug reports + +- Read every `issues.md` in the provided directories +- Look at attached files (.squid, .png, etc.) to understand reproduction data +- Note cross-references between reports (e.g., `../RV-march-2026/issues.md#6`) + +### 2. Deduplicate and categorize + +Group issues into: +- **bugs** — something is broken (crashes, wrong calculations, display errors) +- **improvements** — something works but could be better (formatting, precision, UX) +- **features** — something new that doesn't exist yet (multi-window, file merging) + +Merge duplicates (e.g., if two people report the same crash). Reference all original reporters. + +### 3. Prioritize + +Sort bugs by severity: +1. **critical** — crashes, data loss, wrong scientific calculations +2. **major** — significant UX issues, incorrect display of data +3. **minor** — cosmetic, formatting, minor inconveniences +4. **feature** — new functionality requests (lowest priority) + +### 4. Check which bugs are already fixed + +- Read `git log --oneline -30` to see recent fixes +- Read existing evaluation reports in `test-data/` if any +- If a bug appears to be already fixed, mark it as such and note the commit + +### 5. Build the work plan + +For each issue, define: +- **What**: one-line description +- **Why**: who reported it, what impact it has +- **Reproduction**: exact steps, including which attached files to use +- **Suggested fix direction**: which files/components are likely involved (read the codebase to determine this) +- **Verification**: what the evaluate agent should check after the fix + +### 6. Create evaluation-first steps where needed + +Some bugs may need an evaluate session BEFORE fixing — to confirm the bug still exists and establish a baseline. Add a pre-evaluation step when: +- The bug might have been fixed already +- The reproduction steps are unclear +- You need to understand the current behavior before changing it + +### 7. Save the plan + +1. Read version from `package.json` +2. Save to `test-data/v{version}/workplan-{YYYY-MM-DD}.md` + +## Output format + +```markdown +# Work Plan — PMTools v{version} + +Date: {YYYY-MM-DD} +Sources: {list of issue directories processed} + +## Pre-evaluation (run /evaluate with these checks first) + +{List of bugs that need evaluation before fixing, with exact /evaluate prompts} + +## Fixes (run /generate for each) + +### Fix 1: {title} [critical] +- **Reported by**: {name(s)} ({directory reference}) +- **What**: {description} +- **Reproduce**: {exact steps, mention attached files by path} +- **Files to investigate**: {src/path/to/likely/files} +- **Fix direction**: {suggested approach} +- **Verify**: {what /evaluate should check after fix} + +### Fix 2: ... + +## Improvements + +### Improvement 1: {title} [major/minor] +... + +## Feature Requests (backlog) + +### Feature 1: {title} +- **Reported by**: {name(s)} +- **What**: {description} +- **Complexity estimate**: small / medium / large + +## Summary + +| Category | Count | Already fixed | +|----------|-------|---------------| +| Critical bugs | | | +| Major bugs | | | +| Minor bugs | | | +| Improvements | | | +| Features | | | +``` + +## Important + +- Write the plan in English +- Keep reproduction steps specific — the generate agent will follow them literally +- Include file paths for attached .squid/.png files so agents can find them +- If a bug report is in Russian, translate the key details to English in the plan +- Reference original issue files so a human can trace back: `(.claude/issues/RV-march-2026/issues.md#4)` From 625285820b9693b5606ef35ba8fa29a82df73145 Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 29 Mar 2026 08:47:18 +1300 Subject: [PATCH 02/25] =?UTF-8?q?add=20.claude/issues/=20to=20gitignore=20?= =?UTF-8?q?=E2=80=94=20private=20user=20reports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 747f141..4c1b7e7 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ yarn-error.log* .vercel .playwright-mcp/ + +# user bug reports (private) +.claude/issues/ From 06a65839c99668a104d44dd2054fa57a7e632691 Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 29 Mar 2026 09:35:50 +1300 Subject: [PATCH 03/25] fix: read demagType from first non-NRM step instead of steps[0] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NRM step always has undefined demagType, causing the magnetization graph to show "mT" instead of "°C" for thermal demagnetization data. --- src/utils/graphs/formatters/mag/dataToMag.ts | 2 +- .../statistics/formtatters/rawStatisticsPMDToInterpretation.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/graphs/formatters/mag/dataToMag.ts b/src/utils/graphs/formatters/mag/dataToMag.ts index b6554e0..3109116 100644 --- a/src/utils/graphs/formatters/mag/dataToMag.ts +++ b/src/utils/graphs/formatters/mag/dataToMag.ts @@ -51,7 +51,7 @@ const dataToMag = (data: IPmdData, graphSize: number, hiddenStepsIDs: Array step.demagType)?.demagType; return { dotsData, diff --git a/src/utils/statistics/formtatters/rawStatisticsPMDToInterpretation.ts b/src/utils/statistics/formtatters/rawStatisticsPMDToInterpretation.ts index 7f80ce6..4b1ec17 100644 --- a/src/utils/statistics/formtatters/rawStatisticsPMDToInterpretation.ts +++ b/src/utils/statistics/formtatters/rawStatisticsPMDToInterpretation.ts @@ -36,7 +36,7 @@ const rawStatisticsPMDToInterpretation = ( const confidenceRadius = statistics.MAD; const comment = ''; - const demagType = selectedSteps[0].demagType; + const demagType = selectedSteps.find((step) => step.demagType)?.demagType; const interpretation: StatisitcsInterpretationFromPCA = { uuid: uuidv4(), From a11fc37403d9517f36f59f7339c75337ebd9a5e3 Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 29 Mar 2026 09:37:32 +1300 Subject: [PATCH 04/25] fix: use string state in metadata editor to prevent float artifacts and NaN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Display values using toPrecision(12) to eliminate IEEE 754 artifacts (e.g., 38.599999999999994 → 38.6). Keep raw string in state during editing so intermediate values like "." or "," don't produce NaN. Parse to number only on apply, with comma-to-dot replacement for Russian locale support. --- .../MetaDataChange/MetaDataChange.tsx | 59 +++++++++++++------ 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/src/components/Common/DataTable/MetaDataChange/MetaDataChange.tsx b/src/components/Common/DataTable/MetaDataChange/MetaDataChange.tsx index c02f880..53eddef 100644 --- a/src/components/Common/DataTable/MetaDataChange/MetaDataChange.tsx +++ b/src/components/Common/DataTable/MetaDataChange/MetaDataChange.tsx @@ -1,6 +1,6 @@ import React, { FC, useState } from 'react'; import styles from './MetaDataChange.module.scss'; -import { Button, IconButton, TextField } from '@mui/material'; +import { Button, TextField } from '@mui/material'; import DirectionsIcon from '@mui/icons-material/Directions'; import { IPmdData } from '../../../../utils/GlobalTypes'; import { useAppDispatch, useAppSelector } from '../../../../services/store/hooks'; @@ -15,15 +15,41 @@ interface IMetaDataChange { onApply: () => void; } +const formatNumericValue = (value: number): string => { + return parseFloat(value.toPrecision(12)).toString(); +}; + +type MetadataProperty = 'a' | 'b' | 's' | 'd' | 'v'; + const MetaDataChange: FC = ({ oldMetadata, onApply }) => { const dispatch = useAppDispatch(); const { t, i18n } = useTranslation('translation'); const { treatmentData } = useAppSelector((state) => state.parsedDataReducer); - const [newMetadata, setNewMetadata] = useState(oldMetadata); + const [inputValues, setInputValues] = useState>({ + a: formatNumericValue(oldMetadata.a), + b: formatNumericValue(oldMetadata.b), + s: formatNumericValue(oldMetadata.s), + d: formatNumericValue(oldMetadata.d), + v: formatNumericValue(oldMetadata.v), + }); if (!treatmentData) return null; + const parseInputValues = (): IPmdData['metadata'] | null => { + const parsed = { ...oldMetadata }; + for (const key of ['a', 'b', 's', 'd', 'v'] as const) { + const raw = inputValues[key].replace(',', '.'); + const num = Number(raw); + if (raw !== '' && isNaN(num)) return null; + parsed[key] = raw === '' ? 0 : num; + } + return parsed; + }; + const handleApply = () => { + const newMetadata = parseInputValues(); + if (!newMetadata) return; + const newTreatmentData = treatmentData.map((pmdData) => { if (pmdData.metadata.name === oldMetadata.name) { const updatedSteps = pmdData.steps.map((step) => { @@ -67,22 +93,19 @@ const MetaDataChange: FC = ({ oldMetadata, onApply }) => { } }; - const handleMetadataPropertyChange = ( + const handleInputChange = ( event: React.ChangeEvent, - property: 'a' | 'b' | 's' | 'd' | 'v', + property: MetadataProperty, ) => { - setNewMetadata({ - ...newMetadata, - [property]: +event.target.value, - }); + setInputValues((prev) => ({ ...prev, [property]: event.target.value })); }; return (
handleMetadataPropertyChange(event, 'a')} + value={inputValues.a} + onChange={(event) => handleInputChange(event, 'a')} onKeyPress={handleEnterPress} variant="standard" size="small" @@ -90,8 +113,8 @@ const MetaDataChange: FC = ({ oldMetadata, onApply }) => { /> handleMetadataPropertyChange(event, 'b')} + value={inputValues.b} + onChange={(event) => handleInputChange(event, 'b')} onKeyPress={handleEnterPress} variant="standard" size="small" @@ -99,8 +122,8 @@ const MetaDataChange: FC = ({ oldMetadata, onApply }) => { /> handleMetadataPropertyChange(event, 's')} + value={inputValues.s} + onChange={(event) => handleInputChange(event, 's')} onKeyPress={handleEnterPress} variant="standard" size="small" @@ -108,8 +131,8 @@ const MetaDataChange: FC = ({ oldMetadata, onApply }) => { /> handleMetadataPropertyChange(event, 'd')} + value={inputValues.d} + onChange={(event) => handleInputChange(event, 'd')} onKeyPress={handleEnterPress} variant="standard" size="small" @@ -117,8 +140,8 @@ const MetaDataChange: FC = ({ oldMetadata, onApply }) => { /> handleMetadataPropertyChange(event, 'v')} + value={inputValues.v} + onChange={(event) => handleInputChange(event, 'v')} onKeyPress={handleEnterPress} variant="standard" size="small" From 6d7d07da1a565b27a5277700b2d562819b7114c8 Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 29 Mar 2026 09:38:30 +1300 Subject: [PATCH 05/25] fix: strip file extension from interpretation labels Labels like "406c.squid" now become "406c". The parentFile field retains the full filename with extension for traceability. --- .../formtatters/rawStatisticsDIRToInterpretation.ts | 7 +------ .../formtatters/rawStatisticsPMDToInterpretation.ts | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/utils/statistics/formtatters/rawStatisticsDIRToInterpretation.ts b/src/utils/statistics/formtatters/rawStatisticsDIRToInterpretation.ts index a6ef227..a6c6a83 100644 --- a/src/utils/statistics/formtatters/rawStatisticsDIRToInterpretation.ts +++ b/src/utils/statistics/formtatters/rawStatisticsDIRToInterpretation.ts @@ -9,12 +9,7 @@ const rawStatisticsDIRToInterpretation = ( filename: IDirData['name'], code: StatisticsModeDIR, ) => { - // ограничение по длине в 7 символов из-за специфики .dir файлов - // здесь оставляется 4 первые символа имени файла, далее добавится id - // получится по итогу такое: aBcD_1 или aBcD_12 - // const filenameWithoutExtension = filename.replace(/\.[^/.]+$/, ""); - // const label: string = filenameWithoutExtension.slice(0, 6); - const label = filename; + const label = filename.replace(/\.[^/.]+$/, ''); const stepRange: string = 'avg'; const stepCount: number = selectedDirections.length; diff --git a/src/utils/statistics/formtatters/rawStatisticsPMDToInterpretation.ts b/src/utils/statistics/formtatters/rawStatisticsPMDToInterpretation.ts index 4b1ec17..dafffb6 100644 --- a/src/utils/statistics/formtatters/rawStatisticsPMDToInterpretation.ts +++ b/src/utils/statistics/formtatters/rawStatisticsPMDToInterpretation.ts @@ -10,12 +10,7 @@ const rawStatisticsPMDToInterpretation = ( metadata: IPmdData['metadata'], code: StatisticsModePCA, ) => { - // ограничение по длине в 7 символов из-за специфики .dir файлов - // здесь оставляется 4 первые символа имени файла, далее добавится id - // получится по итогу такое: aBcD_1 или aBcD_12 - // const filenameWithoutExtension = metadata.name.replace(/\.[^/.]+$/, ""); - // const label: string = filenameWithoutExtension.slice(0, 6); - const label = metadata.name; + const label = metadata.name.replace(/\.[^/.]+$/, ''); const stepRange: string = `${selectedSteps[0].step}-${ selectedSteps[selectedSteps.length - 1].step From 8fbdf2b60bd380dcc3b9108fd1644faba4483396 Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 29 Mar 2026 09:39:24 +1300 Subject: [PATCH 06/25] fix: include error reason in skipped file alert message Users now see why a file was skipped (e.g., "no data lines"), not just the filename. --- src/services/axios/filesAndData.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/services/axios/filesAndData.ts b/src/services/axios/filesAndData.ts index 82782db..83365f1 100644 --- a/src/services/axios/filesAndData.ts +++ b/src/services/axios/filesAndData.ts @@ -19,18 +19,20 @@ export const filesToData = createAsyncThunk( .map((r) => r.value); const rejected = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected'); - // Optionally, inform about skipped files without failing the whole batch if (rejected.length > 0) { try { - const skippedNames = results + const skippedInfo = results .map((r, i) => ({ r, i })) .filter(({ r }) => r.status === 'rejected') - .map(({ i }) => files[i]?.name) - .filter(Boolean) as string[]; - if (skippedNames.length) { - // Non-blocking user notification; keep minimal to avoid UI dependency + .map(({ r, i }) => { + const name = files[i]?.name ?? 'unknown'; + const reason = (r as PromiseRejectedResult).reason; + const message = reason instanceof Error ? reason.message : String(reason); + return `${name}: ${message}`; + }); + if (skippedInfo.length) { // eslint-disable-next-line no-alert - alert(`Some files were skipped:\n${skippedNames.join('\n')}`); + alert(`Some files were skipped:\n\n${skippedInfo.join('\n')}`); } } catch (_) { // ignore any alert errors (e.g., server-side) From 62df3fe993d5b4777a193f2892eb6695c39a8066 Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 29 Mar 2026 09:40:19 +1300 Subject: [PATCH 07/25] fix: replace || 0 MAD fallback with explicit isFinite guard in PCA The || 0 fallback used JavaScript falsy coercion which masks NaN as zero without distinction. isFinite() explicitly catches NaN and Infinity while preserving legitimate zero values. --- src/utils/statistics/calculation/calculatePCA_dir.ts | 3 ++- src/utils/statistics/calculation/calculatePCA_pmd.ts | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/utils/statistics/calculation/calculatePCA_dir.ts b/src/utils/statistics/calculation/calculatePCA_dir.ts index 1c21902..4fc833b 100644 --- a/src/utils/statistics/calculation/calculatePCA_dir.ts +++ b/src/utils/statistics/calculation/calculatePCA_dir.ts @@ -59,7 +59,8 @@ const pcaByReference = (selectedDirections: IDirData['interpretations'], referen // Calculation of maximum angle of deviation const s1 = Math.sqrt(eig.tau[2] / eig.tau[1] + eig.tau[2] / eig.tau[0]); - MAD = Math.atan(s1) * Coordinates.RADIANS || 0; + const madValue = Math.atan(s1) * Coordinates.RADIANS; + MAD = isFinite(madValue) ? madValue : 0; return { direction: meanDirection, diff --git a/src/utils/statistics/calculation/calculatePCA_pmd.ts b/src/utils/statistics/calculation/calculatePCA_pmd.ts index e8b5c92..a50a3f9 100644 --- a/src/utils/statistics/calculation/calculatePCA_pmd.ts +++ b/src/utils/statistics/calculation/calculatePCA_pmd.ts @@ -60,13 +60,15 @@ const calculatePCA_pmd = ( if (type === 'directions') { // Calculation of maximum angle of deviation const s1 = Math.sqrt(eig.tau[0]); - MAD = Math.atan(Math.sqrt(eig.tau[1] + eig.tau[2]) / s1) * Coordinates.RADIANS || 0; + const madValue = Math.atan(Math.sqrt(eig.tau[1] + eig.tau[2]) / s1) * Coordinates.RADIANS; + MAD = isFinite(madValue) ? madValue : 0; } if (type === 'planes') { // Calculation of maximum angle of deviation const s1 = Math.sqrt(eig.tau[2] / eig.tau[1] + eig.tau[2] / eig.tau[0]); - MAD = Math.atan(s1) * Coordinates.RADIANS || 0; + const madValue = Math.atan(s1) * Coordinates.RADIANS; + MAD = isFinite(madValue) ? madValue : 0; } return { From adce5417d482ca41b65a42e91221c45bb30e7c68 Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 29 Mar 2026 10:19:53 +1300 Subject: [PATCH 08/25] fix: reject SQUID files with header but no measurement data Files with only a header line were silently accepted with 0 steps, bypassing the alert mechanism. Now throws before returning empty steps. --- src/utils/files/parsers/parserSQUID.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/utils/files/parsers/parserSQUID.ts b/src/utils/files/parsers/parserSQUID.ts index a1ec7d9..2648088 100644 --- a/src/utils/files/parsers/parserSQUID.ts +++ b/src/utils/files/parsers/parserSQUID.ts @@ -37,7 +37,12 @@ const parseSQUID = (data: string, name: string): IPmdData => { // поправка параметров 'a' и 'b': metadata.a = metadata.a < 90 ? metadata.a + 270 : metadata.a - 90; - const steps = lines.slice(1).map((line, index) => { + const dataLines = lines.slice(1); + if (dataLines.length === 0) { + throw new Error(`No measurement data in .squid file: ${name}`); + } + + const steps = dataLines.map((line, index) => { // Описывать здесь формат .squid файла я не вижу смысла, формат относительно редкий и никто // не использует его как что-то, данные в себе хранящее - все данные из него в .pmd переводят const stepSymbol = line.slice(0, 1); From d8c87b25a0e61971ef238da5fd8161ff7bce493c Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 29 Mar 2026 10:20:21 +1300 Subject: [PATCH 09/25] fix: guard against -Infinity in magnetization graph for empty data Math.max() on empty array returns -Infinity, displayed as "Mmax = -INFINITY A/m". Now defaults to 0 when no steps are present. --- src/utils/graphs/formatters/mag/dataToMag.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/utils/graphs/formatters/mag/dataToMag.ts b/src/utils/graphs/formatters/mag/dataToMag.ts index 3109116..ffdec1d 100644 --- a/src/utils/graphs/formatters/mag/dataToMag.ts +++ b/src/utils/graphs/formatters/mag/dataToMag.ts @@ -32,10 +32,13 @@ const dataToMag = (data: IPmdData, graphSize: number, hiddenStepsIDs: Array 0 ? Math.max(...mag) : 0; + const maxStep = stepValues.length > 0 ? Math.max(...stepValues) : 0; + const maxStepOrder = maxStep > 0 ? maxStep.toFixed(0).length - 1 : 0; + const stepsCeil = + maxStepOrder > 0 + ? Math.ceil(maxStep / Math.pow(10, maxStepOrder)) * Math.pow(10, maxStepOrder) + : 1; const dotsData: DotsData = stepValues.map((value, index) => { const normalizedMAG = mag[index] / maxMAG; From 69810e115f9c1c42559460d8b2ff3c21f00909a2 Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 29 Mar 2026 10:40:34 +1300 Subject: [PATCH 10/25] chore: bump version to 2.6.2 and add changelog entry --- package.json | 2 +- src/data/changelog.ts | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 9d770f9..2a9df9f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pmtools_2.0", - "version": "2.6.1", + "version": "2.6.2", "private": true, "homepage": "https://pmtools.ru/", "dependencies": { diff --git a/src/data/changelog.ts b/src/data/changelog.ts index 2b87ebf..a494f6d 100644 --- a/src/data/changelog.ts +++ b/src/data/changelog.ts @@ -5,6 +5,27 @@ export type ChangelogEntry = { }; export const CHANGELOG: ChangelogEntry[] = [ + { + version: '2.6.2', + date: 'March 29, 2026', + items: [ + 'PCA: fixed MAD calculation returning 0.0 for highly collinear data — now uses isFinite guard instead of || 0 fallback.', + 'Magnetization graph: fixed x-axis showing "mT" instead of "°C" for thermal demagnetization (read demagType from first non-NRM step).', + 'Metadata editor: fixed IEEE 754 floating-point artifacts (e.g., 22.599999999999994 instead of 22.6) by using string-based state.', + 'Metadata editor: fixed NaN when typing decimal separator ("." or ",") as first character — field no longer gets stuck.', + 'Interpretation labels: stripped file extension from component names (e.g., "300b" instead of "300b.pmd").', + 'SQUID parser: files with header but no measurement data are now rejected with a clear error message instead of crashing the app.', + 'Magnetization graph: guarded against -Infinity display when data is empty.', + 'File import: added validation modal that detects and reports invalid data rows on upload.', + 'Error boundary: added crash recovery UI so the app no longer shows a permanent white screen on unexpected errors.', + 'Parsers: added numeric field validation in DIR/PMD parsers to prevent NaN propagation.', + 'Zijderveld plot: guarded unit label against -INFINITY for empty datasets.', + 'localStorage: added safe JSON parsing to prevent blank screen on corrupted storage.', + 'Fixed React hooks order violations in DataTable components.', + 'Added ESLint, Prettier, Husky pre-commit hook, and CI typecheck/lint steps.', + 'Formatted entire codebase with Prettier; removed all console.log statements.', + ], + }, { version: '2.6.1', date: 'December 19, 2025', From 502b647df8398becf8907745d7bb18d077621993 Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 29 Mar 2026 11:10:09 +1300 Subject: [PATCH 11/25] chore: add v2.6.3 feature requests, evaluation reports and workplan --- .../merge-pmm-files.md | 22 ++ .../multi-window-support.md | 22 ++ .../vgp-both-coordinate-systems.md | 24 +++ test-data/v2.6.1/evaluation-report-10.md | 59 ++++++ test-data/v2.6.1/evaluation-report-8.md | 77 +++++++ test-data/v2.6.1/evaluation-report-9.md | 69 +++++++ test-data/v2.6.1/verify-fixes-5.md | 13 ++ test-data/v2.6.1/verify-fixes-6.md | 12 ++ test-data/v2.6.1/workplan-2026-03-29.md | 191 ++++++++++++++++++ 9 files changed, 489 insertions(+) create mode 100644 .claude/feature-requests-v2.6.3/merge-pmm-files.md create mode 100644 .claude/feature-requests-v2.6.3/multi-window-support.md create mode 100644 .claude/feature-requests-v2.6.3/vgp-both-coordinate-systems.md create mode 100644 test-data/v2.6.1/evaluation-report-10.md create mode 100644 test-data/v2.6.1/evaluation-report-8.md create mode 100644 test-data/v2.6.1/evaluation-report-9.md create mode 100644 test-data/v2.6.1/verify-fixes-5.md create mode 100644 test-data/v2.6.1/verify-fixes-6.md create mode 100644 test-data/v2.6.1/workplan-2026-03-29.md diff --git a/.claude/feature-requests-v2.6.3/merge-pmm-files.md b/.claude/feature-requests-v2.6.3/merge-pmm-files.md new file mode 100644 index 0000000..547626d --- /dev/null +++ b/.claude/feature-requests-v2.6.3/merge-pmm-files.md @@ -0,0 +1,22 @@ +# Merge multiple PMM files (append collections) + +**Requested by**: Ekaterina Kulakova (EK-march-2026, issue #2) +**Priority**: feature request +**Complexity**: medium + +## Problem + +Importing a new file replaces the previously loaded data. Users who have interpretation results split across multiple PMM files (e.g., different field seasons or labs) cannot combine them without external tools. + +## Desired behavior + +Users should be able to either: +1. Select multiple PMM files at once (Ctrl+click / Shift+click in the file dialog), or +2. Use an "Add to results" / "Append" option that imports a file without clearing existing data. + +## Technical notes + +- The file upload pipeline currently replaces the Redux store with new data on each import. +- Need to modify the import flow to support an "append" mode alongside "replace" mode. +- UI options: a toggle/checkbox in the upload dialog, or a separate "Add files" button next to the existing "Open files" button. +- Must handle potential ID/label collisions when merging collections. diff --git a/.claude/feature-requests-v2.6.3/multi-window-support.md b/.claude/feature-requests-v2.6.3/multi-window-support.md new file mode 100644 index 0000000..d0bc093 --- /dev/null +++ b/.claude/feature-requests-v2.6.3/multi-window-support.md @@ -0,0 +1,22 @@ +# Multi-window / multi-collection support + +**Requested by**: Roman Veselovsky (RV-march-2026, issue #1) +**Priority**: feature request +**Complexity**: large + +## Problem + +All browser tabs/windows share the same Redux state via localStorage. Opening PMTools in a new tab mirrors the first tab's data. Users cannot view different collections side by side. + +## Desired behavior + +Users should be able to open multiple PMTools instances (tabs or windows) and work with independent collections in each. + +## Technical notes + +- Current architecture persists Redux state to localStorage, which is shared across all tabs of the same origin. +- Possible approaches: + 1. **sessionStorage** — scoped per tab, simplest migration but breaks "reopen tab" persistence. + 2. **URL-based state** — encode collection ID in the URL; each tab loads its own data. + 3. **Per-tab ID** — generate a unique tab ID on load, key localStorage entries by tab ID. +- This is a fundamental architecture change — needs careful design to avoid breaking existing single-tab workflows. diff --git a/.claude/feature-requests-v2.6.3/vgp-both-coordinate-systems.md b/.claude/feature-requests-v2.6.3/vgp-both-coordinate-systems.md new file mode 100644 index 0000000..1edfd37 --- /dev/null +++ b/.claude/feature-requests-v2.6.3/vgp-both-coordinate-systems.md @@ -0,0 +1,24 @@ +# VGP for both geographic and stratigraphic coordinate systems + +**Requested by**: Alexander Pasenko (AP-march-2026, issue #2) +**Priority**: major improvement +**Complexity**: medium + +## Problem + +VGP calculations currently use whichever coordinate system is selected (geographic OR stratigraphic). Users need VGP computed and displayed for both systems simultaneously, similar to how PCA shows Dgeo/Igeo and Dstrat/Istrat side by side. + +## Desired behavior + +1. VGP table and graph should show poles for both geographic and stratigraphic coordinate systems at the same time. +2. (Stretch) Allow users to input custom directions directly in the VGP section and calculate VGP without importing a DIR file — currently requires an external tool like Excel. + +## Technical notes + +- Key files: + - `src/components/AppLogic/DataTablesDIR/SitesDataTable/SitesDataTable.tsx` — lines 128–130 select one coord system + - `src/utils/statistics/calculation/calculateVGP.ts` — pure VGP math + - `src/components/AppLogic/VGP/` — VGP display components + - `src/utils/GlobalTypes.ts` — VGPData type definition +- Current behavior: VGP uses either `DgeoFinal/IgeoFinal` or `DstratFinal/IstratFinal` based on the `reference` toggle. VGPData stores a single set of pole coordinates. +- Suggested approach: extend VGPData to include both `poleLatGeo/poleLonGeo` and `poleLatStrat/poleLonStrat`. Calculate VGP for both systems when site data is processed. diff --git a/test-data/v2.6.1/evaluation-report-10.md b/test-data/v2.6.1/evaluation-report-10.md new file mode 100644 index 0000000..50b6412 --- /dev/null +++ b/test-data/v2.6.1/evaluation-report-10.md @@ -0,0 +1,59 @@ +# Evaluation Report — PMTools v2.6.1 + +Date: 2026-03-29 +Scope: Targeted verification of fixes from verify-fixes-6.md (Bug 1 & Bug 2 from evaluation-report-9.md + regression checks) + +## Test Results + +### Test 1: SQUID parser rejects header-only files +- **Status**: PASS +- **Steps**: Uploaded `10bg136b.squid` (header only, no data rows) on PCA page +- **Result**: Alert appeared: "Some files were skipped: 10bg136b.squid: No measurement data in .squid file: 10bg136b.squid" +- **File did NOT appear** in the file list +- **Previously loaded data preserved**: 406c.squid and its interpretation (406c, pca, T420-T560, MAD=29.7) remained intact +- **App did NOT crash** — no white screen + +### Test 2: Magnetization graph handles empty data gracefully +- **Status**: PASS +- **Steps**: Loaded 406c.squid, checked magnetization graph +- **Result**: Graph shows "Mmax = 3.79E-3 A/m" — a valid real value, not "-INFINITY" +- **Note**: Since the header-only file is now properly rejected before loading, the empty-data scenario cannot arise in normal use. The guard against -Infinity is a defense-in-depth measure. + +### Test 3: Regression checks — all previous fixes still work + +| Check | Status | Details | +|-------|--------|---------| +| X-axis shows "°C" with range 0-600 | PASS | Magnetization graph correctly shows °C (thermal demag), range 0-600 | +| Metadata: clean float values | PASS | Core Azimuth=38.6, Core Dip=0.5, Bedding Strike=159.6, Bedding Dip=19.0 — no IEEE 754 artifacts | +| Metadata: "." then "5" input | PASS | Typed ".5" in Core Dip field, applied — shows 0.5. No NaN | +| Interpretation label | PASS | Label shows "406c" (not "406c.squid") | +| PCA MAD for T420-T560 | PASS | MAD = 29.7 (non-zero, scientifically reasonable) | + +## Bug Report + +No new bugs found. + +## Data Consistency Issues + +None detected. Table values are consistent with graph displays. + +## Console Errors + +Zero console errors throughout all tests. + +## Scores (1-5) + +| Category | Score | Notes | +|----------|-------|-------| +| Design quality | 4 | Consistent dark theme, clear layout, readable data tables | +| Functionality | 5 | All tested features work correctly, error handling is graceful | +| Technical quality | 5 | Zero console errors, proper validation with user-friendly messages | +| UX | 4 | Clear error messaging on bad file upload, metadata editing works smoothly | + +## Summary + +All 5 checks from verify-fixes-6.md pass. The two bugs from evaluation-report-9.md (header-only SQUID crash and -Infinity magnetization) are confirmed resolved. All previous fixes (°C axis, clean floats, no NaN on decimal input, label without extension, non-zero MAD) remain working. No regressions detected. + +## Priority Fixes + +None needed — all tested functionality works correctly. diff --git a/test-data/v2.6.1/evaluation-report-8.md b/test-data/v2.6.1/evaluation-report-8.md new file mode 100644 index 0000000..e166a0f --- /dev/null +++ b/test-data/v2.6.1/evaluation-report-8.md @@ -0,0 +1,77 @@ +# Evaluation Report — PMTools v2.6.1 + +Date: 2026-03-29 +Scope: Pre-evaluation checks from workplan-2026-03-29.md — confirming which reported bugs still exist + +## Pre-evaluation Results Summary + +| Check | Bug | Status | Notes | +|-------|-----|--------|-------| +| 1 | MAD=0 for PCA line fitting | **NOT REPRODUCING** | Tested with a11-19.squid (T600-T700: MAD=16.4, T510-T620: MAD=10.1) and 406c.squid (T420-T560: MAD=29.7). All MAD values non-zero. | +| 2 | Demagnetization unit (mT vs °C) | **CONFIRMED** | 406c.squid (thermal) shows "mT" on magnetization graph x-axis instead of "°C" | +| 3 | Crash on malformed file | **FIXED** | 10bg136b.squid (header only, no data) silently rejected. No crash, existing data/interpretations preserved. | +| 4 | Metadata floating-point precision | **CONFIRMED** | Core Azimuth shows `38.599999999999994` instead of `38.6` in metadata editor | +| 5 | Metadata NaN on decimal input | **INCONCLUSIVE** | Playwright automation couldn't fully reproduce the manual user flow (fill→type "." resulted in "0", not NaN). Needs manual testing. | + +--- + +## Bug Report + +### Bug 1: Magnetization graph shows "mT" instead of "°C" for thermal treatment +- **What**: When loading a SQUID file with thermal demagnetization steps (prefixed "T"), the magnetization decay graph x-axis incorrectly shows "mT" (millitesla) instead of "°C" (degrees Celsius). +- **Steps**: + 1. Upload `.claude/issues/RV-march-2026/406c.squid` on PCA page + 2. Click "RMG" tab to view magnetization graph + 3. Observe x-axis label +- **Expected vs Actual**: Expected "°C" for thermal demagnetization; actual shows "mT" +- **Severity**: major — misleading scientific data presentation + +### Bug 2: Metadata editor shows floating-point artifacts +- **What**: The metadata editor shows IEEE 754 floating-point artifacts for parsed decimal values (e.g., `38.599999999999994` instead of `38.6`). +- **Steps**: + 1. Upload `406c.squid` on PCA page + 2. Click the Edit (pencil) icon in the metadata row + 3. Observe the Core Azimuth field +- **Expected vs Actual**: Expected `38.6`; actual shows `38.599999999999994` +- **Severity**: major — confusing to users, could lead to incorrect manual edits + +### Bug 3: No user feedback when malformed file is silently rejected +- **What**: When uploading a SQUID file with header but no data rows, the file is silently ignored with no error message or alert. +- **Steps**: + 1. Load a valid file (e.g., 406c.squid) and compute interpretations + 2. Upload `.claude/issues/RV-march-2026/10bg136b.squid` + 3. Observe: nothing happens — no error shown, no modal, file silently skipped +- **Expected vs Actual**: Expected a validation modal or error alert explaining the file has no data; actual is silent rejection +- **Severity**: minor — no crash (good!), but confusing UX. The user won't know why the file wasn't loaded. + +## Data Consistency Issues + +None observed. PCA results matched expected ranges for the test data. + +## Console Errors + +- 2x `findDOMNode` deprecation warnings (React StrictMode + DraggableCore) — known, non-blocking +- No actual runtime errors + +## Scores (1-5) + +| Category | Score | Notes | +|----------|-------|-------| +| Design quality | 4 | Clean layout, consistent styling | +| Functionality | 3 | Two confirmed bugs affecting data display (mT/°C, float artifacts) | +| Technical quality | 4 | No crashes, graceful handling of malformed input, only StrictMode warnings | +| UX | 3 | Silent file rejection is confusing; metadata float display is misleading | + +## Priority Fixes + +1. **Fix 2: Demagnetization unit (mT → °C)** — straightforward, high visibility, misleading scientific output +2. **Fix 3: Metadata floating-point display** — affects data integrity perception, same component as NaN bug +3. **Fix 4: Metadata NaN on decimal input** — needs manual verification, but likely still present based on code analysis +4. **Fix 1: MAD=0** — not reproducing in current tests, may need specific data conditions or may have been partially fixed +5. **Silent rejection feedback** — minor UX improvement, add error toast/modal for files with no data rows + +## Notes + +- **MAD=0 bug**: Could not reproduce with either a11-19.squid or 406c.squid. The workplan mentions steps 17-23 (T510-T700) but step 17 in the app corresponds to T600, not T510. The workplan step labels may refer to a different numbering. However, all tested ranges produced non-zero MAD values. This may have been partially fixed by recent eigenvalue handling changes, or may require very specific data conditions not met by the available test files. **Recommend keeping in the fix list** — the code analysis in the workplan identifies real issues (the `|| 0` fallback and eigenvalue normalization). +- **Crash on malformed file**: Appears to be fixed by recent validation work (commits 591c6ef, 2bd821a, bb7494b, 0be633d). The app handles it gracefully. Consider adding a user-facing notification. +- **NaN bug**: Playwright's `fill()` method bypasses React's `onChange` handler, so the NaN path wasn't triggered. The code analysis in the workplan (`+event.target.value` on ".") strongly suggests the bug still exists for real user input. **Recommend manual testing or fixing based on code analysis.** diff --git a/test-data/v2.6.1/evaluation-report-9.md b/test-data/v2.6.1/evaluation-report-9.md new file mode 100644 index 0000000..ce41db3 --- /dev/null +++ b/test-data/v2.6.1/evaluation-report-9.md @@ -0,0 +1,69 @@ +# Evaluation Report — PMTools v2.6.1 + +Date: 2026-03-29 +Scope: verify-fixes-5.md — targeted verification of 6 fixes from workplan-2026-03-29.md + +## Test Results Summary + +| # | Fix | Result | Details | +|---|-----|--------|---------| +| 1 | Magnetization graph shows °C for thermal data | **PASS** | X-axis shows "°C" with range 0–600 for 406c.squid (thermal demag) | +| 2 | Metadata editor shows clean float values | **PASS** | Core Azimuth=38.6, Core Dip=55, Bedding Strike=159.6 — no IEEE 754 artifacts | +| 3 | Metadata editor handles decimal input without NaN | **PASS** | Typing "." then "5" → field shows ".5", Apply → 0.5. No NaN at any point | +| 4 | Interpretation labels don't include file extension | **PASS** | New PCA interpretation shows label "406c" (not "406c.squid") | +| 5 | Rejected files show error reason in alert | **PARTIAL** | No crash, data preserved, but no alert shown (see Bug 1) | +| 6 | MAD values for PCA are non-zero | **PASS** | MAD=29.7 for 406c.squid T420-T560 (5 steps). Non-zero, scientifically reasonable | + +## Bug Report + +### Bug 1: Malformed SQUID file silently accepted with 0 data rows — no alert shown +- **What**: Uploading 10bg136b.squid (header only, no data rows) does not show an alert with error reason. The file is silently accepted and added to the file list with 0 data rows. +- **Steps**: + 1. Load 406c.squid on PCA page and compute interpretations + 2. Upload 10bg136b.squid via file upload + 3. No alert appears + 4. Navigate to 10bg136b.squid — shows "No rows" in data table and "Mmax = -INFINITY A/m" on magnetization graph +- **Expected**: Alert saying "Some files were skipped: 10bg136b.squid: no data lines" (or similar). The file should be rejected, not added to file list. +- **Actual**: File is silently loaded with metadata but 0 data rows. No user feedback. The SQUID parser does not reject the promise for files with header-only content — it resolves successfully with empty steps. The alert mechanism in `filesAndData.ts` (line 35) only triggers for rejected promises. +- **Severity**: major — user has no feedback that file is invalid; navigating to it shows broken graph with "-INFINITY" + +### Bug 2: Magnetization graph shows "Mmax = -INFINITY A/m" for empty file +- **What**: When navigating to 10bg136b.squid (0 data rows), the magnetization graph shows "Mmax = -INFINITY A/m" instead of a graceful empty state. +- **Steps**: Navigate to a SQUID file with header but no data rows +- **Expected**: Graph should show empty state or "No data" message +- **Actual**: Shows "Mmax = -INFINITY A/m" and "Unit= A/m" (missing value) +- **Severity**: minor — cosmetic issue on an edge case, but looks broken + +### Bug 3: Stale interpretation label persists in localStorage +- **What**: Interpretations created before the extension-stripping fix retain the old label format (e.g., "406c.squid" instead of "406c") in localStorage. Only newly created interpretations get the correct label. +- **Steps**: Load the app with a pre-existing interpretation from before the fix +- **Expected**: All labels should show without extension +- **Actual**: Old labels show "406c.squid", new labels show "406c" +- **Severity**: cosmetic — resolves itself as users re-compute interpretations + +## Data Consistency Issues + +No mismatches found between table data and graph visualizations during testing. + +## Shortcut Issues + +No shortcut issues found (shortcuts not explicitly tested in this targeted evaluation). + +## Console Errors + +1. `Warning: Failed prop type: apiRef.current is null in DataGrid` — React PropTypes warning in StatisticsDataTablePMD. Non-critical but recurring. + +## Scores (1-5) + +| Category | Score | Notes | +|----------|-------|-------| +| Design quality | 4 | Clean layout, consistent styling, graphs render well | +| Functionality | 4 | All 6 targeted fixes verified. 5/6 fully working, 1 partial (malformed file handling) | +| Technical quality | 3.5 | PropTypes warning in console, -INFINITY display for empty data, silent failure on malformed files | +| UX | 3.5 | Good overall, but no feedback when malformed file is uploaded silently | + +## Priority Fixes + +1. **SQUID parser should reject files with 0 data rows** — add validation that rejects the promise when `steps.length === 0` (or only NRM), so the alert mechanism in `filesAndData.ts` can catch it and show the error reason +2. **Handle -INFINITY / empty data in magnetization graph** — guard against `Math.max()` on empty arrays returning `-Infinity` +3. **Migration for stale localStorage labels** — optional, low priority since it self-resolves diff --git a/test-data/v2.6.1/verify-fixes-5.md b/test-data/v2.6.1/verify-fixes-5.md new file mode 100644 index 0000000..80b8d27 --- /dev/null +++ b/test-data/v2.6.1/verify-fixes-5.md @@ -0,0 +1,13 @@ +Check that all fixes from evaluation-report-8.md are resolved: + +1. **Magnetization graph shows correct unit for thermal data**: Upload `.claude/issues/RV-march-2026/406c.squid` on PCA page. Click the "RMG" tab to view the magnetization decay graph. The x-axis label should show "°C" (not "mT") since this is thermal demagnetization data. Also test with an AF demagnetization file (e.g., a .pmd file) — x-axis should still show "mT". + +2. **Metadata editor shows clean float values**: Upload `406c.squid` on PCA page. Click the Edit (pencil) icon in the metadata row. Core Azimuth should show a clean number like `38.6` (not `38.599999999999994`). All other fields (Core Dip, Bedding Strike, Bedding Dip, Volume) should also show clean values without IEEE 754 artifacts. + +3. **Metadata editor handles decimal input without NaN**: In the metadata editor (from step 2), clear the Core Dip field completely and type just `.` (period). The field should NOT show NaN — it should accept the intermediate input. Then type `5` so the field shows `.5` or `0.5`. Press Apply — the value should be applied as `0.5`. Also try typing `,5` (comma as decimal separator for Russian locale) — should work the same way. + +4. **Interpretation labels don't include file extension**: Upload a `.squid` or `.pmd` file on PCA page. Select some steps and compute a PCA direction (Odir). In the interpretations table, the label column should show the filename WITHOUT extension (e.g., `406c` not `406c.squid`). Export to DIR format — labels should also be without extension. + +5. **Rejected files show error reason in alert**: Load a valid file (e.g., `406c.squid`) on PCA page. Then upload `.claude/issues/RV-march-2026/10bg136b.squid` (header only, no data rows). An alert should appear that includes both the filename AND the reason it was skipped (e.g., "no data lines"). Previously loaded data and interpretations should be preserved. + +6. **MAD values for PCA**: Upload `.claude/issues/AP-march-2026/a11-19.squid` on PCA page. Select steps in the T510-T700 range, compute PCA line (Odir). MAD should be a reasonable positive value (typically 1-15 degrees for real data), not exactly 0.0. Also test with `406c.squid` — select any range of steps and compute PCA, verify MAD is non-zero. diff --git a/test-data/v2.6.1/verify-fixes-6.md b/test-data/v2.6.1/verify-fixes-6.md new file mode 100644 index 0000000..9b9a541 --- /dev/null +++ b/test-data/v2.6.1/verify-fixes-6.md @@ -0,0 +1,12 @@ +Check that Bug 1 and Bug 2 from evaluation-report-9.md are resolved: + +1. **SQUID parser rejects header-only files**: Upload 10bg136b.squid (header only, no data rows) on PCA page. An alert should appear saying "Some files were skipped: 10bg136b.squid: No measurement data in .squid file: 10bg136b.squid" (or similar). The file should NOT appear in the file list. + +2. **Magnetization graph handles empty data gracefully**: If any file with 0 data rows somehow gets loaded, the magnetization graph should show "Mmax = 0.00E+0 A/m" instead of "Mmax = -INFINITY A/m". Verify by loading a valid file (406c.squid), confirming the graph renders normally with a real Mmax value. + +3. **Regression check — all previous fixes still work**: + - Load 406c.squid (thermal demag) on PCA page. X-axis should show "°C" with range 0-600. + - Check metadata editor: Core Azimuth, Core Dip, Bedding Strike show clean float values (no IEEE 754 artifacts). + - In metadata editor, type "." then "5" in a numeric field — should show ".5", Apply should give 0.5. No NaN. + - Create a new PCA interpretation — label should show "406c" (not "406c.squid"). + - Compute PCA on 406c.squid T420-T560 — MAD should be non-zero (around 29.7). diff --git a/test-data/v2.6.1/workplan-2026-03-29.md b/test-data/v2.6.1/workplan-2026-03-29.md new file mode 100644 index 0000000..d54f342 --- /dev/null +++ b/test-data/v2.6.1/workplan-2026-03-29.md @@ -0,0 +1,191 @@ +# Work Plan — PMTools v2.6.1 + +Date: 2026-03-29 +Sources: .claude/issues/RV-march-2026, .claude/issues/AP-march-2026, .claude/issues/EK-march-2026 + +## Pre-evaluation (run /evaluate with these checks first) + +Before fixing, confirm these bugs still exist on the live app: + +1. **MAD=0 bug**: Upload `.claude/issues/AP-march-2026/a11-19.squid` on PCA page. Select steps 17–23 (T510–T700), compute PCA line (Odir). Check if MAD shows 0.0. Also try steps 9–18 (T320–T600). + +2. **Demagnetization unit bug**: Upload `.claude/issues/RV-march-2026/406c.squid` on PCA page. Check whether the magnetization decay graph x-axis shows "mT" or "°C". It should show "°C" (thermal treatment). + +3. **Crash on malformed file**: Upload `.claude/issues/RV-march-2026/10bg136b.squid` on PCA page (has header but no data rows). Check if the app crashes to white screen or shows a graceful error. + +4. **Metadata floating-point bug**: Upload any SQUID file (e.g., 406c.squid). Open the metadata editor (click pencil/edit icon). Check if Core Azimuth shows a floating-point value like `22.599999999999994` instead of `22.6`. + +5. **Metadata NaN bug**: In the metadata editor, try typing a decimal point (`.` or `,`) as the first character in Core Dip. Check if the field becomes NaN and gets stuck. + +--- + +## Fixes (run /generate for each) + +### Fix 1: MAD = 0.0 for PCA line fitting [critical] +- **Reported by**: Roma Veselovsky (.claude/issues/RV-march-2026/issues.md#2b), Sasha Pasenko (.claude/issues/AP-march-2026/issues.md#1) +- **What**: When computing PCA directions (line fitting), MAD is calculated as exactly 0.0 in cases where it should be a small positive number. Scientifically incorrect — MAD should never be exactly zero for real measurement data. +- **Reproduce**: + 1. Upload `.claude/issues/AP-march-2026/a11-19.squid` on PCA page + 2. Select steps 17–23 (T510–T700), compute PCA line (Odir) + 3. MAD shows 0.0 — should be a small positive value + 4. Also reproducible with steps 9–18 (T320–T600) + 5. Also reproducible with `.claude/issues/RV-march-2026/406c.squid` steps 9–13 (any steps) +- **Files to investigate**: + - [src/utils/statistics/calculation/calculatePCA_pmd.ts](src/utils/statistics/calculation/calculatePCA_pmd.ts) — lines 60–64, MAD formula for directions + - [src/utils/statistics/calculation/calculatePCA_dir.ts](src/utils/statistics/calculation/calculatePCA_dir.ts) — lines 60–62, same formula for DIR page + - [src/utils/statistics/eigManipulations.ts](src/utils/statistics/eigManipulations.ts) — eigenvalue normalization +- **Fix direction**: The root cause is in the MAD formula at line 63 of `calculatePCA_pmd.ts`: + ```typescript + MAD = Math.atan(Math.sqrt(eig.tau[1] + eig.tau[2]) / s1) * Coordinates.RADIANS || 0; + ``` + When eigenvalues `tau[1]` and `tau[2]` are very small (highly collinear data), the numerator approaches 0, giving `Math.atan(0) = 0`. The `|| 0` fallback also masks NaN cases into 0. Two issues to fix: + 1. The `|| 0` should only catch NaN/Infinity, not legitimate small values — replace with explicit `isNaN`/`isFinite` guard + 2. Check if eigenvalue normalization in `sortEigenvectors()` causes precision loss when eigenvalues span many orders of magnitude. The normalization divides by trace, which can lose precision for very small eigenvalues. + + **Important**: This is scientific calculation code — verify the formula against Kirschvink (1980) or Lisa Tauxe's textbook before modifying. The correct MAD formula for directions is: `MAD = arctan(sqrt(τ₂ + τ₃) / sqrt(τ₁))` where τ values are eigenvalues (NOT normalized). Check whether using raw eigenvalues instead of normalized ones fixes the precision issue. +- **Verify**: Upload a11-19.squid, compute PCA for steps 17–23 and 9–18. MAD should be > 0.0 and scientifically reasonable (typically 1–15° for real data). Cross-check with 406c.squid steps 9–13. + +### Fix 2: Magnetization graph shows "mT" instead of "°C" for thermal treatment [major] +- **Reported by**: Roma Veselovsky (.claude/issues/RV-march-2026/issues.md#2a) +- **What**: When loading a SQUID file with thermal demagnetization (steps prefixed "TT"), the magnetization decay graph x-axis incorrectly shows "mT" instead of "°C". +- **Reproduce**: + 1. Upload `.claude/issues/RV-march-2026/406c.squid` on PCA page + 2. Look at the magnetization decay graph (bottom-right) + 3. X-axis label shows "mT" — should show "°C" +- **Files to investigate**: + - [src/utils/graphs/formatters/mag/dataToMag.ts](src/utils/graphs/formatters/mag/dataToMag.ts) — line 54: `const demagnetizationType = data.steps[0].demagType;` + - [src/components/AppGraphs/MagGraph/AxesAndData.tsx](src/components/AppGraphs/MagGraph/AxesAndData.tsx) — line 50: axis label ternary + - [src/utils/files/parsers/parserSQUID.ts](src/utils/files/parsers/parserSQUID.ts) — lines 76–83: demagType detection +- **Fix direction**: The bug is in `dataToMag.ts` line 54 — it reads `data.steps[0].demagType`, but `steps[0]` is always the NRM step, which has `demagType = undefined`. When undefined, the ternary in AxesAndData defaults to "mT". Fix: use the first non-NRM step to determine demagnetization type. For example: + ```typescript + const demagnetizationType = data.steps.find(step => step.demagType)?.demagType; + ``` + This skips NRM and finds the first step with an actual demagType. +- **Verify**: Upload 406c.squid. Magnetization graph x-axis should show "°C". Also test with an AF demagnetization file to ensure "mT" still works. + +### Fix 3: Metadata editor — floating-point precision error [major] +- **Reported by**: Roma Veselovsky (.claude/issues/RV-march-2026/issues.md#4) +- **What**: When viewing/editing metadata (Core Azimuth, Core Dip, etc.), values show floating-point artifacts like `22.599999999999994` instead of clean numbers. +- **Reproduce**: + 1. Upload a SQUID file (e.g., 406c.squid) on PCA page + 2. Open metadata editor (click on the metadata row) + 3. Core Azimuth may show `22.599999999999994` instead of `22.6` + 4. Screenshot: `.claude/issues/RV-march-2026/screenshot_core_azimuth_nan.png` +- **Files to investigate**: + - [src/components/Common/DataTable/MetaDataChange/MetaDataChange.tsx](src/components/Common/DataTable/MetaDataChange/MetaDataChange.tsx) — lines 70–78, line 84 + - [src/utils/files/parsers/parserSQUID.ts](src/utils/files/parsers/parserSQUID.ts) — metadata parsing +- **Fix direction**: The floating-point issue likely originates from the SQUID parser when parsing the header line. The value `22.6` in the file becomes `22.599999999999994` due to IEEE 754 floating-point representation. Fix at display level: round values in the TextField `value` prop using `parseFloat(value.toFixed(1))` or similar. Also consider rounding at parse time in the SQUID parser for metadata values that are entered as integers/simple decimals. +- **Verify**: Upload 406c.squid. Open metadata editor. Core Azimuth should show a clean number (e.g., `22.6` or `23`), not `22.599999999999994`. + +### Fix 4: Metadata editor — NaN on decimal separator input [major] +- **Reported by**: Roma Veselovsky (.claude/issues/RV-march-2026/issues.md#5) +- **What**: Typing a decimal separator (`.` or `,`) as the first character in a metadata field produces NaN, which then can't be cleared — the field is permanently stuck on NaN until the page is reloaded. +- **Reproduce**: + 1. Upload any file on PCA page + 2. Open metadata editor + 3. Clear the Core Dip field and type just `.` or `,` + 4. Field shows NaN and can't be fixed + 5. Screenshot: `.claude/issues/RV-march-2026/screenshot_core_azimuth_nan.png` +- **Files to investigate**: + - [src/components/Common/DataTable/MetaDataChange/MetaDataChange.tsx](src/components/Common/DataTable/MetaDataChange/MetaDataChange.tsx) — line 76: `[property]: +event.target.value` +- **Fix direction**: The unary `+` operator on strings like `"."`, `","`, or `""` returns `NaN`. Fix the `handleMetadataPropertyChange` function to: + 1. Keep the raw string in a local state for display (so users can type intermediate values like "0." or "22.") + 2. Only convert to number on blur or when applying + 3. Or: validate the string before conversion — if `isNaN(+value)`, keep the previous valid value + 4. Also handle comma as decimal separator (common in Russian locale) by replacing `,` with `.` before parsing +- **Verify**: Open metadata editor. Type `.5` in Core Dip — should show `0.5` (or allow intermediate "." without NaN). Type `,5` — should also work. Clear field and re-enter — should work without getting stuck. + +### Fix 5: File extension appears in component label on export [minor] +- **Reported by**: Roma Veselovsky (.claude/issues/RV-march-2026/issues.md#3) +- **What**: When exporting interpreted components to PMM or other formats, the ID/label includes the file extension (e.g., `300b.pmd` instead of `300b`). +- **Reproduce**: + 1. Upload a .pmd file on PCA page + 2. Compute a PCA component + 3. Export to DIR format and to PMM format + 4. The label/ID column shows `300b.pmd` instead of `300b` +- **Files to investigate**: + - [src/utils/statistics/formtatters/rawStatisticsPMDToInterpretation.ts](src/utils/statistics/formtatters/rawStatisticsPMDToInterpretation.ts) — line 18: `const label = metadata.name;` (includes extension) + - [src/utils/files/converters/dir.ts](src/utils/files/converters/dir.ts) — line 20: `interpretation.label.slice(0, 6)` (truncates but doesn't strip extension) + - [src/utils/files/subFunctions.ts](src/utils/files/subFunctions.ts) — `getFileName()` function (already strips extension, but not used for labels) +- **Fix direction**: In `rawStatisticsPMDToInterpretation.ts`, line 18 should strip the extension before assigning label. The old code is even commented out on lines 14–16 — uncomment and adapt: + ```typescript + const label = metadata.name.replace(/\.[^/.]+$/, ''); + ``` + Note: keep `parentFile: metadata.name` unchanged (with extension) for traceability. Only the `label` field should lose the extension. Also check if `.dir` export's `slice(0, 6)` truncation is still appropriate after the fix. +- **Verify**: Upload a .pmd file, compute PCA, export to PMM format. The ID column should show `300b` not `300b.pmd`. Export to .dir — label should be `300b` (up to 6 chars, no extension). + +### Fix 6: Crash on malformed SQUID file (header only, no data) [critical — already partially fixed] +- **Reported by**: Roma Veselovsky (.claude/issues/RV-march-2026/issues.md#6), Katya Kulakova (.claude/issues/EK-march-2026/issues.md#1) +- **What**: Loading a SQUID file that has a header but no measurement rows crashes the app to a white screen. User loses all previously computed interpretations. This is especially painful when working with 900+ files. +- **Reproduce**: + 1. Upload some valid files and compute interpretations on PCA page + 2. Then upload `.claude/issues/RV-march-2026/10bg136b.squid` (2-line file: header + metadata, no data) + 3. Check if app crashes or shows graceful error +- **Files to investigate**: + - [src/utils/files/parsers/parserSQUID.ts](src/utils/files/parsers/parserSQUID.ts) — empty data validation + - [src/services/axios/filesAndData.ts](src/services/axios/filesAndData.ts) — error handling pipeline + - [src/components/Common/ErrorBoundary/ErrorBoundary.tsx](src/components/Common/ErrorBoundary/ErrorBoundary.tsx) — fallback UI + - Recent commits: `591c6ef` (validate numeric fields), `2bd821a` (validation modal), `bb7494b` (safe JSON parsing), `0be633d` (guard Zijderveld) +- **Fix direction**: The SQUID parser already has validation for empty files and no-data-lines (added in recent commits). The validation modal catches invalid rows. However, need to verify: + 1. Does the SQUID parser correctly reject a file with header but 0 data rows? + 2. Does the rejection propagate as a user-friendly error (not a crash)? + 3. Are existing interpretations preserved when a bad file is uploaded in a batch? + This may already be fixed by recent work — pre-evaluation will confirm. +- **Verify**: Upload 10bg136b.squid. Should see either a validation modal or an error alert. App should NOT crash. Any previously loaded data should be preserved. + +--- + +## Improvements + +### Improvement 1: VGP for both coordinate systems [major] +- **Reported by**: Sasha Pasenko (.claude/issues/AP-march-2026/issues.md#2) +- **What**: VGP calculations currently use whichever coordinate system the user has selected (geographic OR stratigraphic). Users want VGP calculated and displayed for BOTH coordinate systems simultaneously, similar to how PCA shows Dgeo/Igeo and Dstrat/Istrat. +- **Files to investigate**: + - [src/components/AppLogic/DataTablesDIR/SitesDataTable/SitesDataTable.tsx](src/components/AppLogic/DataTablesDIR/SitesDataTable/SitesDataTable.tsx) — lines 128–130: selects one coord system + - [src/utils/statistics/calculation/calculateVGP.ts](src/utils/statistics/calculation/calculateVGP.ts) — pure VGP math + - [src/components/AppLogic/VGP/](src/components/AppLogic/VGP/) — VGP display components + - [src/utils/GlobalTypes.ts](src/utils/GlobalTypes.ts) — VGPData type definition +- **Current behavior**: VGP uses either `DgeoFinal`/`IgeoFinal` or `DstratFinal`/`IstratFinal` based on the current `reference` toggle. The VGPData type stores a single set of pole coordinates. +- **Suggested approach**: Extend VGPData to include both `poleLatGeo`/`poleLonGeo` and `poleLatStrat`/`poleLonStrat`. Calculate VGP for both systems when site data is processed. Display can show both or allow toggling. + +### Improvement 2: VGP inline coordinate input [minor] +- **Reported by**: Sasha Pasenko (.claude/issues/AP-march-2026/issues.md#2) +- **What**: Allow users to input custom directions directly in the VGP section and calculate VGP without going through DIR import. Currently this requires an external tool (Excel). +- **Complexity estimate**: medium — needs new input UI component in VGP section, wiring to calculateVGP, and display of results. + +--- + +## Feature Requests (backlog) + +### Feature 1: Multi-window / multi-collection support +- **Reported by**: Roma Veselovsky (.claude/issues/RV-march-2026/issues.md#1) +- **What**: Allow opening PMTools in multiple browser tabs/windows with independent data. Currently all tabs share the same Redux state via localStorage, so opening a new tab shows the same collection. +- **Complexity estimate**: large — requires either session-scoped storage (sessionStorage instead of localStorage), URL-based state, or a per-tab ID system. Fundamental architecture change. + +### Feature 2: Merge multiple PMM files +- **Reported by**: Katya Kulakova (.claude/issues/EK-march-2026/issues.md#2) +- **What**: Allow importing multiple PMM files at once (Ctrl+click) or adding files to existing results ("append to collection"). Currently importing a new file replaces the previous data. +- **Complexity estimate**: medium — need to modify the file upload pipeline to support "append" mode alongside "replace" mode. UI needs a toggle or dialog. + +--- + +## Summary + +| Category | Count | Already fixed | +|----------|-------|---------------| +| Critical bugs | 2 | 1 partially (crash on malformed — needs verification) | +| Major bugs | 3 | 0 | +| Minor bugs | 1 | 0 | +| Improvements | 2 | 0 | +| Features | 2 | 0 | + +### Recommended execution order + +1. **Pre-evaluate** crash on malformed file (Fix 6) — may already be fixed +2. **Fix 1** (MAD=0) — critical scientific correctness, two reporters +3. **Fix 2** (demagnetization unit) — straightforward, high visibility +4. **Fix 4** (NaN on decimal input) — blocks user workflow +5. **Fix 3** (floating-point display) — related to Fix 4, same component +6. **Fix 5** (file extension in label) — minor, quick fix +7. **Fix 6** (crash on malformed) — if pre-evaluation shows it's still broken +8. **Improvement 1** (VGP both coord systems) — significant value for users From 8b8147e55fc2b0f60279ed27ac399b7b26db24e4 Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 29 Mar 2026 11:12:23 +1300 Subject: [PATCH 12/25] =?UTF-8?q?fix:=20remove=20premature=20MAD=3D0=20fix?= =?UTF-8?q?=20from=20changelog=20=E2=80=94=20issue=20still=20under=20inves?= =?UTF-8?q?tigation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data/changelog.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/data/changelog.ts b/src/data/changelog.ts index a494f6d..502ae8d 100644 --- a/src/data/changelog.ts +++ b/src/data/changelog.ts @@ -9,7 +9,6 @@ export const CHANGELOG: ChangelogEntry[] = [ version: '2.6.2', date: 'March 29, 2026', items: [ - 'PCA: fixed MAD calculation returning 0.0 for highly collinear data — now uses isFinite guard instead of || 0 fallback.', 'Magnetization graph: fixed x-axis showing "mT" instead of "°C" for thermal demagnetization (read demagType from first non-NRM step).', 'Metadata editor: fixed IEEE 754 floating-point artifacts (e.g., 22.599999999999994 instead of 22.6) by using string-based state.', 'Metadata editor: fixed NaN when typing decimal separator ("." or ",") as first character — field no longer gets stuck.', From 779aca0ddb7aa3e20fcf073c8fceb73d1512331c Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 29 Mar 2026 11:13:41 +1300 Subject: [PATCH 13/25] chore: add MAD=0 investigation issue to feature-requests-v2.6.3 --- .../mad-zero-investigation.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .claude/feature-requests-v2.6.3/mad-zero-investigation.md diff --git a/.claude/feature-requests-v2.6.3/mad-zero-investigation.md b/.claude/feature-requests-v2.6.3/mad-zero-investigation.md new file mode 100644 index 0000000..6ea2af0 --- /dev/null +++ b/.claude/feature-requests-v2.6.3/mad-zero-investigation.md @@ -0,0 +1,22 @@ +# MAD=0 investigation — awaiting data from scientists + +**Reported by**: scientists (EK-march-2026) +**Priority**: bug (unresolved) +**Complexity**: unknown — need more information + +## Problem + +PCA MAD calculation returns 0.0 for some data. Root cause is unclear — the initial `|| 0` → `isFinite` guard did not address the real issue. + +## Status + +Request sent to scientists (2026-03-29) asking for: +1. Direct comparisons between PMTools and software where MAD is calculated correctly +2. The formula they expect for PCA and PCA0 MAD calculation +3. More data files and step ranges showing discrepancies + +## Technical notes + +- The `|| 0` fallback in PCA was replaced with an `isFinite` guard, but this is a defensive fix, not a root-cause fix. +- Relevant code: `src/utils/statistics/calculation/calculatePCA_pmd.ts` +- Cannot proceed without reference data or expected formulas from scientists. From 131cd8be23ea83597031bb00098508af27bba626 Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Thu, 2 Apr 2026 13:58:39 +1300 Subject: [PATCH 14/25] fix: resolve MAD=0 bug in PCA eigenvalue sorting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Off-by-one indexing in sortEigenvectors() used [1,2,3] instead of [0,1,2] to find the middle eigenvalue index. When numeric.eig returned eigenvalues with the middle one at index 0, the filter produced index 3 (out of bounds) → undefined → NaN → MAD=0 via isFinite guard. Also removed no-op vectors.push(...vectors) in PCA0 path — TMatrix normalizes by array length, so doubling had no mathematical effect. --- .../statistics/calculation/calculatePCA_pmd.ts | 7 +++---- src/utils/statistics/eigManipulations.ts | 2 +- test-data/406c.squid | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 test-data/406c.squid diff --git a/src/utils/statistics/calculation/calculatePCA_pmd.ts b/src/utils/statistics/calculation/calculatePCA_pmd.ts index a50a3f9..12b897d 100644 --- a/src/utils/statistics/calculation/calculatePCA_pmd.ts +++ b/src/utils/statistics/calculation/calculatePCA_pmd.ts @@ -24,10 +24,9 @@ const calculatePCA_pmd = ( const firstVector = new Coordinates(...vectors[0]); const lastVector = new Coordinates(...vectors[vectors.length - 1]); - // When anchoring we mirror the points and add them - // in opposite case need to transform to the center of mass - if (anchored) vectors.push(...vectors); - else { + // When not anchoring, transform to the center of mass + // For PCA0 (anchored), we use raw vectors — no mean subtraction needed + if (!anchored) { for (var i = 0; i < vectors.length; i++) { for (var j = 0; j < 3; j++) { centerMass[j] += vectors[i][j] / selectedSteps.length; diff --git a/src/utils/statistics/eigManipulations.ts b/src/utils/statistics/eigManipulations.ts index b7594ed..4ee6b08 100644 --- a/src/utils/statistics/eigManipulations.ts +++ b/src/utils/statistics/eigManipulations.ts @@ -36,7 +36,7 @@ export const sortEigenvectors = (eig: { lambda: any; E: any }) => { const indexOfT1: number = eig.lambda.x.indexOf(t1); const indexOfT3: number = eig.lambda.x.indexOf(t3); // now we can determine middle eigenvalue index and find it - const indexOfT2 = [1, 2, 3].filter((index) => index !== indexOfT1 && index !== indexOfT3)[0]; + const indexOfT2 = [0, 1, 2].filter((index) => index !== indexOfT1 && index !== indexOfT3)[0]; const t2 = eig.lambda.x[indexOfT2]; // Sort eigenvectors diff --git a/test-data/406c.squid b/test-data/406c.squid new file mode 100644 index 0000000..3b373f1 --- /dev/null +++ b/test-data/406c.squid @@ -0,0 +1,15 @@ +7bg 406c + 128.6 35.0 159.6 19.0 8.0 +NRM 107.5 46.5 126.3 59.6 3.79E-06 000.4 108.4 47.4 0.015896 0.023829 0.019909 khramov 2025-06-11 20:08:31 +TT 100 095.1 46.6 109.8 62.7 1.88E-06 000.4 100.4 54.3 0.005911 0.010791 0.020029 khramov 2025-06-16 11:44:38 +TT 150 091.4 43.3 102.7 60.3 1.24E-06 000.5 093.3 54.5 0.005783 0.007358 0.018769 khramov 2025-06-16 13:41:26 +TT 200 092.9 37.5 102.2 54.4 7.89E-07 000.8 087.4 49.9 0.001980 0.004037 0.018396 khramov 2025-06-16 15:42:27 +TT 250 093.4 33.5 101.4 50.4 5.20E-07 001.6 083.5 46.7 0.001937 0.002109 0.018941 khramov 2025-06-16 19:51:47 +TT 300 094.9 28.1 101.7 44.8 3.25E-07 002.9 079.9 41.8 0.000345 0.001518 0.018909 khramov 2025-06-17 11:02:40 +TT 340 088.1 34.4 094.8 52.1 2.82E-07 000.3 079.6 50.4 0.000407 0.001392 0.000594 khramov 2025-06-17 13:47:27 +TT 380 088.5 41.6 097.8 59.1 1.78E-07 000.6 088.6 55.1 0.000385 0.001956 0.000553 khramov 2025-06-17 15:32:47 +TT 420 096.3 34.2 105.4 50.6 1.42E-07 000.7 086.8 45.6 0.000518 0.002101 0.000458 khramov 2025-06-17 17:23:32 +TT 460 103.3 62.2 142.8 74.3 1.15E-07 000.8 129.3 57.0 0.000630 0.001625 0.000378 khramov 2025-06-17 19:04:00 +TT 500 092.2 49.9 108.0 66.5 8.13E-08 002.6 103.7 57.8 0.001856 0.003105 0.000493 khramov 2025-06-18 12:56:05 +TT 530 095.4 03.7 097.2 20.7 6.29E-08 002.4 063.8 21.5 0.001811 0.001213 0.000408 khramov 2025-06-18 14:45:32 +TT 560 102.3 04.1 104.5 19.9 3.74E-08 002.1 070.2 18.2 0.001012 0.000460 0.000251 khramov 2025-06-18 16:44:54 From 5ef61679b416a25524702f47b153d6e0900b16fa Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 5 Apr 2026 11:15:19 +1200 Subject: [PATCH 15/25] feat: add merge-on-upload option for DIR page files Users can now merge multiple DIR files (.dir, .pmm, .csv, .xlsx) into a single collection when uploading. A checkbox in the upload modal enables merge mode, with an optional custom name for the merged collection. This addresses the need to combine interpretation results split across multiple files from different field seasons or labs. - Add mergeUtils.ts with merge/name-generation utilities - Extend filesToData thunk to carry mergeMode through to reducer - Handle merge in parsedData fulfilled handler and confirmPendingUpload - Add checkbox + name field UI in UploadModal for DIR page - Add i18n keys (en/ru) for merge UI labels - Add test data files for merge testing (3 PMM, 1 CSV, 1 DIR) --- public/locales/en/translation.json | 5 +- public/locales/ru/translation.json | 5 +- .../Modal/UploadModal/UploadModal.module.scss | 16 +++++++ .../Common/Modal/UploadModal/UploadModal.tsx | 47 +++++++++++++++++-- src/locales/en/translation.json | 5 +- src/locales/ru/translation.json | 5 +- src/services/axios/filesAndData.ts | 10 +++- src/services/reducers/parsedData.ts | 20 ++++++-- src/utils/files/mergeUtils.ts | 40 ++++++++++++++++ test-data/field_batch.dir | 6 +++ test-data/lab_results.csv | 8 ++++ test-data/season1_north.pmm | 11 +++++ test-data/season1_south.pmm | 9 ++++ test-data/season2_extra.pmm | 8 ++++ 14 files changed, 180 insertions(+), 15 deletions(-) create mode 100644 src/utils/files/mergeUtils.ts create mode 100644 test-data/field_batch.dir create mode 100644 test-data/lab_results.csv create mode 100644 test-data/season1_north.pmm create mode 100644 test-data/season1_south.pmm create mode 100644 test-data/season2_extra.pmm diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 434443c..c16feee 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -156,7 +156,10 @@ "import": "Import files", "useExample": "Or use an example", "useDnD": "Or use drag and drop", - "close": "Close" + "close": "Close", + "mergeFiles": "Merge all files into one collection", + "mergedCollectionName": "Collection name", + "mergedCollectionPlaceholder": "Auto-generated from file names" }, "pcaPage": { "metadataModal": { diff --git a/public/locales/ru/translation.json b/public/locales/ru/translation.json index eebd972..fd8034a 100644 --- a/public/locales/ru/translation.json +++ b/public/locales/ru/translation.json @@ -156,7 +156,10 @@ "import": "Загрузите файлы", "useExample": "Или воспользуйтесь примером", "useDnD": "Или перетащите файлы поверх приложения", - "close": "Закрыть" + "close": "Закрыть", + "mergeFiles": "Объединить все файлы в одну коллекцию", + "mergedCollectionName": "Имя коллекции", + "mergedCollectionPlaceholder": "Сгенерируется из имён файлов" }, "pcaPage": { "metadataModal": { diff --git a/src/components/Common/Modal/UploadModal/UploadModal.module.scss b/src/components/Common/Modal/UploadModal/UploadModal.module.scss index f36a21e..2dfd353 100644 --- a/src/components/Common/Modal/UploadModal/UploadModal.module.scss +++ b/src/components/Common/Modal/UploadModal/UploadModal.module.scss @@ -33,6 +33,22 @@ } } +.mergeOptions { + display: flex; + flex-direction: row; + align-items: center; + gap: 16px; + width: 660px; +} + +@media screen and (max-width: 1140px) { + .mergeOptions { + width: fit-content; + flex-direction: column; + align-items: flex-start; + } +} + .dropContainer { display: flex; flex-direction: column; diff --git a/src/components/Common/Modal/UploadModal/UploadModal.tsx b/src/components/Common/Modal/UploadModal/UploadModal.tsx index 6cc8202..7af0cf9 100644 --- a/src/components/Common/Modal/UploadModal/UploadModal.tsx +++ b/src/components/Common/Modal/UploadModal/UploadModal.tsx @@ -1,5 +1,5 @@ -import { Typography, Button } from '@mui/material'; -import React, { useCallback } from 'react'; +import { Typography, Button, Checkbox, FormControlLabel, TextField } from '@mui/material'; +import React, { useCallback, useState } from 'react'; import { useDropzone } from 'react-dropzone'; import { useAppDispatch } from '../../../../services/store/hooks'; import { textColor } from '../../../../utils/ThemeConstants'; @@ -12,6 +12,7 @@ import exampleDIR from '../../../../assets/examples/exampleDIR.pmm'; import { useMediaQuery } from 'react-responsive'; import { useTranslation } from 'react-i18next'; import { filesToData } from '../../../../services/axios/filesAndData'; +import { generateMergedName } from '../../../../utils/files/mergeUtils'; type Props = { page: 'pca' | 'dir'; @@ -19,21 +20,33 @@ type Props = { const UploadModal = ({ page }: Props) => { const theme = useTheme(); - const { t, i18n } = useTranslation('translation'); + const { t } = useTranslation('translation'); const dispatch = useAppDispatch(); const widthLessThan720 = useMediaQuery({ maxWidth: 719 }); + const [mergeEnabled, setMergeEnabled] = useState(false); + const [mergeName, setMergeName] = useState(''); + const handleFileUpload = (event: any, files?: Array) => { const acceptedFiles: File[] = files ? files : Array.from(event.currentTarget.files); + + const mergeMode = + mergeEnabled && page === 'dir' + ? { + enabled: true as const, + name: mergeName || generateMergedName(acceptedFiles.map((f) => f.name)), + } + : undefined; + if (page === 'pca') dispatch(filesToData({ files: acceptedFiles, format: 'pmd' })); - if (page === 'dir') dispatch(filesToData({ files: acceptedFiles, format: 'dir' })); + if (page === 'dir') dispatch(filesToData({ files: acceptedFiles, format: 'dir', mergeMode })); }; const onDrop = useCallback( (acceptedFiles) => { handleFileUpload(undefined, acceptedFiles); }, - [page], + [page, mergeEnabled, mergeName], ); const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, noClick: true }); @@ -77,6 +90,30 @@ const UploadModal = ({ page }: Props) => { {t('importModal.useExample')}
+ {page === 'dir' && ( +
+ setMergeEnabled(e.target.checked)} + size="small" + /> + } + label={t('importModal.mergeFiles')} + /> + {mergeEnabled && ( + setMergeName(e.target.value)} + placeholder={t('importModal.mergedCollectionPlaceholder')} + sx={{ minWidth: 280 }} + /> + )} +
+ )} {!widthLessThan720 && (
getDirectionalData(file, format)), @@ -61,7 +67,7 @@ export const filesToData = createAsyncThunk( } } - return { format, data, validationIssues }; + return { format, data, validationIssues, mergeMode }; } catch (error: any) { return rejectWithValue(error); } diff --git a/src/services/reducers/parsedData.ts b/src/services/reducers/parsedData.ts index c6458e6..f325593 100644 --- a/src/services/reducers/parsedData.ts +++ b/src/services/reducers/parsedData.ts @@ -2,11 +2,13 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { IPmdData, IDirData, ISitesData } from '../../utils/GlobalTypes'; import { filesToData, sitesFileToLatLon } from '../axios/filesAndData'; import { FileValidationIssue } from '../../utils/files/validation'; +import { createMergedDirData } from '../../utils/files/mergeUtils'; interface PendingUpload { format: string; data: any[]; validationIssues: FileValidationIssue[]; + mergeMode?: { enabled: true; name: string }; } interface IInitialState { @@ -55,7 +57,7 @@ const storePMDData = (state: IInitialState, format: string, data: any[]) => { }; const storeDIRData = (state: IInitialState, format: string, data: any[]) => { - if (format === 'dir' || format === 'pmm') { + if (format === 'dir' || format === 'pmm' || format === 'merged') { state.dirStatData.push(...(data as IDirData[])); localStorage.setItem('dirStatData', JSON.stringify(state.dirStatData)); @@ -142,7 +144,13 @@ const parsedDataSlice = createSlice({ }, confirmPendingUpload(state) { if (state.pendingUpload) { - storeData(state, state.pendingUpload.format, state.pendingUpload.data); + const { format, data, mergeMode } = state.pendingUpload; + if (mergeMode?.enabled && (format === 'dir' || format === 'pmm')) { + const merged = createMergedDirData(data as IDirData[], mergeMode.name); + storeData(state, 'merged', [merged]); + } else { + storeData(state, format, data); + } state.pendingUpload = null; } }, @@ -156,11 +164,15 @@ const parsedDataSlice = createSlice({ state.errorInfo = null; }); builder.addCase(filesToData.fulfilled, (state, action) => { - const { format, data, validationIssues } = action.payload; + const { format, data, validationIssues, mergeMode } = action.payload; if (validationIssues.length > 0) { // Store as pending — user must confirm via modal - state.pendingUpload = { format, data, validationIssues }; + state.pendingUpload = { format, data, validationIssues, mergeMode }; + } else if (mergeMode?.enabled && (format === 'dir' || format === 'pmm')) { + // Merge all parsed files into a single IDirData entry + const merged = createMergedDirData(data as IDirData[], mergeMode.name); + storeData(state, 'merged', [merged]); } else { // No issues — store directly storeData(state, format, data); diff --git a/src/utils/files/mergeUtils.ts b/src/utils/files/mergeUtils.ts new file mode 100644 index 0000000..ce5bd7b --- /dev/null +++ b/src/utils/files/mergeUtils.ts @@ -0,0 +1,40 @@ +import { IDirData } from '../GlobalTypes'; + +type DirInterpretation = IDirData['interpretations'][number]; + +/** + * Merge interpretations from multiple IDirData sources into a single array, + * re-indexing IDs sequentially starting from 1. + */ +export const mergeInterpretations = (sources: IDirData[]): IDirData['interpretations'] => { + let nextId = 1; + return sources.flatMap((source) => + source.interpretations.map( + (interp): DirInterpretation => ({ + ...interp, + id: nextId++, + }), + ), + ); +}; + +/** + * Create a single merged IDirData from multiple sources. + */ +export const createMergedDirData = (sources: IDirData[], name: string): IDirData => ({ + name, + interpretations: mergeInterpretations(sources), + format: 'merged', + created: new Date().toISOString(), +}); + +/** + * Generate a default name for a merged collection from source file names. + * Takes the first two file names joined with " + ", adding "..." if there are more. + */ +export const generateMergedName = (fileNames: string[]): string => { + if (fileNames.length === 0) return 'merged'; + if (fileNames.length === 1) return fileNames[0]; + const base = fileNames.slice(0, 2).join(' + '); + return fileNames.length > 2 ? `${base} + ...` : base; +}; diff --git a/test-data/field_batch.dir b/test-data/field_batch.dir new file mode 100644 index 0000000..2eeafcb --- /dev/null +++ b/test-data/field_batch.dir @@ -0,0 +1,6 @@ +FB-01 DirOPCAT300-T580 8 13.7 51.5 11.2 47.9 4.9 118.3 normal +FB-02 DirOPCAT350-T620 7 20.4 54.3 17.8 50.8 5.5 108.7 normal +FB-03 DirOPCAT400-T660 9 6.1 48.9 3.5 45.3 4.2 142.1 normal +FB-04 DirOPCAT250-T550 6 190.8 -49.2 188.4 -45.8 7.3 85.6 reversed +FB-05 DirOPCAT300-T620 8 187.3 -52.4 184.9 -49.0 4.7 131.5 reversed +FB-06 DirOPCAT350-T580 7 17.1 53.0 14.5 49.5 5.8 112.4 normal diff --git a/test-data/lab_results.csv b/test-data/lab_results.csv new file mode 100644 index 0000000..bd0741c --- /dev/null +++ b/test-data/lab_results.csv @@ -0,0 +1,8 @@ +id,Code,StepRange,N,Dgeo,Igeo,Kgeo,MADgeo,Dstrat,Istrat,Kstrat,MADstrat,Comment +LAB-01,DirOPCA,T300-T580,8,16.4,53.2,128.7,4.6,14.0,49.7,123.5,4.8,lab measurement +LAB-02,DirOPCA,T350-T620,7,9.8,50.1,102.3,6.0,7.4,46.6,97.8,6.2,lab measurement +LAB-03,DirOPCA,T400-T660,9,21.5,54.8,151.2,4.0,19.1,51.3,146.0,4.2,lab measurement +LAB-04,DirOPCA,T300-T580,6,189.3,-47.9,88.5,7.1,186.9,-44.5,84.2,7.4,reversed polarity +LAB-05,DirOPCA,T350-T620,8,194.7,-51.6,135.8,4.5,192.3,-48.2,130.6,4.7,reversed polarity +LAB-06,DirOPCA,T250-T550,7,3.2,46.7,76.9,6.9,0.8,43.2,72.5,7.2,normal weak +LAB-07,DirOPCA,T400-T680,10,11.6,49.4,162.4,3.6,9.2,45.9,157.1,3.7,normal strong diff --git a/test-data/season1_north.pmm b/test-data/season1_north.pmm new file mode 100644 index 0000000..e4a1632 --- /dev/null +++ b/test-data/season1_north.pmm @@ -0,0 +1,11 @@ +"" +"season1_north","","2025-06-15" +ID, CODE, STEPRANGE, N, Dg, Ig, kg, a95g, Ds, Is, ks, a95s, comment, +S1N-01, DirOPCA, T300-T580, 8, 15.3, 52.1, 120.5, 4.8, 12.1, 48.7, 115.2, 5.0, "normal" +S1N-02, DirOPCA, T350-T620, 7, 8.7, 49.8, 95.3, 6.2, 6.4, 46.3, 90.1, 6.5, "normal" +S1N-03, DirOPCA, T400-T660, 9, 22.1, 55.4, 145.7, 4.1, 19.5, 51.9, 140.3, 4.3, "normal" +S1N-04, DirOPCA, T250-T550, 6, 355.2, 47.6, 78.4, 7.6, 352.8, 44.1, 73.9, 7.9, "normal" +S1N-05, DirOPCA, T300-T620, 8, 10.5, 51.3, 112.8, 5.0, 8.2, 47.8, 108.4, 5.2, "normal" +S1N-06, DirOPCA, T350-T580, 7, 18.9, 53.7, 132.1, 5.3, 16.3, 50.2, 127.5, 5.5, "normal" +S1N-07, DirOPCA, T400-T620, 6, 5.1, 48.2, 88.6, 7.1, 2.8, 44.7, 84.2, 7.4, "normal" +S1N-08, DirOPCA, T300-T660, 10, 12.8, 50.5, 158.3, 3.7, 10.4, 47.0, 153.1, 3.8, "normal" diff --git a/test-data/season1_south.pmm b/test-data/season1_south.pmm new file mode 100644 index 0000000..081bbf2 --- /dev/null +++ b/test-data/season1_south.pmm @@ -0,0 +1,9 @@ +"" +"season1_south","","2025-06-20" +ID, CODE, STEPRANGE, N, Dg, Ig, kg, a95g, Ds, Is, ks, a95s, comment, +S1S-01, DirOPCA, T300-T580, 7, 192.4, -48.7, 105.2, 5.9, 190.1, -45.3, 100.8, 6.1, "reversed" +S1S-02, DirOPCA, T350-T620, 8, 188.1, -51.2, 130.7, 4.6, 185.7, -47.8, 125.4, 4.8, "reversed" +S1S-03, DirOPCA, T400-T660, 6, 195.6, -46.3, 92.8, 7.0, 193.2, -42.9, 88.5, 7.3, "reversed" +S1S-04, DirOPCA, T250-T550, 9, 183.9, -53.1, 148.5, 4.0, 181.5, -49.7, 143.2, 4.2, "reversed" +S1S-05, DirOPCA, T300-T620, 7, 199.2, -44.8, 85.3, 6.6, 196.8, -41.4, 81.0, 6.9, "reversed" +S1S-06, DirOPCA, T350-T580, 8, 186.7, -50.5, 118.9, 4.8, 184.3, -47.1, 114.5, 5.0, "reversed" diff --git a/test-data/season2_extra.pmm b/test-data/season2_extra.pmm new file mode 100644 index 0000000..16bbea5 --- /dev/null +++ b/test-data/season2_extra.pmm @@ -0,0 +1,8 @@ +"" +"season2_extra","","2025-09-10" +ID, CODE, STEPRANGE, N, Dg, Ig, kg, a95g, Ds, Is, ks, a95s, comment, +S2E-01, DirOPCA, M10-M80, 8, 14.2, 51.8, 125.3, 4.7, 11.8, 48.3, 120.1, 4.9, "AF normal" +S2E-02, DirOPCA, M15-M90, 7, 7.5, 48.6, 98.7, 6.1, 5.1, 45.1, 94.3, 6.3, "AF normal" +S2E-03, DirOPCA, M10-M70, 6, 191.3, -49.5, 110.4, 6.4, 188.9, -46.1, 105.9, 6.7, "AF reversed" +S2E-04, DirOPCA, M20-M80, 9, 185.8, -52.7, 142.6, 4.1, 183.4, -49.3, 137.8, 4.3, "AF reversed" +S2E-05, DirOPCA, M10-M90, 8, 19.7, 54.1, 136.9, 4.5, 17.3, 50.6, 131.7, 4.7, "AF normal" From d164e5253c5584bfb9410502c2255274e84c366f Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 5 Apr 2026 11:49:51 +1200 Subject: [PATCH 16/25] fix: disable DataGrid virtualization for small DIR datasets The merge-on-upload feature produced correct data (15 interpretations) but MUI DataGrid row virtualization only rendered 14 rows in the DOM when the container height was insufficient, making the last row invisible without scrolling. Disable virtualization for <100 rows so all rows are always present in the DOM. Also adds unit tests for the merge pipeline (parsePMM + parseCSV_DIR + mergeInterpretations) confirming the data layer produces correct results. --- .../InputDataTable/DataTableDIR.tsx | 1 + src/utils/files/__tests__/mergeDir.test.ts | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 src/utils/files/__tests__/mergeDir.test.ts diff --git a/src/components/AppLogic/DataTablesDIR/InputDataTable/DataTableDIR.tsx b/src/components/AppLogic/DataTablesDIR/InputDataTable/DataTableDIR.tsx index 5727a6f..ada3615 100644 --- a/src/components/AppLogic/DataTablesDIR/InputDataTable/DataTableDIR.tsx +++ b/src/components/AppLogic/DataTablesDIR/InputDataTable/DataTableDIR.tsx @@ -287,6 +287,7 @@ const DataTableDIR: FC = ({ data }) => { }} density={'compact'} hideFooter={rows.length < 100} + disableVirtualization={rows.length < 100} getRowClassName={(params) => hiddenDirectionsIDs.includes(params.row.id) ? styles.hiddenRow : '' } diff --git a/src/utils/files/__tests__/mergeDir.test.ts b/src/utils/files/__tests__/mergeDir.test.ts new file mode 100644 index 0000000..5249cc1 --- /dev/null +++ b/src/utils/files/__tests__/mergeDir.test.ts @@ -0,0 +1,50 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import parsePMM from '../parsers/parserPMM'; +import parseCSV_DIR from '../parsers/parserCSV_DIR'; +import { createMergedDirData, mergeInterpretations } from '../mergeUtils'; + +const testDataDir = path.resolve(__dirname, '../../../../test-data'); + +describe('DIR file merge', () => { + const pmmData = fs.readFileSync(path.join(testDataDir, 'season1_north.pmm'), 'utf-8'); + const csvData = fs.readFileSync(path.join(testDataDir, 'lab_results.csv'), 'utf-8'); + + it('parsePMM should produce 8 interpretations from season1_north.pmm', () => { + const result = parsePMM(pmmData, 'season1_north.pmm'); + expect(result.interpretations).toHaveLength(8); + expect(result.interpretations[0].label).toBe('S1N-01'); + expect(result.interpretations[7].label).toBe('S1N-08'); + }); + + it('parseCSV_DIR should produce 7 interpretations from lab_results.csv', () => { + const result = parseCSV_DIR(csvData, 'lab_results.csv'); + expect(result.interpretations).toHaveLength(7); + expect(result.interpretations[0].label).toBe('LAB-01'); + expect(result.interpretations[6].label).toBe('LAB-07'); + }); + + it('mergeInterpretations should produce 15 items from PMM(8) + CSV(7)', () => { + const pmm = parsePMM(pmmData, 'season1_north.pmm'); + const csv = parseCSV_DIR(csvData, 'lab_results.csv'); + const merged = mergeInterpretations([pmm, csv]); + expect(merged).toHaveLength(15); + expect(merged[0].label).toBe('S1N-01'); + expect(merged[7].label).toBe('S1N-08'); + expect(merged[8].label).toBe('LAB-01'); + expect(merged[14].label).toBe('LAB-07'); + }); + + it('createMergedDirData should produce correct merged IDirData', () => { + const pmm = parsePMM(pmmData, 'season1_north.pmm'); + const csv = parseCSV_DIR(csvData, 'lab_results.csv'); + const merged = createMergedDirData([pmm, csv], 'test-merge'); + expect(merged.name).toBe('test-merge'); + expect(merged.format).toBe('merged'); + expect(merged.interpretations).toHaveLength(15); + // IDs should be re-indexed 1-15 + expect(merged.interpretations.map((i) => i.id)).toEqual( + Array.from({ length: 15 }, (_, i) => i + 1), + ); + }); +}); From 4fadfc1589f5075a06f0a5f1d93bde7ee96b0103 Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 5 Apr 2026 11:51:25 +1200 Subject: [PATCH 17/25] fix: reset merge state when upload modal reopens The UploadModal is inside a keepMounted MUI Modal, so its useState values for mergeEnabled and mergeName persisted across close/open cycles. When data was cleared and the modal reopened, the stale collection name caused confusion. Add an open prop and useEffect that resets merge checkbox and name field whenever the modal transitions to open. --- .../Common/Modal/UploadModal/UploadModal.tsx | 12 ++++++++++-- src/pages/DIRPage/DIRPage.tsx | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/Common/Modal/UploadModal/UploadModal.tsx b/src/components/Common/Modal/UploadModal/UploadModal.tsx index 7af0cf9..7a1037a 100644 --- a/src/components/Common/Modal/UploadModal/UploadModal.tsx +++ b/src/components/Common/Modal/UploadModal/UploadModal.tsx @@ -1,5 +1,5 @@ import { Typography, Button, Checkbox, FormControlLabel, TextField } from '@mui/material'; -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import { useDropzone } from 'react-dropzone'; import { useAppDispatch } from '../../../../services/store/hooks'; import { textColor } from '../../../../utils/ThemeConstants'; @@ -16,9 +16,10 @@ import { generateMergedName } from '../../../../utils/files/mergeUtils'; type Props = { page: 'pca' | 'dir'; + open?: boolean; }; -const UploadModal = ({ page }: Props) => { +const UploadModal = ({ page, open }: Props) => { const theme = useTheme(); const { t } = useTranslation('translation'); const dispatch = useAppDispatch(); @@ -27,6 +28,13 @@ const UploadModal = ({ page }: Props) => { const [mergeEnabled, setMergeEnabled] = useState(false); const [mergeName, setMergeName] = useState(''); + useEffect(() => { + if (open) { + setMergeEnabled(false); + setMergeName(''); + } + }, [open]); + const handleFileUpload = (event: any, files?: Array) => { const acceptedFiles: File[] = files ? files : Array.from(event.currentTarget.files); diff --git a/src/pages/DIRPage/DIRPage.tsx b/src/pages/DIRPage/DIRPage.tsx index fa09094..fbf4291 100644 --- a/src/pages/DIRPage/DIRPage.tsx +++ b/src/pages/DIRPage/DIRPage.tsx @@ -71,7 +71,7 @@ const DIRPage: FC = ({}) => { size={{ width: '60vw', height: widthLessThan720 ? 'fit-content' : '60vh' }} showBottomClose > - + From d880eb9fb42b17c0eb57a53e0e9254a08a393271 Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 5 Apr 2026 11:53:18 +1200 Subject: [PATCH 18/25] chore: add verify-fixes-report-1.md for evaluation-report-1 fix checks --- test-data/v2.6.2/verify-fixes-report-1.md | 9 +++++++++ test-data/v2.6.2/verify-fixes.md | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 test-data/v2.6.2/verify-fixes-report-1.md create mode 100644 test-data/v2.6.2/verify-fixes.md diff --git a/test-data/v2.6.2/verify-fixes-report-1.md b/test-data/v2.6.2/verify-fixes-report-1.md new file mode 100644 index 0000000..6f0fc12 --- /dev/null +++ b/test-data/v2.6.2/verify-fixes-report-1.md @@ -0,0 +1,9 @@ +Check that all fixes from evaluation-report-1.md are resolved: + +1. **All rows visible after mixed-format merge**: Navigate to /app/dir. Check "Merge all files into one collection". Upload season1_north.pmm + lab_results.csv. Count rows in the data table — should be exactly 15 (8 + 7). The last row should be LAB-07 with Dgeo=11.6, Igeo=49.4. All 15 rows must be present in the DOM (no rows hidden by virtualization). + +2. **Collection name field resets on modal reopen**: Navigate to /app/dir. Check "Merge all files into one collection", type "Combined Season 1" in the name field, upload files. Click delete/clear all data (the upload modal should reopen). Verify the merge checkbox is unchecked and the collection name field is empty — not showing "Combined Season 1". Re-enable merge, upload new files — the dropdown should show an auto-generated name, not the previously typed name. + +3. **Regression: merge PMM-only still works**: Upload season1_north.pmm + season1_south.pmm with merge enabled. Should produce 14 rows (8 + 6) in a single collection. Verify all rows are present. + +4. **Regression: non-merge upload still works**: Upload season1_north.pmm without merge enabled. Should create a single dropdown entry with 8 interpretations. No merge UI artifacts. diff --git a/test-data/v2.6.2/verify-fixes.md b/test-data/v2.6.2/verify-fixes.md new file mode 100644 index 0000000..f39dc80 --- /dev/null +++ b/test-data/v2.6.2/verify-fixes.md @@ -0,0 +1,21 @@ +Check that the merge-on-upload feature for DIR page files works correctly: + +1. **Merge checkbox appears on DIR page only**: Navigate to `/app/pca` — the upload modal should NOT show any merge checkbox. Navigate to `/app/dir` — the upload modal should show a "Merge all files into one collection" checkbox below the import button row. + +2. **Merge multiple PMM files**: On the DIR page, check the "Merge all files into one collection" checkbox. Leave the name field empty (auto-generated name will be used). Select and upload `test-data/season1_north.pmm` and `test-data/season1_south.pmm` together (Ctrl+click). The file selector dropdown should show ONE entry (not two) with a name like "season1_north.pmm + season1_south.pmm". The data table should contain 14 interpretations total (8 from north + 6 from south) with sequential IDs 1-14. + +3. **Merge with custom name**: Clear data and reload. Check the merge checkbox and type "Combined Season 1" in the collection name field. Upload the same two PMM files. The dropdown should show "Combined Season 1" as the collection name. + +4. **Upload without merge (default behavior preserved)**: Uncheck the merge checkbox. Upload `test-data/season1_north.pmm` and `test-data/season1_south.pmm`. The dropdown should show TWO separate entries, one for each file. Each has its own interpretations. + +5. **Merge mixed formats**: Check the merge checkbox. Upload `test-data/season1_north.pmm` and `test-data/lab_results.csv` together. Should produce ONE merged entry with 15 interpretations (8 + 7). Verify all data is visible in the table and on the stereonet graph. + +6. **Merge with validation issues**: Check the merge checkbox. Upload `test-data/field_batch.dir` alongside another file. If a validation modal appears, clicking "Load Anyway" should still produce a single merged collection. + +7. **Single file with merge enabled**: Check the merge checkbox and upload just one file (`test-data/season2_extra.pmm`). Should still work — creates one entry with 5 interpretations (merge of 1 file is effectively a no-op). + +8. **Drag-and-drop does not merge**: With the merge checkbox checked, drag and drop multiple files onto the app. Files should be added as separate entries (drag-and-drop bypasses the modal merge option). + +9. **Language switching**: Switch to Russian language. The checkbox should read "Объединить все файлы в одну коллекцию" and the name field label should be "Имя коллекции". Switch back to English and verify English labels. + +10. **localStorage persistence**: Merge and upload files. Refresh the page. The merged collection should still be present in the dropdown with all its interpretations. From 71b82a903f5f689d67d2e65ab1bc6bdd8cf30829 Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 5 Apr 2026 12:14:06 +1200 Subject: [PATCH 19/25] fix: improve dark theme contrast in upload modal and fix dropdown overflow - Add explicit text color to merge checkbox label and collection name field so they are visible on the dark (#101010) modal background - Add text-overflow ellipsis to file selector dropdown so long merged names truncate instead of overflowing the container --- .../DropdownSelectWithButtons.tsx | 7 ++ .../Common/Modal/UploadModal/UploadModal.tsx | 10 ++- test-data/v2.6.2/evaluation-report-1.md | 69 +++++++++++++++++++ test-data/v2.6.2/evaluation-report-2.md | 50 ++++++++++++++ 4 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 test-data/v2.6.2/evaluation-report-1.md create mode 100644 test-data/v2.6.2/evaluation-report-2.md diff --git a/src/components/Common/DropdownSelect/DropdownSelectWithButtons.tsx b/src/components/Common/DropdownSelect/DropdownSelectWithButtons.tsx index e326464..b012071 100644 --- a/src/components/Common/DropdownSelect/DropdownSelectWithButtons.tsx +++ b/src/components/Common/DropdownSelect/DropdownSelectWithButtons.tsx @@ -145,15 +145,22 @@ const DropdownSelectWithButtons: FC = ({ sx={{ margin: 0, border: 0, + maxWidth: maxWidth || 'none', '::before': { border: 0, }, + '& .MuiSelect-select': { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + }, '& .MuiListItem-root': { display: 'none', }, '& .MuiListItemText-root': { textOverflow: 'ellipsis', overflow: 'hidden', + whiteSpace: 'nowrap', margin: 0, }, }} diff --git a/src/components/Common/Modal/UploadModal/UploadModal.tsx b/src/components/Common/Modal/UploadModal/UploadModal.tsx index 7a1037a..3f70ebe 100644 --- a/src/components/Common/Modal/UploadModal/UploadModal.tsx +++ b/src/components/Common/Modal/UploadModal/UploadModal.tsx @@ -109,6 +109,7 @@ const UploadModal = ({ page, open }: Props) => { /> } label={t('importModal.mergeFiles')} + sx={{ color: textColor(theme.palette.mode) }} /> {mergeEnabled && ( { value={mergeName} onChange={(e) => setMergeName(e.target.value)} placeholder={t('importModal.mergedCollectionPlaceholder')} - sx={{ minWidth: 280 }} + sx={{ + minWidth: 280, + '& .MuiInputBase-input': { color: textColor(theme.palette.mode) }, + '& .MuiInputLabel-root': { color: textColor(theme.palette.mode) }, + '& .MuiOutlinedInput-root': { + '& fieldset': { borderColor: textColor(theme.palette.mode) }, + }, + }} /> )}
diff --git a/test-data/v2.6.2/evaluation-report-1.md b/test-data/v2.6.2/evaluation-report-1.md new file mode 100644 index 0000000..ae473b2 --- /dev/null +++ b/test-data/v2.6.2/evaluation-report-1.md @@ -0,0 +1,69 @@ +# Evaluation Report — PMTools v2.6.2 + +Date: 2026-04-05 +Scope: Merge-on-upload feature for DIR page files (verify-fixes.md, 10 test cases) + +## Bug Report + +### Bug 1: Last row of second file dropped during merge (mixed formats) +- **What**: When merging season1_north.pmm (8 rows) + lab_results.csv (7 rows), only 14 interpretations load instead of 15. The last row of the CSV (LAB-07) is silently dropped. +- **Steps**: + 1. Navigate to /app/dir + 2. Check "Merge all files into one collection" + 3. Click Import and select season1_north.pmm + lab_results.csv + 4. Count rows in the data table +- **Expected vs Actual**: Expected 15 interpretations (8 + 7). Actual: 14 (8 + 6). LAB-07 is missing. Uploading lab_results.csv alone without merge loads all 7 rows correctly. +- **Severity**: major + +### Bug 2: Collection name field persists across clear/reload cycles +- **What**: The custom collection name text (e.g., "Combined Season 1") persists in the input field even after data is cleared and new files are uploaded. This causes subsequent merged uploads to use the stale name instead of auto-generating from file names. +- **Steps**: + 1. Check merge, type "Combined Season 1", upload files + 2. Click delete/clear all data + 3. Upload new files with merge enabled (without clearing the name field) + 4. The dropdown shows "Combined Season 1" instead of auto-generated name +- **Expected vs Actual**: Expected the name field to clear when data is deleted, or at minimum on each new upload session. Actual: the old name persists indefinitely. +- **Severity**: minor + +## Data Consistency Issues + +- No mismatches between table data and stereonet graph positions were observed during merge testing. +- All declination/inclination values in the table correspond correctly to dot positions on the stereonet. +- Sequential IDs (1-14) are correctly assigned after merge. + +## Shortcut Issues + +No shortcut issues observed (shortcuts were not the focus of this evaluation). + +## Test Results Summary + +| Test | Result | Notes | +|------|--------|-------| +| 1. Merge checkbox on DIR only | PASS | PCA has no modal/checkbox; DIR shows checkbox correctly | +| 2. Merge PMM files (auto name) | PASS | 14 rows, one dropdown entry "season1_north.pmm + season1_south.pmm" | +| 3. Merge with custom name | PASS | Dropdown shows "Combined Season 1" | +| 4. No merge (default behavior) | PASS | Two separate dropdown entries, each with own data | +| 5. Merge mixed formats | BUG | 14/15 rows — last CSV row (LAB-07) dropped | +| 6. Merge with validation | N/A | field_batch.dir did not trigger validation modal | +| 7. Single file with merge | PASS | 5 interpretations loaded correctly | +| 8. Drag-and-drop bypass | SKIPPED | Not testable via Playwright MCP | +| 9. Language switching | PASS | RU: "Объединить все файлы в одну коллекцию", "Имя коллекции" — correct | +| 10. localStorage persistence | PASS | Merged collection survives page refresh | + +## Console Errors + +- 1 pre-existing DOM nesting warning: `
  • ` cannot appear as descendant of `
  • ` in the dropdown file selector. Not related to the merge feature. + +## Scores (1-5) + +| Category | Score | Notes | +|----------|-------|-------| +| Design quality | 4 | Clean checkbox + name field layout, good spacing, consistent with app style | +| Functionality | 3 | Core merge works well, but last-row-dropped bug in mixed format merge is a data integrity issue | +| Technical quality | 4 | No console errors from the merge feature itself, good localStorage persistence | +| UX | 3 | Name field persistence across sessions is confusing; merge checkbox state could be clearer | + +## Priority Fixes + +1. **Fix last-row-dropped bug in mixed-format merge** — data integrity issue where the final interpretation from the second file is silently lost during merge. Likely an off-by-one error in the merge concatenation logic. +2. **Clear collection name field when data is cleared** — the stale custom name causes confusion when uploading new files with merge enabled. diff --git a/test-data/v2.6.2/evaluation-report-2.md b/test-data/v2.6.2/evaluation-report-2.md new file mode 100644 index 0000000..d1da9cb --- /dev/null +++ b/test-data/v2.6.2/evaluation-report-2.md @@ -0,0 +1,50 @@ +# Evaluation Report — PMTools v2.6.2 + +Date: 2026-04-05 +Scope: Re-evaluation after fixes for bugs from evaluation-report-1 (verify-fixes-report-1.md, 4 test cases) + +## Bug Report + +No new bugs found. Both bugs from evaluation-report-1 have been fixed. + +### Fixed: Last row of second file dropped during merge (was major) +- **Status**: FIXED +- **Verification**: Merging season1_north.pmm (8 rows) + lab_results.csv (7 rows) now produces 15 interpretations. LAB-07 is present as the last row with Dgeo=11.6, Igeo=49.4. + +### Fixed: Collection name field persists across clear/reload cycles (was minor) +- **Status**: FIXED +- **Verification**: After clearing data, the merge checkbox resets to unchecked and the collection name field is empty. Re-enabling merge shows the placeholder "Auto-generated from file names" with no stale text. Uploading without a custom name produces the expected auto-generated name. + +## Test Results Summary + +| Test | Result | Notes | +|------|--------|-------| +| 1. Mixed-format merge (8+7=15) | PASS | All 15 rows present, LAB-07 with correct values | +| 2. Name field resets on clear | PASS | Checkbox unchecked, name field empty after clear | +| 3. Regression: PMM-only merge | PASS | 14 rows (8+6) in single collection | +| 4. Regression: non-merge upload | PASS | 8 rows, single dropdown entry, no merge artifacts | + +## Data Consistency Issues + +None found. + +## Shortcut Issues + +Not tested (out of scope for this re-evaluation). + +## Console Errors + +0 errors, 4 warnings (all are React render warnings, not related to the merge feature). + +## Scores (1-5) + +| Category | Score | Notes | +|----------|-------|-------| +| Design quality | 4 | Clean UI, merge checkbox and name field layout consistent | +| Functionality | 5 | All merge scenarios work correctly after fixes | +| Technical quality | 4 | No console errors from merge feature, clean state management | +| UX | 4 | Checkbox and name field properly reset on clear, auto-generated names work well | + +## Priority Fixes + +None — all previously reported bugs are resolved. From 329b9e87343567622eb3ecad83e77aac8bc436a7 Mon Sep 17 00:00:00 2001 From: Ivan Efremov Date: Sun, 5 Apr 2026 12:16:07 +1200 Subject: [PATCH 20/25] chore: bump version to 2.6.3 and update changelog Add changelog entries for merge-on-upload feature, MAD=0 fix, dark theme contrast fix, and dropdown overflow fix. --- package.json | 2 +- .../Common/Modal/ChangelogModal/ChangelogModal.tsx | 2 +- src/data/changelog.ts | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 2a9df9f..a3645dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pmtools_2.0", - "version": "2.6.2", + "version": "2.6.3", "private": true, "homepage": "https://pmtools.ru/", "dependencies": { diff --git a/src/components/Common/Modal/ChangelogModal/ChangelogModal.tsx b/src/components/Common/Modal/ChangelogModal/ChangelogModal.tsx index 9825574..0f63306 100644 --- a/src/components/Common/Modal/ChangelogModal/ChangelogModal.tsx +++ b/src/components/Common/Modal/ChangelogModal/ChangelogModal.tsx @@ -62,7 +62,7 @@ const ChangelogModal: FC = () => { )} {e.items && e.items.length > 0 && ( -