refactor: rebuild the admin catalogue tables - #117
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Four defects that all present as the interface ignoring the reader. revalidatePath received the record page's own URL, which carries the academic year as a query string, but it matches a route path. Every revalidation missed, so accepting a change or publishing a draft only appeared after a manual reload. The path is now trimmed at the query. The record page is a server component whose tab lives in the query string, yet it mounted an uncontrolled Tabs. The editor's redirect to the preview after a save re-rendered the same instance and the tab never moved. A small client wrapper controls the value from the URL, which also keeps a reloaded or shared link on the expected tab. The directory passed a controlled state to FilterBar. That branch calls onQueryChange immediately, skipping the 250ms debounce the URL-bound branch uses, so every keystroke pushed a server navigation into a dynamic page and the pending state was lost. The directory now lets FilterBar bind to the URL, as the other list pages do. The remaining controlled callers filter client side, where immediate updates are correct, so the shared component is unchanged. The ten admin error boundaries removed with the old interface are restored. CatalogueError survived the rebuild but nothing imported it, so any failure under /admin/<kind> took down the whole page. The open-review indicator is a counted badge rather than a bare amber dot, and the record container drops a mx-auto that had no width to centre within.
The import page held three hand-rolled lists. Runs were a column of buttons 20rem wide whose run number, status, progress and timestamp sat in unaligned stacked spans, so nothing lined up across rows. Records were a wrapping flex row where a long error message pushed the Review button onto the next line. Stages were bordered list items printing a raw millisecond count. All three are now DataTableShell tables. The six-column data-imports grid already existed in catalogue-table.module.css, written for this page and unused since the rebuild; the record table adopts it. Runs and stages get their own grids alongside it. The run list moves out of the narrow master column and across the page, so model, cost and progress are readable. Durations render as seconds past a thousand milliseconds. Run status shows a label rather than the raw enum. The target detail placeholder is a skeleton matching the card it replaces, instead of a bare sentence that shifted the layout when the detail arrived.
The rebuild in A5 to A7 was specified in data-flow terms and reused none of the existing component kit, so the plan now names the kit every remaining project draws from and the review rule that goes with it. The new document carries the interface, pipeline, schema and test work still outstanding, with the findings behind each item so they are not rediscovered: the review panel cannot show a per-field diff because changes.ts diffs whole collections, a manual edit clears the publication gate, summary fields are parsed and never stored, discarding a model extraction is silent, and a failed target blocks its own retry.
changes.ts records a collection as one row, so altering a single session's delivery mode produced an entry whose old and new values were the whole session array. ValueDiff rendered both sides as JSON and left the reviewer to find the difference by eye, then offered Accept or Reject for the collection entire. That is the reason reviewing a record did not read as a review. The diff now walks into the change and lists only the fields that differ, naming each one, so the example above reads "Item 1 · Delivery mode: In person / Online". Added and removed rows are labelled as such. Values render through CatalogueValue, recovered from the interface removed in 80b95de, which shows a record as a definition list rather than braces. A change too wide to reduce still falls back to both raw values, so nothing is ever hidden. Component tests cover a scalar, a single altered field inside a collection, an added row and the fallback. Flag codes and entry statuses are named for the reader instead of printing the database vocabulary, and Reject all, which discards every imported value at once, now asks first. ConfirmDialog was already used twice in neighbouring files.
The previous commit reached for tables but invented four CSS grid layouts for them. The interface removed in 80b95de did not work that way, and the result was similar without being the same. There are two table shells in the repository. ui/common/data-table wraps an ordinary primitive table with a minimum width, which is what CourseImportPipeline used for pipeline stages. The catalogue-table shell lays a CSS grid over a list of records and owns the data-imports grid written for the imports list. The stage and run tables now use the first; the record table uses the second, with the columns that grid was sized for. The record table recovers what ImportsList actually did: LinkedTableRow so the whole row is a target, CatalogueIdentity so a record reads as an icon, title and code rather than a bare monospace button, and CatalogueRowActions for the actions the grid already reserves a column for. Targets carry the directory title for that, joined through the directory entry. Snapshot history was not a table at all. CourseImportHistory and CatalogueReviewHistory were timelines, so the history tab is one again, with snapshots and publications merged into a single ordered story instead of two lists to interleave by eye. The earlier note claiming this surface was tabular was wrong and is corrected. The extraction summary regains the token, cost and latency grid it had as a definition list, with truncation raised as an Alert rather than a sentence appended to a description.
The reuse list named one table shell where the repository has two, and the outstanding work claimed version history was tabular. Both were inferred from the data rather than read off the component being replaced, and both were wrong. The list now separates the catalogue grid, which sizes columns for a list of records, from the ordinary shell used for every other table, and names the timeline as the shape a version or activity history takes. A short note says to read the deleted component first and how to find it.
Artefacts were raw download links that took the reader out of the application and into a browser rendering of a JSON file. The viewer that did this properly was removed in 80b95de for schema reasons, not design ones, and its dependencies all survived. It is restored with its vertical artefact navigation, its own stylesheet so the list does not inherit the Nova segmented-tab track, the attempt picker for artefacts recorded more than once, syntax highlighting for HTML and Markdown through the retained highlight.js entry, a loading state and a retry on failure. Two pieces it depended on did not come back: the scroll preview the viewport only used when asked, and the change-set reader, whose artefact is JSON like the others. The pipeline table drops the artefacts column it briefly carried. CourseImportPipeline never had one, because the artefacts had a viewer.
Only the title text was a link in a 68px row, and the directory was the one catalogue table with no actions column although its grid reserved the space the sibling layouts use. Rows become LinkedTableRow, so the row is the hit area, and each carries CatalogueRowActions with the record, its ANU page and its latest import run. The ANU URL moves out of the record header into anu-source.ts because three surfaces now need to point at the same page.
CatalogueLoading knew two of DataTableShell's grids, so five directory routes fell through to a six-column selectable skeleton in front of a five-column directory table, and the imports routes drew a bare table in front of a toolbar, a run table and a card. Every skeleton now names the layout it stands in for, describes each column by the shape of the cell it reserves, and holds the directory's year picker and refresh row open. The table skeleton is separated from the page shell so catalogue-pages.tsx can use it as its Suspense fallback. It previously drew three grey rectangles of its own, which meant the reader saw two different shapes in sequence.
The blockers were right-aligned text-xs text-muted-foreground, repeated in a title attribute on the disabled button: the reason you cannot publish was the faintest thing on the page. They are now a warning Alert with a link into the review that clears them. A record with no draft is no longer told it has nothing to publish. That is the ordinary state of a finished record, not a blocker, so the publish button is simply absent until a draft exists.
/admin/<kind>/[code] had none, so a failed snapshot or review query fell through to the root boundary. CatalogueError's copy is about a list, so the record gets its own variant whose way out is the list it came from rather than the admin overview.
The changes were a <ul> of bordered <li>, each with the field, status, source and three buttons in one wrapping flex row, so no two rows lined up and there was no way to scan what was still open. They are decisions over a fixed schema, so they are a table: field, current, imported, source, decision, on the ordinary table shell rather than the catalogue grid. A change over a collection, or one carrying a source excerpt, opens a detail row with the existing field-by-field ValueDiff underneath. "3 of 11 to decide" becomes the vendored Progress primitive, the excerpt gains the link back to the ANU page it was read from, and the flag note uses the field primitive rather than a hand-rolled <label>. The record page showed every review at full size, so five imports meant five 400px cards. Only the latest is a live decision; the rest sit behind a disclosure.
The runs page loaded every run for the kind, capped at twenty-five with no offset, and offered no way to find one. It then polled router.refresh() every four seconds while a run was active, refetching the whole server page, every run and every target, to learn that one counter had moved, and resetting the reader's scroll position each time. The loader now returns a page: the run rows for the current query, sort and page, plus the selected run with its targets. Only the selected run carries target rows, so listing runs no longer reads every target of every run. Search, status filter, sort and paging bind to the URL through FilterBar, SortMenu and Pagination, as the imports toolbar removed in 80b95de did. An active run is watched through loadImportRunProgress, which reads four counters from one row. The counters are stored against the run they came from, so a reading can never be shown for another run, and a full refresh happens once, when the run settles. The empty state moved inside the table so the toolbar stays reachable, and says whether the list is empty or merely filtered.
A manual edit cleared the publication gate. catalogue_publish_blockers() found open review entries through the draft's import_target_id, which only an imported or applied snapshot carries, so any hand-edited draft had an empty blocker list. Opening a review with a blocking flag and making one trivial edit was enough to publish past it. Blockers now resolve through the item year. Structure summary fields were parsed, carried through the projection and hashed into projectionSha256, but had nowhere to land: the requirements migration dropped academic_structure_summary_fields and nothing replaced it. Every programme import discarded them, and hashing data the database never stored meant two snapshots could differ by hash alone with nothing visible to show for it. They have a table, a write, a read and a place in the review comparison. Discarding a model extraction was silent. The target ended `ready` with a null error code and every stage completed, and only catalogue_extractions recorded what had happened. The merge now emits a blocking flag, and the target carries the code and reason. academicStructureModelResponseError, which explains a truncated response and which nothing had ever imported, is wired in so the cause reaches the extraction record. The structure merge accepted or rejected the whole model response, so one unusable fee cost the requirement tree. It is field by field, as the course merge already was, with an accepted and a rejected list. A target whose worker died stayed queued and the active-target index then refused every retry for that item year, escapable only by a hand-written statement. Recovery no longer needs one, and the sweep it depends on gains its missing index.
The runs page is read on the server and driven from the client, and the client needs the sort default and the status list to build its toolbar. Importing those constants from admin-catalogue.ts pulled a module that begins `import "server-only"` into the browser bundle, which typecheck and lint both allow and only the production build rejects. The vocabulary moves to catalogue-kinds.ts, which the client already reads. admin-catalogue.ts re-exports it, so nothing on the server changes.
Every other admin page hands its tab bar to AppShell, which draws it directly under the breadcrumb. The record page rendered its own inside the content instead, so arriving from the directory moved the tabs down the page, and the preview's own tabs then sat under a second bar at a different indent. The provider now wraps the shell, as SectionTabs has always documented, and only the preview nests tabs of its own. The directory header repeated what the page already showed. "3012 listed" is the number the table footer prints, and an Import runs button sat immediately below an Import runs tab. Both are gone; the refresh time moves onto the action it describes, and the line now speaks only when the listing has never been fetched, failed, or carries a warning. A record could read "Published" on its own page and "Ready for review" in the import runs list at the same time. A first import has no baseline, so its changes are recorded as accepted and the candidate becomes the draft without an Apply step, and applied_snapshot_id was never set. It is set at that point now, backfilled for the targets already written that way, and the outcome reads Applied.
The review tab was a run summary, seven identical full-width flag cards and a thirty-row table, all at the same size and weight, so nothing on it said what needed attention or whether the record could be published. The record header now opens with the verdict: what state the record is in, why a draft is held back, and the single control that moves it on, from outstanding decisions through applying a review to publishing. recordNextStep mirrors catalogue_publish_blockers, including its rule that only the newest finished review and the one behind the draft gate a publication, so the interface never offers a publication the database will refuse. Publishing is outward-facing, so it now confirms. Flags are split by weight rather than repeated. A blocking flag keeps its own row and its note field because it is a decision; the rest collapse into one row per kind, which turns six copies of "Model disagreed with the parser" into one line naming the six fields. Flags carry only the leaf key the extractor used, so they were printing camelCase at the reader; they now resolve through the same label map as changes. The decision table groups its rows into details, requirements and lists, says once above the list rows that the pipeline records a whole collection as one decision, filters to what is still open, and links each row to the place on the ANU page it was read from where the recorded locator is an anchor rather than a CSS selector. A run collapses itself when nothing in it is waiting on the reviewer, so a record with five imports behind it is not five full-height cards wherever the panel is rendered. Reused: DataTableShell, ConfirmDialog, OptionPicker, ProgressRing, Alert, Collapsible, Progress and the existing ValueDiff. The browser spec still expected the pre-table list rows and the old disabled publish button, so it is brought up to the interface it covers.
At 390px the flag row squeezed its own name into a column two words wide, because the icon, the count, the bulk action and the disclosure all competed with it for one line. The controls now drop below the name there, as they already do in the group header, and the same applies to a blocking flag and to the fields inside an expanded group. The changes view picker carried aria-label="Show changes", which replaced its visible value for anyone not looking at it, so the current view was announced as nothing at all. It now names the control and its value together, the way the sort menu beside the directory table does.
The Review tab carried a badge of open flags. It reported a number the review panel below it already states, under a label that says what the tab is for, and it moved the tab's width every time a flag was resolved, so the row of tabs shifted under the reader's cursor mid-decision. Each of Review, Preview, Edit and History now carries an icon, through the shared SectionTabs rather than a second hand-rolled TabsList. The provider still wraps AppShell so the bar renders in the shell's tabs slot, directly under the breadcrumb.
The page was a run list stacked over the records of whichever run was selected. That reads as two disconnected pages, and it asks the wrong question first: an administrator looks for a record, not for the batch that carried it, and could not see what had happened to COMP3600 without first guessing which run imported it. There is now one table of imported records across every run, newest first, with the run as a column and a filter rather than a table of its own. Search over code and title, the outcome filter, sort and paging all act on the records themselves. Choosing a run narrows the same list and raises a single strip carrying that run's model, cost, counters and Stop control, so the master and detail collapse into one view. A failed record now says why in its Outcome cell instead of only that it failed. The table is the existing data-imports DataTableShell with LinkedTableRow, CatalogueIdentity and CatalogueRowActions, so the pipeline stages and the artefact viewer stay one menu item away per record. The section tabs gain an icon each, the loading skeletons draw the table that follows them rather than the card layout the page no longer has, and the record select resolves its labels by id rather than through an embed PostgREST cannot disambiguate.
ReviewPanel now collapses itself and opens only when its run still asks for a decision, so ReviewStack's details element around the older runs had become a disclosure over a disclosure: two clicks to read one old import. The reviews render as a plain list and each panel decides for itself whether to open.
PostgREST answers every request with at most 1,000 rows and says nothing when it stops. The directory reads a year's listing to filter it in memory, which suits a few thousand rows, but read it in one request. The 2026 course listing holds 3,012 codes and the directory saw the first 1,000, up to EMET1001: about two thirds of the catalogue, every MATH, PHYS, STAT and ENGN course among them, could not be found, filtered or imported from here, and the footer reported a total of exactly 1,000. It went unnoticed because the test records are COMP courses, which sort near the six-hundredth code. The truncation also broke the check for records imported without a directory row: any item whose row fell past the thousandth was taken for one. readAllRows pages through every response. Each caller orders by a unique key so a row cannot move between pages; the targets query gains id as a tiebreaker for targets created in the same instant, and the item years query gains an order it had lacked. The directory now finds MATH1005 and reports 3,012. The year list offered every seeded year from 2020 to 2030, eleven years of which one held any catalogue, tall enough to cover the page tabs. It now offers the years whose listing has been fetched, plus this year and next so the coming handbook can be imported before it has rows. It reads the directory statuses, one row per year and kind, so it cannot hit the same cap.
HarryRandall
force-pushed
the
refactor/admin-catalogue-tables
branch
from
September 22, 2026 12:25
25741a0 to
713994c
Compare
This branch was successfully 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.
Summary
This is stack 2 of 16. It contains the focused commits between
fix/catalogue-security-and-importsandrefactor/admin-catalogue-tablesand targets the branch immediately below it.Verification
Deployment notes
Do not deploy this branch independently. Merge the stack from the bottom upwards. The one-time hosted database rebuild remains a separate approved operation after the complete stack reaches
main.