Repository navigation
fix: resolve 26 low-difficulty issues, one commit each - #1825
Merged
joshunrau merged 27 commits intoOct 27, 2026
Merged
Conversation
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
This was referenced Oct 26, 2026
Co-Authored-By: Claude Opus 5.5 <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.
Fixes 26 small, independent
Difficulty: Lowbugs on one branch. Each fix is its own commit with its ownCloses #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
odc-cliin 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 becausetesting/cannot exercise them end to end.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.staleReloadMode: 'blocking'.Issues
hidden: falsemeasures shown · #1743 dropzone reports "too many files" and keeps the earlier selectionreading 'inputs'error · #1799 scoped-package subpaths resolve to.jsVerification
pnpm lint: green, with noeslint --fixrewrites.pnpm test:coverage: 388 files and 4409 tests green, still at 100%.vitest list --filesOnlycollects all 4 new test files.pnpm test:e2e: 308 of 309 passed. The one failure was a pre-existingaccessible-instrumentstest that timed out inbrowser.newContextwhile its fixture was being set up, before the test body ran. It passes when rerun on its own.The full run caught 3 broken new e2e tests, all fixed in their issue's commit:
useSuspenseQueryclampsstaleTimeto 1 s, so an early focus event never refetched. The test now retries until the refetch happens.pointer-events-noneinput.clock.pauseAtcould target a time that had already passed.The About page shows the Core API uptime from when the app was loaded, not when the page is opened #1724 revisit test was also flaky through the user dropup, so it now revisits with
page.goForward().The Profile form on /user discards unsaved edits whenever the user query refetches, such as on returning to the tab #1720 and About page shows the Core API uptime from when the app was loaded, not when the page is opened #1724 e2e tests were also run with the fix removed. Both fail, showing the stale value, so they really pin the bug.
Doc drift fixed with the code:
.agents/docs/playbooks/add-web-data-hook.mdused the very loader call Audit logs loader prefetches under a different query key than the table reads, so every visit fetches the logs twice #1661 corrects as its example, and thedisableDefaultTimeoutcomment inapps/web/src/services/axios.tsnow names repository imports.Worth a look
fetchQuery({ staleTime: 0 })andstaleReloadMode: 'blocking'rather thanensureQueryData. This is deliberate, because the uptime must be live, and a comment beside the loader says so. A failing/v1/setupnow shows the error page on About instead of a stale value.Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com