Repository navigation
fix: fNIRS dataset pages (participants, infant ages, year, paper DOI, sidecars) - #528
Draft
bruAristimunha wants to merge 11 commits into
Draft
bruAristimunha wants to merge 11 commits into
bruAristimunha wants to merge 11 commits into
Conversation
The dataset page pages through /records when the
/datasets/{id}/participants endpoint is unavailable. Its filter allowed
suffix eeg/ieeg/emg/meg only, the same list the API endpoint has, so
fNIRS datasets (records stored with suffix "fnirs", or "nirs" when built
from the BIDS file name) never got a per-subject age-by-sex chart.
The allow-list is now a named constant kept in step with the API
endpoint (eegdash/mongodb-eegdash-server, branch
fnirs-catalog-fixes-1113). tests/tests_support/dataset_page.py imports
the docs extension modules without Sphinx, like
tests/test_dataset_page_stats.py does.
When a README has no citation-style "(YYYY)", the page fell back to
re.findall(r"\b(19|20)\d{2}\b", readme). findall returns the capture
group, so a README mentioning "2019" gave the year "20" in the
citation, byline, info table and JSON-LD. NEMAR documents carry no
dataset_created_at, so every NEMAR page whose README lacks "(YYYY)"
took this fallback.
Both fallbacks now capture all four digits of a 19xx/20xx year and live
in _year_from_readme so they can be tested on their own. The bare-year
fallback skips years inside identifiers (a DOI such as
".../j.neuroimage.2021.118000", a URL path, "sub-2019", a version
"1.2019.3"), which would otherwise become the dataset year now that the
fragment bug no longer hides them.
The card maps the trailing names of one record's storage.dep_keys to labels. It knew eeg.json, meg.json, ieeg.json and electrodes.tsv but neither nirs.json nor optodes.tsv, so fNIRS pages listed only "events · channels · coordsystem" although their records carry both (nm000175 on 13 Nov 2026). "optodes" renders next to "electrodes", "nirs.json" next to the other datatype sidecars.
participants.tsv ages were read as int(float(value)) and kept when 0 < age < 120, so every age under one year became 0 and was dropped: infant cohorts, common among the fNIRS datasets, lost all their ages. Ages recorded in months or days were read as years. An "inf" age raised OverflowError, which the reader did not catch. Ages are now kept as years with their fraction (rounded to 4 decimals); whole-year ages stay int, so datasets recorded in whole years digest exactly as before; non-finite values are skipped. BIDS defines "age" in years. Another unit is applied only when the age column's participants.json entry declares it in "Units": the value must name exactly one unit (years, months, weeks, days or hours, in common spellings), optionally with "decimal", "age", "in" or "(s)" around it. Nothing is guessed from the values or the column name, and any other Units text is read as years, as before. When a unit other than years is applied, the dataset document records it in demographics.participants_age_units, because the records' participant_tsv["age"] keeps the raw value and the page's per-subject chart must convert it the same way. The key is written only when set, so other documents are unchanged. eegdash.schemas.AGE_UNITS_PER_YEAR holds the conversion factors for both. Datasets already in the database pick this up on their next digest with a changed fingerprint (or an inject with --force).
The per-subject age-by-sex chart reads participant_tsv["age"] from the records, in the unit of the participants.tsv column. It now divides by the unit the dataset document names in demographics.participants_age_units (previous commit), so a cohort aged 100-140 days is no longer drawn as 100-140 years. Cohorts entirely under two years (infant fNIRS datasets) all fell into one "0" bar with a whole-year caption such as "range 0-1 yr". They are now drawn in months, one bar per month, with the range and mean in months. Every other cohort renders exactly as before (5-year bars, whole-year range).
…ument Stage 1 sets associated_paper_doi on the source document (NEMAR: the first DataCite related identifier with a paper relation, _PAPER_RELATIONS in 1_fetch_sources/nemar.py; OpenNeuro: associatedPaperDOI) and stage 2 copies it into manifest.json, but extract_dataset_metadata never passed it to create_dataset. Every document digested from a BIDS tree (the git sources: NEMAR, OpenNeuro, GIN) therefore carried associated_paper_doi: null, and pages showed no "Read paper" link or "Paper DOI" row unless the README held a paper badge. The BIDS digest now keeps the value when it is a DOI: bare, "doi:" or doi.org-prefixed; of a list, the first. Free text and other URLs (a publisher page embedding a DOI) give none, so no page links to a made-up DOI, and a value equal to the dataset's own DOI is dropped. The manifest-only digest (API-only sources) is unchanged. On the page, the provenance strip's "Paper" link falls back to the DOI too (after external_links.paper_url, before the README badge), matching the field card's "Read paper" action. Existing documents pick this up on their next digest with a changed fingerprint (or an inject with --force).
One entry each for the ages, paper DOI, fNIRS page and README year fixes on this branch.
Contributor
📚 Documentation Preview📦 Download Documentation Artifact
💡 To enable live previews, add a |
The README year fallback took the first 19xx/20xx number outside an
identifier, so "EEG sampled at 2048 Hz; recorded 2019." gave "2048" and
"Sampling rate 2000 Hz." gave "2000" (develop gave "20" for both). The
wrong value looked like a real year and reached the citation, byline,
info table and JSON-LD.
A 19xx/20xx number is no longer taken:
- when a unit follows it: Hz, kHz, Hertz, ms, msec, s, seconds,
milliseconds, mm, nm, nanometers, µV, volts, Ω, kΩ, ohms, dB, %,
samples, channels, trials, epochs, events, ... (also "2048-Hz");
- when it is one end of a number range, tolerance or dimension:
"2000-4000 Hz", "2018–2019", "2015 to 2017", "2015 until 2017",
"2015 through 2017", "between 2015 and 2017", "2000 ± 500 ms",
"2000 +/- 500", "1920 x 1080". The left end of a dashed range is a
whole number of two or more digits, so "Version 1.0 - 2019" and
"Dataset 3 - 2019" keep their year;
- when it is after the current year (_current_year(), so tests can pin
the clock). This third rule goes beyond the two the maintainers asked
for; it is one line to drop.
ISO dates ("2019-05-03", "2019-10") still give their year. The unit
match is case-sensitive and has no single capital letters, so an initial
after a year ("In 2019 S. Smith") is not read as a unit; count nouns
after a year ("In 2019 events ...") read as counts. No year is shown
rather than a wrong one, so a README whose only year sits in a range
now shows none.
The NEMAR fetcher took the first DataCite related identifier whose
relation was IsSupplementTo, IsDescribedBy or References, in list order.
A dataset also "References" methods, toolbox and preprint papers, and
since the previous commits put the DOI on every page as "Read paper",
such a paper would have been shown as the dataset's paper.
The digest picks the paper in this order:
1. the README "Paper DOI" badge, the paper line a deposit declares (the
same badge the page already reads);
2. else the deposit's paper-match record, code/paper_match/match.json,
when present: only a paper whose association verdict is "confirmed"
counts; any other verdict ("probable", "wrong-paper", "no-paper"),
a missing DOI or an unreadable file gives no paper, and no catalogue
paper is used either. Most NEMAR fNIRS deposits carry this record;
3. else, for OpenNeuro, the associatedPaperDOI its authors declared
(EEGManyLabs: the paper its README names).
A NEMAR dataset with neither a badge nor a confirmed record gets no
paper: its DataCite IsDescribedBy/IsSupplementTo identifiers are no
confirmation, and some name a method paper or another study. No link is
shown rather than a wrong or toolbox paper. The dataset's own DOI is
dropped in any version (".v1.0.1" against ".v1.0.0"), and a doi.org URL
loses its query string or fragment.
Stage 1 (1_fetch_sources/nemar.py) now records the first IsDescribedBy
DOI, else the first IsSupplementTo DOI, wherever they sit in the list;
References and other relations give none. The digest does not use this
value for pages (rule above); it keeps the stage-1 document free of
toolbox papers.
tests/test_dataset_page_ages.py said infant cohorts were captioned "range 0-0 yr"; a 0.75-year age rounds to "0-1 yr". The Demographics docstring typed participants_age_units as "str | None", but the key is left out, not set to None, when ages are in years.
A paper-match record can confirm a paper while its "document" is the data record the match read (document.kind "repository-record", e.g. a Mendeley Data or Zenodo record); the paper itself is then in _paperenrich.confirmed_paper or among the texts its evidence quotes. _paper_match_doi returned document.doi for every confirmed record, so such a record would have shown the data record as the paper. For a confirmed record the paper is now: 1. _paperenrich.confirmed_paper.doi, when the record carries it; 2. else, when the document is a paper, document.doi (unchanged); 3. else, when the document is the data record (kind "repository-record" or a source_kind naming a repository record), a paper an evidence quote was read from: a doi.org URL or "paper doi:" in the item's source, skipping sources that are repository records. The data record is never the paper, in any version: no candidate under its registrant prefix is taken (every Zenodo version of a Zenodo record shares 10.5281). With no such paper the record gives none. The evidence is searched only when the document is the data record, so a record whose document is the confirmed paper keeps exactly that DOI (not a preprint the quotes were read from).
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes for what EEGDash shows on fNIRS dataset pages. The gaps come from an audit (13 Nov 2026) of how each dataset-page field is filled for fNIRS datasets; its notes are internal, so the code references it gives are reproduced below, checked again on develop 8d446ef. Five gaps are fixed here; the live trace viewer is assessed at the end.
Companion API PR (participants endpoint): eegdash/mongodb-eegdash-server#14. Each PR can merge on its own.
Fixes
api/main.py:3414in the API repo); the page's/recordsfallback has the same list (docs/source/_extensions/dataset_page/data_loaders.py:503-507)fnirs(the suffix ingestion stores) andnirs(the BIDS suffix, carried by records built from file names), as a named constantscripts/ingestions/_dataset_metadata.py:66-76:int(float(age)), kept when 0 < age < 120int). Apply a unit only when the age column'sparticipants.jsonentry declares exactly one unit inUnits(years, months, weeks, days, hours); record it indemographics.participants_age_units; the page divides the per-subject ages by it and draws cohorts under two years in monthsdata_loaders.py:807:re.findall(r"\b(19|20)\d{2}\b", readme)returns the capture group_year_from_readme. A number inside an identifier (DOI, URL,sub-2019, version string), next to a unit (2048 Hz,2000 ms,2000 kΩ) or inside a number range or dimension (2000–4000 Hz,2018–2019,1920 x 1080) is never taken, nor is a year after the current oneassociated_paper_doi(1_fetch_sources/nemar.py:213), stage 2 copies it into manifest.json, butextract_dataset_metadatanever passes it on (_dataset_metadata.py:343-377), so every inject sendsnullcode/paper_match/match.jsonwhen present (only a paper it confirms); else, for OpenNeuro, the paper its authors declared. A NEMAR dataset with neither gets no paper. AReferencesidentifier (methods or toolbox paper) never becomes the paper. Only DOI values count, never the dataset's own DOI. The provenance strip's "Paper" link falls back to it, like the field card's "Read paper"data_loaders.py:368-394knows nonirs.jsonoroptodes.tsvCHANGELOG entries: 5518cda (updated in 09b9088 and 3051970). Docstring corrections: 2801e0b.
Live evidence (public API reads by the audit, 13 Nov 2026 16:24 UTC):
/datasets/nm000175/participantsand/datasets/on007738/participantsreturncount: 0, data: [], although nm000175's records carryparticipant_tsvage and sex; nm000175's records have suffixfnirs, extension.snirf, andstorage.dep_keyswith*_nirs.jsonand*_optodes.tsv; both dataset documents haveassociated_paper_doi: null.Behaviour changes to know about
intvalues). Fractional ages are kept instead of truncated. Whenparticipants.jsondeclares months, weeks, days or hours for the age column, ages are converted to years (they were read as years). Units come only fromparticipants.json, never from the values or the column name, and theUnitstext must name exactly one unit (optionally with "decimal", "age", "in" or "(s)"); any other text (e.g. "years and months", "weeks of gestation") is read as years, as before.demographics.participants_age_unitsis written only when a unit other than years was applied, so other documents gain no key. Aninfage raisedOverflowErrorout of_read_participants_demographics; it is now skipped.2048-Hz), or when it is one end of a number range, tolerance or dimension (2000-4000 Hz,20–2000 Hz,2018–2019,2015 to 2017,2015 until 2017,between 2015 and 2017,2000 ± 500 ms,1920 x 1080). The left end of a dashed range must be a whole number of two or more digits, soVersion 1.0 - 2019andDataset 3 - 2019keep their year. ISO dates (2019-05-03,2019-10) still give their year; a2011-12span gives 2011. Count nouns after a year read as counts (In 2019 events ...gives no year). Addition for the maintainers to accept or drop: besides the two rules asked for (units, ranges), a year after the current one is skipped too; dropping it is one line in_year_from_readme(the_current_year()bound). No year is shown rather than a wrong one: a README whose only year sits in a DOI or in a range like2018–2019now shows none.code/paper_match/match.json, when present, and then only a paper whose association verdict isconfirmed(its_paperenrich.confirmed_paperDOI; else its document's DOI when the document is a paper; else, when the document is the data record the match read, a paper its evidence quotes were read from; the data record itself, in any version within its repository (any DOI under its registrant prefix), is never the paper, 1e6392e): any other verdict (probable,wrong-paper,no-paper), a missing DOI or an unreadable file gives no paper, and no catalogue paper is used either (most NEMAR fNIRS deposits carry this record); (3) else, for OpenNeuro, theassociatedPaperDOIits authors declared; the maintainers' decision on paper links covered NEMAR only, so the OpenNeuro paper stays as a last resort. (EEGManyLabs datasets also keep their stage-1 paper, which is the original study the dataset replicates, named in its README, not a paper about the replication data; unchanged here, for the maintainers to keep or drop.) A NEMAR dataset (including NEMAR'son*OpenNeuro mirrors and the non-fNIRS NEMAR datasets) with neither a badge nor a confirmed record gets no paper link: its DataCiteIsDescribedBy/IsSupplementToidentifiers are no confirmation, and some name a method paper or another study. Stage 1 now records only anIsDescribedBy(thenIsSupplementTo) identifier as the NEMAR paper, never aReferencesone, so the stage-1 document carries no toolbox paper either. This applies to every source digested from a BIDS tree, not only fNIRS. Free text and other URLs (a publisher page embedding a DOI) give no DOI, a doi.org URL loses its query string or fragment, and the dataset's own DOI, in any version, is never its paper. The manifest-only digest (API-only sources: Zenodo, Figshare, OSF, SciDB, DataRN) is unchanged.ingestion_fingerprintor injected with5_inject.py --force; nothing here re-ingests.Tests
New tests, red on develop and green here:
tests/test_dataset_page_participants.py,test_dataset_page_year.py,test_dataset_page_sidecars.py,test_dataset_page_ages.py,test_dataset_page_paper_doi.py. They import the extension throughtests/tests_support/dataset_page.py, which works with or without Sphinx (same shim astest_dataset_page_stats.py).scripts/ingestions/tests/digest/test_participant_ages.py,test_paper_doi.py, andscripts/ingestions/tests/unit/test_nemar_paper_relations.py(stage-1 relations).Each file also holds controls that pass on develop too (EEG sidecars and adult charts unchanged, whole-year ages stay
int, values without a DOI give none, ...).Results, Voyager Job
fb-eegpr-tests7, branch head 2801e0b. The Python 3.12 environment installs the union of thetests.ymlandingestions-lint-and-test.ymlinstalls (uv pip install -e ".[tests]" -e "scripts/ingestions[dev]"); the Python 3.11 run installs as the ingestion workflow (-e . -e "scripts/ingestions[dev]"); both then runpython -m eegdash.testing:pre-commit run --fileson the files the last three commits touch (Python 3.12, aspre-commit.yaml): ruff lint & format, ruff format, codespell, blacken-docs and the nested-functions check pass; no file changed. (The earlier commits' files passed the same hooks infb-eegpr-tests4.)ruff check scripts/ingestions/andruff format --check scripts/ingestions/from the repo root (asingestions-lint-and-test.yml): all checks passed; 164 files already formatted.pytest tests(Python 3.12, astests.yml): 1713 passed, 13 skipped, 6 deselected, 0 failed.ingestions-lint-and-test.yml:-m "not network and not slow",--cov-fail-under=61): 1496 passed, 18 skipped, 25 deselected, 0 failed; coverage 73.28%. The e2e job's files (tests/pipeline/test_e2e.py,tests/digest/test_snapshot.py): 22 passed.test_dataset_page_stats.py/test_dataset_page_electrodes.py): 96 passed; the year and age tests in an environment without Sphinx: 72 passed.fb-eegpr-tests4: docs 29 failed / 14 passed, ingestion 46 failed / 17 passed). The tests changed in the last three commits, run on the previous head 5518cda:test_dataset_page_year.py38 failed / 22 passed,test_paper_doi.py19 failed / 22 passed,test_nemar_paper_relations.py8 failed / 7 passed, every failure an assertion on the old behaviour. The tests passing there are controls (years in prose, ISO dates, identifiers, initials after a year, version and item numbers before a dash; DOI parsing; relations that never named a paper) and badge guards that pass on 5518cda only because it ignored README badges.Follow-up 1e6392e, Voyager Job
fb-eegpr-tests10(same steps): pre-commit on the files it touches passes, no file changed;ruff check/ruff format --check scripts/ingestions/clean; package suite (Python 3.12) 1713 passed, 13 skipped, 6 deselected, 0 failed; ingestion suite (Python 3.11) 1505 passed, 18 skipped, 25 deselected, 0 failed, coverage 73.37%; e2e 22 passed. On the previous head 2801e0b its new tests fail as they should (test_paper_doi.py8 failed / 42 passed, every failure an assertion on the old value: the data record shown, or the enriched paper ignored); the passing ones are the earlier tests and the control that a confirmed paper document keeps its DOI.The docs build (
doc.yaml) is not run in the Job, since it renders every dataset page against the live API; CI runs it on this PR (on 5518cda it passed in 29m45s).CI note: the
pre-commit-codestylecheck runspre-commit run --all-filesand has failed on develop itself since 1cbd28d (#524):ruff formatreformatseegdash/io/snirf.pyandtests/unit_tests/io/test_snirf.py, which this PR does not touch (develop runs 43077518909 and 43369786224 fail the same way). Every other hook passes here, andruff formatleaves the files of this PR unchanged.Review
Independent read-only QA reviews:
Review 2 (head 5518cda): no P0 or P1; two P2s: the README year fallback took measurements such as "2048 Hz" (F1), and the NEMAR paper could be a
References(methods or toolbox) paper (F2). The maintainers asked to fix both: 09b9088 and 3051970, with the P3s F3/F4 (docstrings, 2801e0b) and F8 (doi.org query strings, versioned own DOI, 3051970).Review 3 (a first version of those fixes): RED with one P1: a NEMAR dataset with neither a README badge nor a match record still got its unconfirmed DataCite paper. Fixed in 3051970 (NEMAR: badge or confirmed record only; the "most deposits carry a record" wording; test order and labels), with its year findings fixed in 09b9088 (more units; ranges in words or as a tolerance; version and item numbers before a dash) and the future-year rule flagged above.
Review 4 (this head, 2801e0b): GREEN: no P0, P1 or P2; 4 P3 (responseModel claude-opus-5-5). Its required checks on the 329 cached NEMAR fNIRS deposits, with the code at 2801e0b: 189 have a confirmed paper-match record, 99 no-paper, 34 no record, 4 probable, 3 wrong-paper; 0 of the 140 without a confirmed record get a paper link; 189 of 189 confirmed papers are shown, each with exactly its confirmed DOI. Its P3s: wording in this description (fixed here); the EEGManyLabs paper wording, which a code comment in
_dataset_metadata.pyshares (left as reviewed); year edge cases (open, below).Follow-up 1e6392e (a confirmed record whose document is the data record it read): GREEN: no P0, P1 or P2; 7 P3, 5 of them about this PR (responseModel claude-opus-5-5). Its required checks on the live NEMAR fNIRS deposits (fetched 14 Nov 03:31 UTC, after the metadata batch that confirmed such a record): 191 confirmed records, 191 show a paper (169 from a README badge, 22 from the record), 0 of them a data record or a version of the dataset (at 2801e0b: 1, a Mendeley Data record, now its journal data descriptor); a further record held from tonight's batches (a Zenodo data record whose confirmed paper has a URL but no DOI), simulated from its staged edits, would show its Zenodo record at 2801e0b and no paper at 1e6392e; the 190 records whose document is a paper show the same DOI as at 2801e0b; 0 of 138 unconfirmed records show a paper.
Open after the follow-up (review 5's P3s): the evidence fallback reads
doi.org/<doi>andpaper doi:<doi>sources but notapi.crossref.org/works/<doi>(such a record then shows no paper, never the data record); "any version" means any DOI under the data record's registrant prefix, so a copy of the data in another repository cited in evidence not labelled as a repository record, a wrong_paperenrich.confirmed_paperDOI on a record whose document is a paper, or a data-record document with anotherkindandsource_kindwould not be caught (no live record does any of this); thepaper doi:evidence form and the enriched-paper-before-evidence order for data-record documents have no test of their own; a code comment above_PAPER_MATCH_RELPATHstill sketches the old record layout.Open, not changed here: the year fallback's unit match is case-sensitive and lists the common units only, so "2000 KΩ", "2000 Kilohertz", "2000 mW" or "2000 participants" still give 2000, and "In 2019 us ..." gives no year ("us" reads as microseconds); none of these occurs in the cached fNIRS READMEs. participants.json is read twice per digest (
_read_participants_age_unitsruns from_read_participants_demographicsand fromextract_dataset_metadata); two commit messages of the first commits name the companion branch (the descriptions of both PRs link each other); a "2011-12" span gives 2011. Pre-existing:_SIDECAR_SUFFIXESmatches withendswithand lists "eeg.json" before "ieeg.json", so a*_ieeg.jsondependency is labelled "eeg.json"; the field card buildshttps://doi.org/{paper_doi}even when a stored value is already a URL (possible only for documents not written by the BIDS digest).Not done here: live trace viewer for fNIRS (gap 6)
_get_first_eeg_record(sections.py:1746-1762) asks the API for a record with suffix eeg/ieeg/emg/meg and extension .set/.edf/.bdf/.vhdr/.fif, so fNIRS pages get no "Signal Preview". The viewer (eegdash/eegdash-viewer, main 6f7f0b9) already ships a SNIRF reader (formats/snirf.js) and routes?suffix=nirsto the BIDSnirs/folder. Loading?dataset=nm000175&sub=01&task=tapping&ext=snirf&suffix=nirs&embed=1in the deployed viewer (headless Chromium, 13 Nov) fails in two places before any SNIRF byte is read:bids-recording/nemar.js:144calls_buildBidsRelpath(params)without the suffix, so every NEMAR recording resolves tosub-01/eeg/..._eeg.<ext>("NEMAR manifest has no entry for sub-01/eeg/sub-01_task-tapping_eeg.snirf"). Passingparams.suffixfixes the path (checked by patching it in the page).https://data.nemar.org/nm000175/v1.0.0/sub-01/nirs/...URLs, whichnemar.jsrejects ("NEMAR manifest url has unrecognised shape"; it accepts onlyraw.githubusercontent.com/nemarDatasets/...andnemar.s3...amazonaws.com/.../objects/..., lines 75-76 and 187). This looks like it affects NEMAR datasets of every modality, not only fNIRS.So enabling it takes: (a) those two viewer fixes and a viewer deploy; (b) a check that
formats/snirf.jsrenders a NEMAR fNIRS recording (nm000175's file is 10.8 MB, read whole by jsfive); (c) here,fnirs/nirsand.snirfin_get_first_eeg_record's filter,?suffix=nirsfor records stored asfnirs(the viewer does not knowfnirs), and a caption that does not offer.set/.edf/.bdf/.vhdror an OpenNeuro link for NEMAR fNIRS datasets. (c) is about ten lines, but without (a) it would embed a viewer that fails, so it is left out.Not touched
No deploy, no docs build, no writes to the EEGDash API or database. Draft until reviewed.