Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
5448ddb
fix(containers): save a Container edit atomically and edit Paper subl…
m-murphy Nov 1, 2026
62b9fda
fix(web): sort lists by declared type across the whole list (#116)
m-murphy Nov 1, 2026
a7c1ad1
fix(qpcr): record result uploads, plate uploads and deletes atomicall…
m-murphy Nov 1, 2026
b4144a2
fix(web): read bulk import Collection names and barcodes from their c…
m-murphy Nov 1, 2026
7babf1c
fix(position): enforce one Grid position rule on every Container writ…
m-murphy Nov 1, 2026
16ea982
fix(collections): show unplaced Containers on plate and box pages (#103)
m-murphy Nov 1, 2026
b921513
fix(security): hash session tokens, cap error-log intake, generic pro…
m-murphy Nov 1, 2026
2605018
fix: store every Specimen collection date as YYYY-MM-DD (#104)
m-murphy Nov 1, 2026
dfd45ec
Merge branch 'fix/103-unplaced-containers' into fix/125-repair-grid-p…
m-murphy Nov 1, 2026
67481ab
fix(web): parse server timestamps as UTC (#117)
m-murphy Nov 1, 2026
e81227a
refactor(position): one Grid position rule for reads and writes
m-murphy Nov 1, 2026
53a9055
fix(collections): show delete impact and refresh the app after a dele…
m-murphy Nov 1, 2026
2d4390c
fix(scan-move): survive retries, unpadded positions and double submit…
m-murphy Nov 1, 2026
b6f6847
fix(move): report and record what moved (#110)
m-murphy Nov 1, 2026
ec367ef
fix(api): apply created_from/created_to on the studies and subjects l…
m-murphy Nov 1, 2026
e8e2168
fix(web): make dashboard stat tiles honest
m-murphy Nov 1, 2026
ddd5e6c
fix: make destructive actions admin-only per the role policy (#112)
m-murphy Nov 1, 2026
971174c
fix(position): repair legacy Grid positions and flag invalid ones (#125)
m-murphy Nov 1, 2026
66a5e9d
fix(containers): keep Container quantities within bounds on every wri…
m-murphy Nov 1, 2026
f06e63f
docs(context): record that subject merge, qPCR experiment delete and …
m-murphy Nov 1, 2026
0bfa674
fix(barcodes): refuse to guess between Containers sharing a barcode (…
m-murphy Nov 1, 2026
6fe16ba
Merge branch 'fix/104-iso-collection-dates' into integrate/review-fixes
m-murphy Nov 1, 2026
70a6742
Merge branch 'fix/121-security-hardening' into integrate/review-fixes
m-murphy Nov 1, 2026
a1c14e0
Merge branch 'fix/125-repair-grid-positions' into integrate/review-fixes
m-murphy Nov 1, 2026
ea97ab6
Merge branch 'fix/110-move-results' into integrate/review-fixes
m-murphy Nov 1, 2026
6d41aa5
Merge branch 'fix/111-scan-move-robustness' into integrate/review-fixes
m-murphy Nov 1, 2026
45ab1e6
Merge branch 'fix/108-atomic-container-edit' into integrate/review-fixes
m-murphy Nov 1, 2026
75efc51
Merge branch 'fix/109-quantity-bounds' into integrate/review-fixes
m-murphy Nov 1, 2026
c4b6ee5
Merge branch 'fix/105-ambiguous-barcodes' into integrate/review-fixes
m-murphy Nov 1, 2026
92a1f5b
Merge branch 'fix/123-atomic-qpcr-upload' into integrate/review-fixes
m-murphy Nov 1, 2026
1a448b9
Merge branch 'fix/114-import-collection-names' into integrate/review-…
m-murphy Nov 1, 2026
32c0054
Merge branch 'fix/116-list-sorting' into integrate/review-fixes
m-murphy Nov 1, 2026
b4c1a71
Merge branch 'fix/117-utc-timestamps' into integrate/review-fixes
m-murphy Nov 1, 2026
57d3079
fix(web): sort date columns with the display parser so server timesta…
m-murphy Nov 1, 2026
60dfb88
Merge branch 'fix/113-delete-impact-refresh' into integrate/review-fixes
m-murphy Nov 1, 2026
79fcabc
Merge branch 'fix/112-destructive-actions-admin' into integrate/revie…
m-murphy Nov 1, 2026
a042b19
Merge branch 'fix/118-dashboard-numbers' into integrate/review-fixes
m-murphy Nov 1, 2026
e5371d1
fix(api): reject malformed created_from/created_to on the studies and…
m-murphy Nov 1, 2026
3162282
fix(qpcr): report amplification rows skipped for wells with no result
m-murphy Nov 1, 2026
e6d1bc6
fix: Studies search matches lead person and description (#128)
m-murphy Nov 1, 2026
21d7dbf
fix(web): name barcode-only Derivations import Collections after the …
m-murphy Nov 1, 2026
aa62d30
fix(api): compare stored timestamps with bounds in the stored format …
m-murphy Nov 1, 2026
514c178
fix(specimens): page, sort, and search the Specimens list on the serv…
m-murphy Nov 1, 2026
5258650
fix(api): add lookup indexes and load bag and box papers in one query…
m-murphy Nov 1, 2026
aabf5d8
fix(api): return 4xx for bad input and honour location_id on the cont…
m-murphy Nov 1, 2026
b1afdc8
fix(web): refresh other pages after Container, Subject, Control and R…
m-murphy Nov 1, 2026
4881c85
Merge branch 'fix/106-indexes-detail-query' into integrate/review-fixes
m-murphy Nov 1, 2026
f21edd7
Merge branch 'fix/127-timestamp-comparisons' into integrate/review-fixes
m-murphy Nov 1, 2026
6ad7f11
Merge branch 'fix/120-api-4xx' into integrate/review-fixes
m-murphy Nov 1, 2026
0850364
Merge branch 'fix/115-specimens-pagination' into integrate/review-fixes
m-murphy Nov 1, 2026
398d2ad
fix(api): read pagination settings through the settings cache
m-murphy Nov 1, 2026
e8c7ac7
Merge branch 'fix/119-cache-invalidation' into integrate/review-fixes
m-murphy Nov 1, 2026
7fccd8a
Merge branch 'fix/128-studies-search' into integrate/review-fixes
m-murphy Nov 1, 2026
2c5088b
Merge branch 'fix/129-qpcr-amplification-rows' into integrate/review-…
m-murphy Nov 1, 2026
e1283ef
Merge branch 'fix/130-derivations-collection-names' into integrate/re…
m-murphy Nov 1, 2026
c12132c
fix(api): clear specimen type cache, dedupe sheet moves, auth the uni…
m-murphy Nov 1, 2026
8367a12
fix(db): back up the database before startup migrations (#132)
m-murphy Nov 1, 2026
065d547
fix(web): show Admin Users and Reference Data errors where the admin …
m-murphy Nov 1, 2026
e30e79a
test(api): give createTestSpecimen's default Study a unique title
m-murphy Nov 1, 2026
06de873
fix(controls): unique names, zero quantities and spreadsheet lines fo…
m-murphy Nov 1, 2026
1bab280
test(api): stop createTestSpecimen's default studies colliding
m-murphy Nov 1, 2026
3bf9316
fix(api): check control batch properties, Location collection flag an…
m-murphy Nov 1, 2026
3e7d4cf
fix(web): number import errors by file row and drop blank Derivations…
m-murphy Nov 1, 2026
683a0e2
fix(subjects): search Merge Subjects on the server; create a new subj…
m-murphy Nov 1, 2026
d41ecb7
Merge branch 'fix/132-migration-backup' into integrate/review-fixes
m-murphy Nov 1, 2026
facf859
Merge branch 'fix/133-api-data-checks' into integrate/review-fixes
m-murphy Nov 1, 2026
21b7594
Merge branch 'fix/134-cache-moves-units' into integrate/review-fixes
m-murphy Nov 1, 2026
47ca330
Merge branch 'fix/135-subjects' into integrate/review-fixes
m-murphy Nov 1, 2026
3149151
Merge branch 'fix/136-control-batches' into integrate/review-fixes
m-murphy Nov 1, 2026
5be6d02
Merge branch 'fix/137-admin-errors' into integrate/review-fixes
m-murphy Nov 1, 2026
d866634
Merge branch 'fix/138-import-rows' into integrate/review-fixes
m-murphy Nov 1, 2026
18e3c55
fix(subjects): Create New Subject refuses a name the study already ha…
m-murphy Nov 1, 2026
8626013
fix(statistics): report study dates and location counts that match th…
m-murphy Nov 1, 2026
e6a5b1e
fix(export): export modal applies Tags in CSV mode, keeps errors, and…
m-murphy Nov 1, 2026
eff1ab9
test(export): a control tube's empty study_code is null in a JSON bar…
m-murphy Nov 1, 2026
d88b559
fix(db): apply the lab's legacy data cleanup decisions in migration 0…
m-murphy Nov 1, 2026
30ea366
fix(subjects): Subject names compare ignoring case within a Study (#126)
m-murphy Nov 1, 2026
e95bd6f
feat(data-audit): report legacy data problems in the integrity report…
m-murphy Nov 1, 2026
c671fc3
fix(migrations): 008 and 010 leave what they can't fix instead of abo…
m-murphy Nov 1, 2026
936aee9
fix(dates): keep a collection date migration 008 left out of filters,…
m-murphy Nov 1, 2026
a06a04d
fix(db): remove migration 013; the lab's cleanup is not a migration (…
m-murphy Nov 1, 2026
364267f
feat(db): one-time data fix runner, with the lab's cleanup as a fix f…
m-murphy Nov 1, 2026
bf5dff3
Merge branch 'fix/122-export-modal' into integrate/review-fixes
m-murphy Nov 1, 2026
de99405
Merge branch 'fix/126-legacy-cleanup' into integrate/review-fixes
m-murphy Nov 1, 2026
f31cb17
Merge branch 'fix/107-statistics-dates' into integrate/review-fixes
m-murphy Nov 1, 2026
23d38e1
Merge branch 'fix/lenient-migrations' into integrate/review-fixes
m-murphy Nov 1, 2026
4e51a0b
fix(data-fix): Polyclonal Ab allows micronix tubes in the EPPIcenter …
m-murphy Nov 2, 2026
a7cf9b6
fix(web): Merge Subjects keeps its options while a newer search loads
m-murphy Nov 2, 2026
bf771e9
test(web): Merge Subjects tests wait for the debounced search before …
m-murphy Nov 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion CODING_STANDARDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Paths are relative to `packages/api/`.
- Return the full `SpecimenSource` shape to every caller, not a per-endpoint subset (ADR 0007).
- Load a Container's read view (placement, identity, Tags, unit, location, Specimen, Source) with `loadContainerReadViews` or `loadContainerReadViewsByIds` from `src/lib/container-read-view.ts`, not with per-endpoint queries (ADR 0009).
- Map the read view to its read path's type: `EnrichedContainerApi` for list, derivations and PATCH, `EnrichedStorageContainer` for collection detail, and CSV row fields for Container export (ADR 0009).
- To choose which Containers an export or statistic covers, call `selectContainers` from `src/lib/container-selection/`, and filter a paginated Container query by Tags with `containerHasAllTags` (ADR 0011).
- To choose which Containers an export or statistic covers, call `selectContainers` from `src/lib/container-selection/`, and filter a paginated Container query by Tags with `containerHasAllTags` and by Location with `containerAtLocation` (ADR 0011).

### Routes

Expand Down
48 changes: 46 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,38 @@ _Avoid_: End user, operator (without context)
Lab staff responsible for long-term specimen storage, inventory accuracy, and sample retrieval.
_Avoid_: Curator (unless that is your org's term)

### Roles

Every account has one role. The API enforces it on each route, and the web app hides actions the role cannot take.

**Admin**:
Can do everything a **Member** can, plus manage users, settings, **Reference data**, **Locations**, and every **Destructive action**.

**Member**:
Lab staff who create and change **Operational data**: register, store, move, derive, and import. A member cannot take a **Destructive action**, except deleting a tutorial **Study** (short code starting with TUT).
_Avoid_: User (every role is a user)

**Viewer**:
Can read data and run exports, but not create or change anything.

**Destructive action**:
An action that removes or rewrites recorded **Operational data** or configuration in bulk, or that the app cannot undo. Admin-only. Today these are:

- Deleting a **Collection** with its contents, and its preflight (removes every **Container** in it, orphaned **Specimens**, and optionally emptied **Subjects**)
- Deleting an empty **Collection** (Data Integrity)
- Deleting a **Control batch** (removes its **Specimens** and their **Containers**)
- Editing or deleting a **Derivation** record
- Deleting a non-tutorial **Study** (removes its **Subjects**, **Specimens**, and **Containers**)
- Deleting **Reference data**, a **Location**, or a user account

Members may also take these actions, which cannot be undone but are part of everyday lab work, so they are not destructive actions:

- Merging two **Subjects**
- Deleting a qPCR experiment with its plate layout and results
- Deleting one **Specimen** and its **Containers** from a **Control batch**

_Avoid_: Calling an ordinary edit (renaming a **Collection**, changing a **Container**'s quantity, removing a **Tag** from a **Container**) a destructive action.

### Provenance hierarchy

**Study → Subject → Specimen** is the primary provenance chain. Most specimens trace back to a study participant or sample source through this path.
Expand All @@ -25,11 +57,15 @@ The top-level organizational unit for a research project, clinical trial, or oth
_Avoid_: Project (unless referring to non-study work), trial (as a synonym for study)

**Subject**:
A participant or sample source within a study, identified by a unique name within that study.
A participant or sample source within a study, identified by a unique name within that study. Names compare ignoring case (ASCII letters, as SQLite's `NOCASE` does): a study can't have both `ABC-1` and `abc-1`, and creating, renaming or importing a Subject whose name differs from an existing one only by case is refused. The write paths enforce it; there is no database index for it, because a database from before the rule may already hold such names, and an index would stop it from starting. The integrity report lists those pairs for an operator to merge or rename. Decided in issue #124 (decision 6).
_Avoid_: Patient (unless the study is clinical and that is your org's term), donor (unless that is your org's term)

**Specimen**:
The collected unit — a registered biological material from a specific collection event. One collection event, one specimen record.

**Specimen identity**:
A Subject's Specimen is identified by its Subject, Specimen type and collection date: **Specimen registration** adds Containers to the Specimen with that combination instead of creating a second one. Existing duplicates stay as they are (issue #124, decision 7): no migration merges them or adds a unique index to an upgraded database, and the integrity report lists them. Migration 008 can create dated duplicates by converting legacy `M/D/YY` dates, and clearing a placeholder collection date can make a Specimen an undated duplicate of another. Known difference: fresh installs have the unique index `idx_specimen_study_subject_type_date` on (`study_subject_id`, `specimen_type_id`, `collection_date`) and upgraded databases do not. That index treats an empty collection date as distinct, so fresh installs also allow undated duplicates.
_Avoid_: Assuming the database refuses a duplicate Specimen; on an upgraded database only registration's lookup prevents one.
_Avoid_: Sample (as the domain term; see **Sample** below)

**Sample**:
Expand Down Expand Up @@ -132,9 +168,13 @@ Where a **Container** sits in the physical storage hierarchy — its **grid posi
_Avoid_: Enrichment (implementation term); conflating placement with specimen or source lookups; placing paper directly on a box or bag without a sheet

**Grid position**:
The well or slot of a tube or well **Container** in its plate or box (e.g. A01). Optional: a tube or well may belong to a **Collection** with no grid position (legacy rows). At most one Container occupies a given grid position in a Collection; a missing position occupies no cell. Not applicable to **Paper**.
The well or slot of a tube or well **Container** in its plate or box, stored as a row letter plus a two-digit column (A01). One rule, the Grid position rule, says which positions a **Collection** has (micronix plate A01–H12, cryovial box A01–J10) and how a typed position maps onto one: `a1`, `A1` and `a01` all mean A01, and anything else, including a cell outside the plate or box, is not a Grid position. Every write that sets a position applies the rule and rejects a position it does not accept; every read that lays Containers out on the grid applies the same rule, so a position a write would reject places no Container (see **Unplaced container**). Migration 010 repairs the legacy malformed positions it can and leaves the rest as they were; the integrity report flags any stored position that fails the rule. Optional: a tube or well may belong to a **Collection** with no grid position (legacy rows). At most one Container occupies a given grid position in a Collection; a missing position occupies no cell. Not applicable to **Paper**.
_Avoid_: Well (as the domain term); treating a missing position as occupying a slot; requiring every tube to have a position before it can exist in a Collection

**Unplaced container**:
A tube or well **Container** that belongs to a plate or box but occupies no cell: it has no **Grid position**, or its stored position fails the Grid position rule for that Collection. It still counts as part of the Collection. Plate and box pages list unplaced Containers in their own section, labelled with the stored value, and plate scan validation expects them rather than treating them as extras.
_Avoid_: Empty (an empty cell holds no Container); treating an unplaced Container as missing from its Collection

### Export

**Container selection**:
Expand Down Expand Up @@ -218,6 +258,10 @@ _Avoid_: Stuffing paper spot ids into `collection.barcode`; sending JSON `null`
The application — a specimen inventory and workflow system for research and clinical labs.
_Avoid_: LIMS (unless describing the broader category externally), ELN, SRA submission system (not available today)

**Data fix**:
A one-time SQL change that one install's operator runs by hand against that install's database with `bun run data-fix` in `packages/api`, to carry out decisions about that install's own data. Unlike a numbered migration, it ships to no other install, so it may name that install's rows by id. The runner copies the database first, refuses a database below the current schema version, runs the fix in one transaction, and records it in `data_fix_log` so it runs only once. The EPPIcenter lab's fixes live in `ops/data-fixes/`.
_Avoid_: Migration (for a change to one install's data); a migration changes every install's schema.

## Scope

What SampleDB is and is not — for product descriptions and documentation.
Expand Down
3 changes: 2 additions & 1 deletion docs/adr/0005-wire-dto-omit-on-wire.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ SampleDB API responses use a **wire DTO** layer between persistence (Drizzle/SQL
| **Wire** (HTTP JSON) | Key **omitted** when unset |
| **Client** (web parse / UI) | TypeScript optional / `undefined` |

Persistence-shaped rows must not be spread directly into JSON responses. Route handlers return wire-shaped payloads via `c.json(...)`; **global `omitOnWireMiddleware`** applies the null-omission policy to every successful JSON response.
Persistence-shaped rows must not be spread directly into JSON responses. Route handlers return wire-shaped payloads via `c.json(...)`; **global `omitOnWireMiddleware`** applies the null-omission policy to every successful JSON response, except a JSON Container export (see Consequences).

### Two responsibilities (do not conflate)

Expand Down Expand Up @@ -53,6 +53,7 @@ Middleware is **permanent infrastructure** for null omission — not a substitut
- Expand `@sampledb/contract/wire` schemas incrementally where discriminated shape matters; do not wire-schema every CRUD route.
- Web parsers import `@sampledb/contract/wire` for container responses; optional fields use `.optional()` in Zod, not `.nullish()`.
- Inbound container writes use the unified `ContainerWriteInput` shape ([ADR 0006](./0006-unified-container-inbound-write.md)) — same placement/identity split as outbound wire, not flat legacy fields.
- A JSON Container export keeps its nulls: the handler calls `keepNullsOnWire` from `packages/api/src/middleware/omit-on-wire.ts`, and every row carries every export column, null when empty. An export is a file, and a row missing a key reads as a missing column (#122). No other response opts out.
- Export columns split accordingly (`barcode`, `sublabel`, `sheet_name`). Stored export configurations migrate retired keys on read (`label` → `sheet_name`; append `sublabel` when `barcode` or `sheet_name` is present).

## Related ADRs
Expand Down
Loading
Loading