Repository navigation
Conversation
UNIQUE and FOREIGN KEY violations (a duplicate study title, re-using a deleted account's email) returned 500 with the raw SQLite message. They now return 409 with a readable message; the SQLite text is only included outside production. Login matched email or username with one query and took the first row, so a username equal to someone's email could shadow them. Email is now matched first, and usernames cannot contain "@". Admin settings writes validate the target userId. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
escapeCsvCell only quoted cells containing a comma, so ";" and tab exports split any value containing the delimiter into extra columns. Data exports (API exports and the collection table download) now prefix text cells starting with =, +, @, tab, or CR with "'" so a spreadsheet does not run them. A leading "-" is treated the same way unless it starts a number or a phrase, so values like "-131" export unchanged. Import templates are not affected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Positions were compared unnormalized, so "A2" passed validation over a
stored A02 and "A3"/"A03" were not flagged as duplicates; the import
then failed and rolled back. Positions are normalized and checked
with the shared occupancy rule (static wells included).
- A blank reduce_parent_quantity cell overrode a shared "false" setting.
- Quantities that are not numbers ("2,5", "3x") were silently dropped;
they are now row errors.
- Paper rows passed validation if any paper existed anywhere, then
failed on import when the named box or bag was missing.
- Headers match case-insensitively, and rows with no values (Excel's
trailing ",,,") are skipped.
- The source specimen type warning compares against the first row whose
parent resolves, not only row 0.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
parsePlateCSV split lines on raw commas, so a scanner export with quoted fields produced headers like "Position" (with quotes) that never matched the scanner configuration, and every row was dropped. It now uses the shared contract CSV parser. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- An unknown study code returned totals for the whole database. - The location filter only counted micronix and cryovial tubes; static wells and papers on sheets in boxes or bags were left out (a DBS location reported 0 of its 2,873 papers). - Location and tag filters sent their full container id lists as IN parameters inside every chunk, which could exceed SQLite's limit and made a large location take about 60 s. They are now matched with subqueries and in-memory sets (under 1 s on the production copy). - A bare created_to date (YYYY-MM-DD) compared "<=" against timestamps and left out everything created on that day, in stats, exports, and the specimen list. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Multi-study and single-study exports stored one date filter per subject, so a manifest listing a subject for two visits exported only the last one. Once any subject in a study had a date, subjects listed without a date matched nothing. Each subject now keeps a list of date filters, and subjects listed without a date match all of their dates (still honoring the global date range). Exact-date tolerance is computed in UTC, and the single-study container query chunks specimen ids to stay under SQLite's parameter limit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…limit. - Replacing qPCR targets deleted the old ones before inserting the new ones outside a transaction; two targets with the same name (two blank names both become varATS) hit the unique index and left the experiment with no targets. Duplicates are now rejected up front and the replace runs in one transaction. - Control definition PATCH accepted controlType, which could move a definition out of every blood route, and skipped the name and composition uniqueness checks that create applies. - /activity/recent took any limit, including -1 (no limit in SQLite), with several queries per row. It is now clamped to 1-50. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
check-integrity opened the file through openOperationalDatabase, which runs schema migrations, so pointing it at a backup upgraded the backup. It now opens the file read-only (DATABASE_PATH or --db=<path>). GROUP_CONCAT(<table>.id) failed on storage_container_tag, which has no id column, and the error was skipped, so orphaned tag links were never reported. Ids now come from rowid. qPCR well references are checked too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Paper moves resolved a sheet by name alone and took the first match anywhere, but default names like "Sheet-1" repeat in every box: a paper in box BB's Sheet-1 failed with "No target collection mapping", or moved into another box's sheet. Source sheets now match every sheet with that name (optionally narrowed by fromParentName), and the target must be a single sheet (narrowed by toParentName), otherwise the move reports the ambiguity. UNIQUE(name, box_id, bag_id) never fired because one of box_id / bag_id is always NULL. Migration 006 adds a unique (box_id, name) index; bags are exempt because legacy DBS bags hold many same-named sheets. Moving sheets into a box that already has a sheet with that name returns 409. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Creating a root location without an explicit parentId stored a NULL storage type (the check was parentId === null), and moving a root under a parent without sending storageTypeId kept it; both violated the location CHECK and returned 500. Moving a child to the root now requires a storage type up front. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The batch wizard created one batch per density with separate requests. If the third of four failed, the first two stayed, and a retry created them again under new suggested names. The error shown was Axios's "Request failed with status code 400" rather than the server's message. A new POST /blood-controls/batches/create-many-with-specimens creates every batch in one transaction; the wizard sends all densities in one request and shows the server's error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…hange. When a session expired, every request returned 401 but the user stayed set, so AuthGuard never redirected and each page showed "Could not load" with a Retry that kept failing. A 401 from any non-auth endpoint now clears the user, which sends them to login. The query cache also survived logout, login, and user switch, so the next user briefly saw the previous user's personal export configurations and settings. It is cleared whenever the user changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Saving settings only invalidated the settings query, so the Export, qPCR, and move pages kept old export and scanner configurations for up to 5 minutes. A save now marks every query stale. - qPCR well content-type edits updated only local state; revisiting the page within 5 minutes showed the old wells. - Clicking a qPCR well and quickly another could show the first well's container in the second well's panel. - A slow Locations collection search response could overwrite newer results or reopen the dropdown after the box was cleared. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The bulk import parser, the import preview, and the scanner plate parser split lines on raw commas, so a quoted comment like "low vol, hemolyzed" shifted every later column, and quoted scanner exports kept the quote characters. They now use the contract's RFC 4180 parser, like the derivation and control batch imports. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
new Date('2024-01-15') is UTC midnight, so collection and derivation
dates rendered a day early in the Americas. "Today" defaults used
toISOString(), which is tomorrow's date after about 5pm Pacific, and
that date was saved as the derivation or production date.
Bare YYYY-MM-DD values are now parsed as local calendar dates, and
today's date comes from the local clock.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ContainerEditModal sent `formData.comment || undefined`, so an emptied comment was dropped from the JSON body, the server changed nothing, and the modal closed as if it had saved. It now sends '', which the API stores as NULL. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rid. In Add Specimens to Collection, a barcode scanner's trailing Enter submitted the form, rows filled so far were created, half-filled rows were skipped silently, and a 400 showed only "Validation failed". Enter in a field no longer submits, partly filled positions are listed as errors, and per-row server errors are shown against their positions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…oads. - /specimens read its filters from the URL only on mount, so "Clear Filters" (and back/forward) changed the URL but not the table. Filters are now derived from the URL. - The Backspace "back to specimens" hotkey fired while the Add Container modal was open if focus was on a button. - qPCR result uploads encoded the file with String.fromCharCode(...bytes), which throws on files over a few hundred KB; encoding is now chunked. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The study summary and study delete passed every subject, specimen, and container id as bound parameters, which fails once a study passes SQLite's variable limit (about 65k here). They now select through subqueries on study_id. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Login and /auth/switch skipped bcrypt when the account did not exist, so response time revealed which emails, usernames, and user ids are real. Both now compare against a throwaway hash in that case. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Search queries built LIKE '%<text>%' from raw user input, so "_" and "%" acted as wildcards: searching "3D7_0" also matched "3D7X0.05". Search, specimen list, location, and error-log filters now escape the text with ESCAPE '\'. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ueries. Each recent specimen ran up to 3 follow-up queries and each container up to 7. The feed now uses three joined queries in total (specimens, studies, containers) and produces the same labels, which a new test pins. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Studies page kept search, sort, and type/lead filters in component state, so the "Clear Filters" command (which clears the URL) did nothing, filtered views could not be linked, and back/forward lost them. They now live in the URL (q, sort, dir, type, lead), with defaults left out; each control updates the URL in a single change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Adds resetUserContextForTesting() for the provider's once-per-load cache, and tests that switching users clears cached queries and that a 401 from a data request signs the user out. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
qpcr_run, qpcr_well_result, and qpcr_amplification_data are in the Drizzle schema and in databases created before the SQL snapshot, but initial_schema.sql never created them, so uploading qPCR results failed with "no such table" on any new install. Migration 007 creates them with IF NOT EXISTS (existing databases are unchanged), and the snapshot now includes them. Adds a route test for results upload that covers amplification rows without a target name, which found this. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The check skipped any text starting with "-" and a digit, so
"-1+cmd|'/C calc'!A0" exported unescaped. Now a leading "-" is spared
only when the whole cell is a number ("-131", "-0.5"); "-2 in SubjID"
gets the ' prefix like other formula-shaped text.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
resolveCollectionByName returned the first sheet with a name anywhere, which the "resolve by parent" decision ruled out. Sheets now resolve only through resolveSheetsByName with their box or bag, and /collections/check no longer accepts type 'sheet' (no client sent it). resolveContainerByPosition is narrowed to plates and cryovial boxes, the only types it ever matched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Collection, derivation, and production dates on the container, specimen, control batch, control definition, and blood control pages, the study stats date range, and the qPCR list still parsed YYYY-MM-DD as UTC midnight, so they showed the previous day in the Americas. They now go through parseDisplayDate. Web tests now run in America/Los_Angeles, so date tests fail on this bug wherever CI runs; the new SpecimenDetail test fails on the old code. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every URL update replaced the history entry, so Back left the page instead of undoing a filter. Sort, type, lead, and clear now push an entry; search keystrokes still replace so typing doesn't flood history. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
createBatchesWithSpecimens wrapped every failure in a ValidationError, so a UNIQUE violation or ConflictError came back 400. Those now stay 409 with the readable conflict message; other failures stay 400. The message reads "Control batch N (...): ... No control batches were created.", per CONTEXT.md, as do the wizard's own fallbacks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
5a3ceb1 fixed both without tests. Each new test fails with the stale-response guard removed: a slow reply for well A01 must not replace well B01's container, and a slow "PL-1" collection search must not replace "PL-12" results. The three qPCR well edits share one applyUpdatedWells helper instead of three copies. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…userId. 938057f and 6de04c6 shipped without tests. The integrity test plants an orphaned tag link (no id column) and qPCR well, and fails if ids come from .id instead of rowid; checkDatabaseIntegrity is exported for it. The settings test sends userId "abc", -1, and 1.5, each now a 400. .gitignore's blanket scripts/ rule also hid packages/api/src/scripts (its tracked files were force-added), so it is now an exception like the other script folders. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…E asks. Use the render helper, createMockedDomainModule, and user-event. The scanner test now checks that Enter starts no submit (no half-filled-row error), and still fails with the Enter guard removed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- parseCsv takes skipBlankRows instead of four callers pasting the same filter. - shiftCalendarDate (lib/datetime.ts) replaces the two inline Date.UTC day shifts in the stats upper bound and export tolerance window. - The "Create batch with specimens" comment is back on its route. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Control definition PATCH calls findBloodControlDefinitionByComposition with a new excludeId option instead of re-creating its query; a test checks a definition doesn't conflict with itself. - Study delete names the specimen subquery once instead of inlining it twice. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.
Follow-up to #87: the medium and low findings from the code audit, plus a schema bug found while adding tests. One commit per fix; each fix has a regression test that fails without it (the activity refactor's test pins unchanged output instead). The last 10 commits address a review of this PR; see Review follow-ups.
API
@./auth/switchtake as long to reject a missing account as a wrong password.;, tab) are quoted.=,+,@, and-unless the whole cell is a number, so-131is unchanged but-2 in SubjIDgets a'). Import templates are unaffected.reduce_parent_quantityno longer overrides the shared setting.created_toincludes the whole day (also in exports and the specimen list).%and_in search text match literally.controlType, and checks name and composition uniqueness./activity/recentis capped at 50 rows and built with three joined queries instead of up to seven per row.fromParentName/toParentName; an ambiguous target is reported./collections/checkno longer accepts typesheet(no client sent it).parentId, or moving a root under a parent, no longer trips the storage-type CHECK. Moving a location to the root without a storage type is now a 400.POST /blood-controls/batches/create-many-with-specimenscreates all of the wizard's densities in one transaction. A conflict returns 409, other failures 400.qpcr_run,qpcr_well_resultandqpcr_amplification_datawere never created byinitial_schema.sql, so results upload failed on fresh installs. They're createdIF NOT EXISTS; existing databases are unchanged.--db=<path>.rowid) and qPCR wells.Web
America/Los_Angelesso this stays caught./specimensand/studieskeep search and filters in the URL, so Clear Filters works and Back undoes a sort or filter change (search typing replaces the entry).Review follow-ups
-1+cmd|'/C calc'!A0was exported unescaped./studiessort and filter changes.userIdvalidation.CollectionSpecimenEntry.test.tsxfollows the web test README.parseCsv(..., { skipBlankRows }),shiftCalendarDate, the composition finder'sexcludeId, and a named study specimen subquery..gitignoreno longer hides new files inpackages/api/src/scripts.Deploy notes
@are rejected (none exist today).created_tois inclusive of the whole day.fromParentName/toParentName.PUT /settings/:keywith auserIdthat isn't a positive integer or null;/collections/checkwith typesheet.Test plan
bun run ci:verify🤖 Generated with Claude Code