Repository navigation
feat: record instrument repository syncs and the instruments that fail them - #1832
Open
thomasbeaudry wants to merge 13 commits into
Open
thomasbeaudry wants to merge 13 commits into
thomasbeaudry wants to merge 13 commits into
Conversation
joshunrau
requested changes
Oct 30, 2026
joshunrau
left a comment
Collaborator
There was a problem hiding this comment.
please fix merge conflicts and run pnpm lint to identify and fix problems. Many new linting rules have been added
| findSyncLogs( | ||
| @Query(new ParseSchemaPipe({ schema: $InstrumentRepoSyncLogsQueryParams })) | ||
| query: InstrumentRepoSyncLogsQueryParams, | ||
| @CurrentUser('ability') ability?: AppAbility |
Collaborator
There was a problem hiding this comment.
I don't think ability can ever be undefined here
Collaborator
Author
There was a problem hiding this comment.
Right — fixed. All six handlers in InstrumentReposController now take @CurrentUser('ability') ability: AppAbility rather than an optional. 7a663e6ed
|
|
||
| export type InstrumentRepoSyncLogsQueryParams = z.infer<typeof $InstrumentRepoSyncLogsQueryParams>; | ||
| export const $InstrumentRepoSyncLogsQueryParams = z.object({ | ||
| limit: z.coerce |
Collaborator
Author
There was a problem hiding this comment.
Fixed — z.coerce is gone from the file. 7a663e6ed
Collaborator
Author
There was a problem hiding this comment.
Done:
limit: $NumberLike
.pipe(z.number().int().positive().max(INSTRUMENT_REPO_SYNC_LOGS_MAX_PAGE_SIZE))
.default(INSTRUMENT_REPO_SYNC_LOGS_PAGE_SIZE),
page: $NumberLike.pipe(z.number().int().positive()).default(1),imported from @douglasneuroinformatics/libjs. 7a663e6ed
…l them An instrument that failed to compile or load during a repository import or sync was only written to the server log and skipped, so nobody knew it was missing. Every import and sync now writes an InstrumentRepoSyncLog with the number of instruments imported and, for each one that failed, its directory, the stage it failed at and why. A repository that could not be read at all is logged too. Sync now replaces a repository's instrument list with what the repository currently provides instead of adding to it. Instruments that drop off the list stay in the database with their provenance tag, so groups can still see and administer them. On the admin Instrument Repositories page: - a Failed column with the latest sync's failed-instrument count - a Sync All button (POST /v1/instrument-repos/sync/all) - a sortable sync history below the table, plus a per-repository Logs action (GET /v1/instrument-repos/sync/logs, admin only) - a Source link and an instrument search in the View dialog - long repository URLs are truncated instead of running into the next column POST /v1/instrument-repos/:id/sync now returns the sync log rather than the repository. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Adds a Clear History button above the sync history, behind a confirmation that the entries are permanently deleted from the database (DELETE /v1/instrument-repos/sync/logs, admin only). Repositories and their instruments are untouched. Sync status is now shown as a coloured dot beside a plain label, as the datahub tags sex at birth, rather than as coloured text. The Failed column of the repository table is plain text, and its Instruments column is renamed Imported. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The table records both imports and syncs, so "Sync History" was wrong: it is now the Activity Log across the heading, the Clear Log button, both dialogs, the toasts and the API reference. The repositories table's Failed cell said "Sync failed" even for a failed import and now says the repository could not be read, and its instrument count keeps the name Instruments so it no longer collides with the activity log's own Imported column. A failure reason is shown as one plain-text line, clipped rather than wrapped, with every reason in full behind a View button: a long esbuild or validation message made a row several times taller than the rest. An empty log is one line of text instead of a DataTable, whose own empty state is a fixed 288px. POST /:id/sync looked its repository up by id alone, so a user granted update InstrumentRepo for one group could sync a repository belonging only to another; it now goes through accessibleQuery as syncAll already did. Why the log's own route is admin-only while the two sync routes are not is recorded on the handler, since the subject carries no group and cannot be granted. The collection is append-only and the page prefetches it on every visit, so it gains a createdAt index, declared in both the schema and DATABASE_INDEXES because nothing else creates one here, and a request returns at most 500 entries. The page's three prefetches now run together rather than as three round trips, and the newest entry per repository is chosen by its timestamp rather than by its position in the response. Co-Authored-By: Claude Opus 5 <[email protected]>
The admin page read every entry on every visit and filtered client side. The collection is append-only and nothing prunes it, so the response grew with the instance's whole history; the 500-entry cap that bounded it also silently hid older entries, and a rarely-synced repository's history could fall off the end of its own dialog. GET /v1/instrument-repos/sync/logs now takes limit, page, repoId and sortOrder and answers with a page, its page count and the total, so each table asks only for the slice it shows and the per-repository dialog asks the server for that repository. A repoId, createdAt index serves it. The repositories table's failed count cannot come from a page, since the newest entry for one repository may be any distance down the history, so GET /v1/instrument-repos/sync/logs/latest answers with the newest entry per repository. It keys under the history's own query key, so the existing invalidations refresh both. Client-side sorting and searching went with it: both would order or filter the ten rows on screen rather than the history, which reads as a table-wide control and is not one. The date column sorts through the server instead, and SortableHeader takes the order the rows were fetched in, because a table in server mode holds no sorting state of its own. Co-Authored-By: Claude Opus 5 <[email protected]>
The assertion read the row Playwright had resolved a moment earlier, which the second query's render can replace: a detached node reports no width at all, so the clipping check failed in CI and nowhere else. The measurement now retries until it lands on an attached node. Co-Authored-By: Claude Opus 5 <[email protected]>
…arate tabs Co-Authored-By: Claude Opus 5.5 <[email protected]>
`ServerDataTableStoreParams` types `initialState` as `never`, and `calculateColumnSizing` honours `columnDef.size` only on a pinned column, so a table that pages server-side could not give one column the room its content needs: every column took an identical share of the width. The patch is type-only -- the store already read `initialState` in both modes and `reset()` deliberately never rebuilds it. It carries DouglasNeuroInformatics/libui#129; delete it and the `patchedDependencies` entry when the catalog moves to the release that includes it. Co-Authored-By: Claude Opus 5 <[email protected]>
…sitory column A repository name wrapped onto a second line, making its row taller than the rest. libui wraps a cell in a truncating span only when the content is not a React element, and Tanstack's default cell renderer is a function that `flexRender` renders as a component -- so no column was ever truncated. Every text cell now renders `TruncatedCell`: one line, an ellipsis, and the full value in the native `title` for a hover that names what was clipped. The seven columns also shared the width evenly, which left a repository called `owner/some-long-name` a seventh of the table. Pinning the other six with explicit sizes leaves the repository column the room they do not use, the way the repositories table above it already works. The `accessorFn`s on the action and failed-count columns are gone: with an explicit `cell` neither was reachable, and both ids come from `id`. Co-Authored-By: Claude Opus 5 <[email protected]>
… one line Asserts on a live node, since the clipping is layout: the cell carries the full name as its title, its content overflows, and it still stands one line tall. Co-Authored-By: Claude Opus 5 <[email protected]>
- `$InstrumentRepoSyncLogsQueryParams` parses `limit` and `page` with `$NumberLike` instead of `z.coerce.number()`, which eslint now bans because coercion turns `''`, `null` and `true` into valid numbers rather than rejecting them. `$AuditLogsQueryParams` already paged this way. - `InstrumentReposController` takes `ability` as `AppAbility` rather than an optional one. Every route here is behind `@RouteAccess`, so a request reaching a handler always carries an ability, and the rest of the controllers in this app type it the same way. - Satisfies the rules main added: a `beforeEach`/`afterEach` sits inside its `describe`, `toThrow` names what it expects, and a promise executor returns nothing. Co-Authored-By: Claude Opus 5 <[email protected]>
Measured on a live node, the table has 764px to work with there: the
repositories table's pinned columns alone came to 794, which squeezed its name
column to 32px and made the table scroll sideways. Its URL column gives up most
of that width, since a URL is truncated with the full value on hover anyway, and
the name cell renders `TruncatedCell` rather than wrapping once the column is
narrow enough for a long name to need two lines.
The activity log's own pinned columns came to 745 of the same 764, leaving its
repository column 19px. Each one is trimmed to the width its longest header
needs, which gives the repository column back about a hundred pixels at that
viewport and proportionally more on a wider screen.
The card's invisibility is asserted layer by layer: Tailwind v4 compiles
`shadow-none` to transparent shadow layers rather than the keyword `none`, so
`toHaveCSS('box-shadow', 'none')` could never have passed.
Co-Authored-By: Claude Opus 5 <[email protected]>
Both facts were load-bearing in this change and neither is visible from a call site: cell content that is a React element never reaches `DataTableBody`'s truncating span, which is every ordinary column, and `columnDef.size` applies only to a pinned column. Nothing in the repo stated either, so they were worked out from the vendor source twice. Co-Authored-By: Claude Opus 5 <[email protected]>
thomasbeaudry
force-pushed
the
feat/instrument-repo-sync-log
branch
from
October 30, 2026 20:20
91ee212 to
96cdfbc
Compare
6.19.2 carries the type change this branch patched in locally (libui#129), so a server-mode `DataTable` accepts `initialState` and the activity log's columns can keep their own widths from a released dependency. 6.19.0 and 6.19.1 could not be taken: moving off `forwardRef` left published declarations naming `React` unqualified, which resolves only through `@types/react`'s UMD global. This repo has no `@types/react` and vendors React's types as a plain module, so props silently became `any` and surfaced as 26 then 18 `TS7006` errors in unrelated files. Fixed upstream in libui#130 and libui#131. Co-Authored-By: Claude Opus 5 <[email protected]>
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.
What
An instrument that failed to compile or load during a repository import or sync was written only to the server log and skipped. Nobody could tell it was missing. For example,
CPP_SERIESsilently failed because the series referenced editions the instance did not have.Sync log (API)
InstrumentRepoSyncLog: action, number imported,message(set when the repository could not be read at all), and one failure per instrument with its directory, stage (BUNDLE= did not compile,LOAD= the server refused the bundle) and reason. Reasons include the VM error and each validation issue with its path, not just "Instrument validation failed".GET /v1/instrument-repos/sync/logs(admin only) andPOST /v1/instrument-repos/sync/all. Both have two segments, so a:idroute cannot shadow them.POST /v1/instrument-repos/:id/syncnow returns the sync log instead of the repository. The web app is the only consumer.Sync replaces the repo's instrument list
instrumentIdsto what the repository currently provides instead of merging. Nothing is deleted. A dropped edition keeps itssourceRepoId, which is what group visibility reads, so groups can still see and administer old editions.Admin Instrument Repositories page
Verification
pnpm lint: 34/34 tasks pass.pnpm test:coverage: 419 files, 4849 tests, coverage at 100%.apps/api/src/instrument-repos/__tests__/instrument-repos.{service,controller}.test.tscover replace-not-merge, import/sync/download-failure logging,syncAllcarrying on past an unreadable repository, and the failure reasons for each error shape. Alsoapps/web/src/components/InstrumentRepoSyncLogTable/__tests__/, the page test, and the sync, sync-all and sync-logs hook tests.admin instrument repos sync historyblock intesting/src/specs/admin-instrument-repos.spec.ts. It covers the failed count, history and messages, the Logs dialog, the Source link and search, and Sync All. It also measures that the URL stays inside its cell on a selected row. The API responses are mocked, since the suite cannot reach GitHub. 15/15 pass on chromium.pnpm test:e2ewas not, because port 3750 (the playground) was held by another process and firefox is not installed here.Docs:
apps/api/AGENTS.mdand.agents/docs/architecture/auth-and-permissions.mdnow listInstrumentRepoSyncLogamong the models no user can be granted..agents/docs/architecture/instrument-pipeline.mdsays where a cross-repository series failure now shows up.Co-Authored-By: Claude Opus 5.5 [email protected]
✅ No longer blocked — on libui 6.19.2, patch removed
The patched dependency is gone.
chore: take libui 6.19.2 and drop the data table patchbumps thecatalog:entry, deletespatches/@[email protected]and thepatchedDependenciesblock, and drops the paragraph inapps/web/AGENTS.mdthat pointed at the patch. Nothing here depends on unreleased code any more.For the record, since this took three upstream PRs:
ServerDataTableStoreParamstypedinitialStateasnever, so no column in a server-mode table could be pinned — andcalculateColumnSizinghonourscolumnDef.sizeonly on a pinned column. This is the one the activity log needed.TS7006errors in 12 unrelated files.The regression was ours specifically, not a libui bug in general: moving the components off
forwardRefleft props annotatedReact.ComponentProps<…>, and a published declaration that namesReactunqualified resolves only through@types/react's UMD global. This repo deliberately has no@types/react("@types/react": "-"in the rootpnpm.overrides, sincea82250d07) andapps/web'sreactisworkspace:react__19.x, whose types are a plain module declaring no global namespace.skipLibCheckhides the unresolved name, so every prop on the affected components silently becameanyand the error surfaced far away, in our own call sites. #131 fixed it by importing each React type by name, so the emitted declarations are self-contained.Review feedback addressed
$NumberLike.pipe(…)replacesz.coerce.number()inpackages/schemas/src/instrument-repo/instrument-repo.ts— eslint now bansz.coerce.@CurrentUser('ability') ability: AppAbilityis non-optional across all six handlers inInstrumentReposController.vitest/require-top-level-describe(×2),vitest/require-to-throw-message,no-promise-executor-return.Rebased onto
origin/main, not merged — 12 linear commits, no merge commit. Conflicts resolved in four files:SortableHeader.tsxkeeps main'sSORT_ICONSmap with oursortOrder ?? getIsSorted()override, which a server-mode table needs;useSyncInstrumentRepoMutation.tskeeps$InstrumentRepoSyncLog.parsebecauseonSuccess(log)readslog.failures;instrument-repos/index.tsxkeeps our tabs/Card page with main's block-bodiedaccessorFnstyle; its test file adopts main'sdescribe('Route', …)wrapper around our mock resets.Activity log table layout
flexRenderrenders as a component — so no column in any table of ours was ever actually truncated. Every text cell now rendersTruncatedCell(one line, ellipsis, full value in the nativetitle), which is what the repositories table already uses for its URL. The status column keepsColorTagCell, which already clips and carries a title.repoNameis the only unpinned column, so it absorbstableWidth − 670.accessorFns, unreachable once each column had acell.docs(web):the two load-bearingDataTablebehaviours are now recorded inapps/web/AGENTS.md, which had no repo-owned home for them.Two notes on the widths
calculateColumnSizingthen divides the container evenly and ignores everysize.admin-instrument-repos.spec.tsholds both tables to it.The three e2e failures from the last review are fixed
All three were reproduced with my commits stashed, so none came from the layout work — but two were on this branch and would have kept CI red, so I fixed them rather than leave them:
admin-instrument-repos.spec.tsshould draw no card behind either tab — the card carriesshadow-none, which Tailwind v4 compiles to five fully transparent shadow layers rather than the literalnone, sotoHaveCSS('box-shadow', 'none')could never pass. The assertion now strips the transparent layers and asserts nothing is left.record-file-deletion.spec.ts— pre-existing onmain, also noted on feat: email bulk remote assignment links #1838. Untouched.Verification
pnpm lint— 34/34 tasks clean, noeslint --fixrewrites.pnpm test:coverage— 100% statements / branches / functions / lines;vitest list --filesOnlynames all ten of the branch's test files.pnpm test:e2e— 340 passed / 6 failed on patched 6.16.1.test:coverageboth ran green on 6.19.2, but e2e needs the dev-server ports, which were occupied on my machine. CI covers it. Two of the six are the pre-existingmainfailures (record-file-deletion.spec.ts,subject-detail.spec.ts@smoke). The other four (admin-management×3,auth×1) pass 50/50 in isolation and passed in two earlier full runs — flaky underfullyParallelon the single shared database, theauthone via the login throttle. Not from this branch.admin-instrument-repos.spec.ts— 25/25.🤖 Generated with Claude Code