Skip to content

fix: resolve 26 low-difficulty issues, one commit each - #1825

Merged
joshunrau merged 27 commits into
DouglasNeuroInformatics:mainfrom
joshunrau:fix/easy-issues-batch
Oct 27, 2026
Merged

joshunrau merged 27 commits into
DouglasNeuroInformatics:mainfrom
joshunrau:fix/easy-issues-batch

Conversation

@joshunrau

Copy link
Copy Markdown
Collaborator

Fixes 26 small, independent Difficulty: Low bugs on one branch. Each fix is its own commit with its own Closes #N, so this PR can be reviewed and reverted one commit at a time, and each commit becomes one CHANGELOG entry.

How this was produced

  1. Scout. I listed the 230 open issues and the 15 open PRs. I dropped issues an open PR already fixes: Date limit to date of birth #1820 (Add dob limit #1821), Dashboard Records & Sessions chart has no legend, so its two series can only be told apart by colour #1782 (fix(web): add accessible legend to dashboard trends #1819), CSV upload silently truncates RECORD_ARRAY values with a missing closing parenthesis or a colon in the value #1705 (fix(web): stop the record array upload parser truncating values #1818), The playground instrument selector's search matches instrument UUIDs instead of labels, so typing a name finds nothing #1764 (fix(playground): search instruments by label #1817), Blog index page title reads "Open Data Capture - Open Data Capture" #1775 (fix(outreach): use the blog title in metadata #1815), odc-cli crashes with a TypeError traceback when an error response has an empty body #1779 (fix(odc-cli): report the status for an error response with an empty body #1809), pnpm test:e2e replays a cached pass after app code changes because the task hash covers only the testing package #1766 (fix(testing): update turbo task graph for test:e2e cache #1808), and the upgrade chores in build: upgrade dependencies held back by breaking changes #1660. I also dropped issues whose fix would touch a source file an open PR also changes, to avoid merge conflicts: the datahub work in feat: add the instrument hub and record provenance #1807, email templates in fix(web): scope email template languages to active languages #1810, odc-cli in fix(odc-cli): report the status for an error response with an empty body #1809, and the playground selector in fix(playground): search instruments by label #1817. Outreach-docs and CI-script issues were left out because testing/ cannot exercise them end to end.
  2. Triage. 8 read-only agents checked 55 candidates against main. For each one they recorded whether the bug still reproduces (all but Stroop Task never calls done, so it can never be completed #1634 do; the Stroop task was removed), a 1–5 simplicity score, open decisions, and concrete fix, unit-test and e2e plans. I kept the 26 that scored as trivial, had no open design question, and had a feasible e2e test.
  3. Implement. 10 agents worked in parallel, one per area, each in its own git worktree. Each wrote one commit per issue: the fix, a unit test that fails on the old code, and an e2e spec appended to an existing file. During this step they ran only scoped vitest with coverage and scoped lint; Playwright was held for one run at the end.
  4. Adversarial review. Every commit was checked by 2 independent reviewers told to assume it was wrong. One looked at correctness and completeness. The other checked whether the tests actually pin the bug and whether the commit follows the repo rules. 5 commits had blocking findings (About page shows the Core API uptime from when the app was loaded, not when the page is opened #1724, Login page heading shows "Open Data Capture" on narrow screens when the instance name is not set in the reader's language #1704, Profile form on /user discards unsaved edits whenever the user query refetches, such as on returning to the tab #1720, Measures declaring the deprecated hidden: false are hidden from the instrument summary #1694, End Current Session in the mobile menu ends the session without confirmation and discards an instrument in progress #1709). Each was revised once and passed a second two-reviewer round. For example, the first About page shows the Core API uptime from when the app was loaded, not when the page is opened #1724 fix failed on any revisit within the router's 5-minute match cache; the fix now uses staleReloadMode: 'blocking'.
  5. Integrate and verify. I cherry-picked all commits onto this branch, autosquashed the revisions into their issue commits, then ran the full gate. It caught 3 broken e2e tests and 2 pieces of doc drift, all fixed into their issue commits (details under Verification).

Issues

Area Issues
web: About #1724 uptime fetched fresh on every visit · #1643 counter no longer a second ahead
web: audit logs #1663 first Time-header click sorts oldest first · #1661 loader prefetches the key the table reads
web: login / branding #1704 narrow-screen heading falls back through languages · #1676 hidden login image no longer blocks Save
web: account #1720 refetch keeps unsaved profile edits · #1735 rejected password explained in the dialog · #1698 (schemas) international and grouped phone numbers accepted
web: mutations #1738 repository import/sync no longer aborted at 10 s · #1731 five self-handling mutation hooks no longer hit the error boundary
web: misc #1638 upload list render loop · #1644 paste parse errors numbered by file line · #1667 Add Group keeps the form on failure · #1709 mobile End Session asks for confirmation
react-core #1694 deprecated hidden: false measures shown · #1743 dropzone reports "too many files" and keeps the earlier selection
playground #1805 static-assets example submits · #1754 Save disabled for examples · #1753 closing a background tab keeps the active file · #1740 deleting the selected instrument loads the default template
instrument-bundler #1800 no spurious reading 'inputs' error · #1799 scoped-package subpaths resolve to .js
instrument-library #1678 lowercase postal code and immigration age 0 accepted · #1677 copy errors · #1674 Happiness error shown under its field

Verification

Worth a look

Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com

joshunrau and others added 26 commits October 25, 2026 21:36
The About loader read the setup state with ensureQueryData, and that query
has staleTime: Infinity, so the Core API uptime came from whenever the app
first loaded and never reflected an API restart. The loader now calls
fetchQuery with staleTime: 0, so every visit refetches the setup state and
the counter starts from the API's current uptime.

Closes DouglasNeuroInformatics#1724

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TimeValue incremented its counter and then formatted counter + 1, adding
each second twice on the first tick, so the display jumped from the start
value to start + 2 and stayed a second ahead. It now formats the
incremented counter directly.

Closes DouglasNeuroInformatics#1643

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The server-mode DataTable mounts with no sorting state, so the first
toggle of the numeric timestamp column applied TanStack's default first
direction, descending. That matched the order already on screen, so the
click changed nothing and a second click was needed for oldest first.
Setting sortDescFirst: false on the column makes the first toggle go to
ascending, in step with the descending order the rows arrive in.

Closes DouglasNeuroInformatics#1663

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The audit logs loader prefetched under ['audit-logs', search] while the
table read ['audit-logs', { ...search, page: 1, sortOrder: 'desc' }], so
the prefetched page was never used: the table showed "No Results" and
sent a second identical request. The loader now prefetches with the
same initial page and sort order the table starts from, shared through
module constants. Because the table reads with useQuery, whose default
staleTime of 0 would still refetch the just-prefetched page on mount,
the audit logs query also gets the one-second staleTime floor that
useSuspenseQuery applies to every other loader-prefetched route.

Closes DouglasNeuroInformatics#1661

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ding

The login heading shown below the lg breakpoint read the instance name for
the reader's language only, so an instance named in French alone showed
"Open Data Capture" to an English reader on a phone while the branding
panel at lg and wider showed the French name. Resolve it with
getValueForLanguage, as the branding panel does, so both agree at every
width.

Closes DouglasNeuroInformatics#1704

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The custom-size check disabled Save whenever the size was Custom, even
with the Login Image section hidden, where the message explaining it is
unmounted. Gate it on showLogo, as the resource-link check is gated on
showResourceLinks. Saving in that state is safe: unparseable dimensions
are already sent as null and the panel falls back to the small preset.

Closes DouglasNeuroInformatics#1676

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The profile form on /user was keyed on the query's dataUpdatedAt, which
React Query bumps on every successful fetch, including the focus refetch
on returning to the tab and the refetch after a password change. Each one
remounted the form and discarded what the user had typed. Key the form on
a digest of the saved profile fields instead, so it reseeds only when the
stored profile actually changes, such as after this form's own save.

Closes DouglasNeuroInformatics#1720

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Change Password dialog on /user sent the new password through
useSelfUpdateUserMutation, which keeps the app's throwOnError default and
the generic error toast, so a password the API rejects by policy (equal to
the current password or the username, or found in a breach) replaced the
page with the route error boundary. Send it through
useResetPasswordMutation instead, as /auth/reset-password does, and show
the translated reason from usePasswordErrorMessage as an error
notification while the dialog stays open.

Closes DouglasNeuroInformatics#1735

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PHONE_NUMBER_FORMAT allowed at most three digit groups, and a country
code counted as one of them, so ordinary numbers such as +1 514 555 1234,
+1 (514) 555-1234 and 01 23 45 67 89 were rejected by the admin user
forms, the account page and the API alike. Validate the characters and
the digit count instead of the grouping: an optional leading +, then any
mix of digits, spaces, dots, dashes and parentheses, with between the
existing minimum and the E.164 maximum of 15 digits. A number over 15
digits reuses INVALID_FORMAT, so no new translated message is needed.

Closes DouglasNeuroInformatics#1698

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Creating and syncing an instrument repository downloads its zipball from
GitHub and bundles every instrument in it, which routinely outlasts the
axios interceptor's 10 second default. The browser gave up and reported a
failure while the server finished the import. Both requests now opt out of
the default timeout, as the similarly slow setup request already does.

Closes DouglasNeuroInformatics#1738

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The app's QueryClient sets throwOnError: true for mutations, so five hooks
whose failures are already handled still rethrew them during render and
replaced the page with the error page: creating, syncing and deleting an
instrument repository, creating a series instrument, and updating a user.
Each now sets throwOnError: false, as useDeleteSeriesInstrumentMutation
does. useUpdateUserMutation also suppresses the default error toast on both
of its requests, since its caller shows its own inline message.

Closes DouglasNeuroInformatics#1731

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Interactive With Static Assets example called done({ message }) with
no binding for message, so Submit threw a ReferenceError. tsc accepted it
because the legacy-script example's legacy.d.ts declared message as an
ambient global for every file in the playground.

The static-assets example now submits a literal, and legacy.d.ts types the
value the legacy script sets as a Window property instead, which the
legacy example reads as window.message. A bare message is now a type error.

Closes DouglasNeuroInformatics#1805

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SaveButton recalculated its disabled state only while a saved instrument
was selected, so switching from a dirty saved instrument to an example
kept Save enabled, and a click wrote edits into an example that is never
persisted. The condition now lives in isSaveDisabled, which is evaluated
on every tick and is true for any instrument that is not Saved.

Closes DouglasNeuroInformatics#1754

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
closeFile and deleteFile always moved the selection to the tab left of
the one being removed, even when that tab was not the active one, so
closing a background tab switched the editor away from the file being
edited. Both now share closeTab, which reselects only when the removed
file was selected. EditorTab exposes its state as aria-current so the
end-to-end test can tell which tab is active.

Closes DouglasNeuroInformatics#1753

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
removeInstrument reset selectedInstrument to the default template but
left the editor's files, index filename and tabs alone, so a deleted
instrument's code stayed in the editor and preview under the template's
name. When the removed instrument is the selected one, it now applies the
same editor reset resetInstruments does, shared as loadDefaultInstrument.
Removing any other instrument leaves the selection untouched.

Closes DouglasNeuroInformatics#1740

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed builds

esbuild produces no metafile when a build has errors, but the plugin's
onEnd hook read result.metafile.inputs unconditionally. It threw, and
esbuild appended "Cannot read properties of undefined (reading 'inputs')"
to every BuildFailure as a second error. The hook now returns early when
there is no metafile, so authors see only their own error.

Closes DouglasNeuroInformatics#1800

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to .js

The bare-package rule's scope group was `(@.*\/)?`, and `.*` also
matches `/`, so any subpath of a scoped package such as
/runtime/v1/@opendatacapture/runtime-core/constants was treated as a bare
package and rewritten to a nonexistent .../constants/index.js. The scope
group now stops at one segment and the pattern is anchored, so only the
bare package gets /index.js and deeper paths get .js, as AGENTS.md
documents.

Closes DouglasNeuroInformatics#1799

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… immigration of 0

The Enhanced Demographics Questionnaire checked the postal code against a
case-sensitive pattern without normalizing it, so "h4h 1r3" or a value with
surrounding whitespace was rejected. The schema now trims and uppercases the
postal code before the pattern runs, so it is also stored in uppercase.

Age at immigration was bounded below by 1 in both the schema and the field,
rejecting subjects who immigrated before their first birthday. Both bounds
are now 0.

Closes DouglasNeuroInformatics#1678

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…happiness forms

The Enhanced Demographics Questionnaire labelled its marital status question
"Martial Status", repeated half a sentence in its French description, and
only told French subjects that the age-at-immigration question applies if
relevant. The English label now reads "Marital Status", the duplicated
fragment is gone, and the English immigration label gains "(if applicable)".

The Happiness Questionnaire's French instructions read "répondre àux
questions"; this is now "aux", here and in the form-instrument reference page
that reproduces the example.

Closes DouglasNeuroInformatics#1677

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r its field

The Happiness Questionnaire's superRefine raised the missing-reason issue
without a path, so libui's Form treated it as a root error and rendered it
above the Submit button, leaving the reason field unmarked. The issue now
carries path ['reasonNotSatisfied'], so the error appears under "Why do you
feel dissatisfied with your life?". The dynamic field's unread
`isRequired: false`, which contradicted the schema, is removed.

Closes DouglasNeuroInformatics#1674

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
useSearch copied its filtered results into state from an effect keyed on
the data array, so a caller passing a fresh `data ?? []` on every render
scheduled another render each time and looped for as long as the query
had no data. Derive the filtered list with useMemo instead, so a new
array identity only recomputes the filter and never schedules a render.

Closes DouglasNeuroInformatics#1638

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
InstrumentSummary meant to treat `hidden: false` as the old spelling of
`visibility: 'visible'`, but compared `visibility === false`, which can
never be true. Such a measure fell through to the instrument's default
visibility and was hidden unless the instrument set
`defaultMeasureVisibility: 'visible'`. Compare `hidden === false` instead.

Closes DouglasNeuroInformatics#1694

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
parseDelimitedText passed Papa Parse's zero-based error row through
unchanged, so the wizard's error list pointed at the line above the
malformed one, and disagreed with every other row-numbered error, which
counts the header as row 1. Add one to the parser's row.

Closes DouglasNeuroInformatics#1644

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page fired the create mutation and navigated back to the group list
in the same tick, so a refused group (a duplicate name answers 409)
discarded what the admin typed and left only a generic HTTP toast. Await
the mutation and navigate only once it succeeds. On failure, return the
API's reason as the form's submit error, which libui's Form shows inline
while keeping the entered values. The hook now suppresses the default
error toast and sets throwOnError: false, so the rejection no longer
reaches the router error boundary either.

Closes DouglasNeuroInformatics#1667

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Picking more files than a file group allows was reported as an
unsupported file type, because the dropzone listed every rejected file
under that message whatever the rejection code. Now only
file-invalid-type rejections get the type message. A too-many-files
rejection gets a translated count message instead and leaves the
group's earlier selection as it was. Before, the selection was replaced
by whichever files react-dropzone accepted up to the limit.

Closes DouglasNeuroInformatics#1743

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The navigation sheet's End Current Session button ended the session on a
single tap and navigated with ignoreBlocker. On a phone or tablet this
threw away an instrument in progress with no prompt. The sidebar's
confirmation dialog is now an EndSessionButton component that both
layouts render. The navbar passes an onEnded callback so the sheet
closes once the start page is reached.

Closes DouglasNeuroInformatics#1709

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 26, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@joshunrau
joshunrau merged commit 5be9838 into DouglasNeuroInformatics:main Oct 27, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Date limit to date of birth

1 participant