Found by manual testing of the shipped 0.0.1b1 wheel.
What happens
There is no way to reach the annotator from inside the app. The whole point of the product — open an asset and label it — is reachable only by typing a URL.
Three facts, each verified in the running app:
- Every tile in the batch gallery renders
disabled, so clicking one does nothing.
- The annotator's own "Open the gallery" button in the top bar is disabled.
- Nothing anywhere navigates to
/jobs/:jobId. Grepping the frontend for navigation to that path returns only API calls in frontend/ui-core/src/annotator/jobQueries.ts.
I reached the annotator during testing only by pasting /ui/jobs/{jobId} into the address bar, having read the job id out of the REST API. A user has no way to discover it.
Root cause
frontend/app/src/routes.tsx:129-133:
function Gallery(): JSX.Element {
const { projectId, batchId } = useParams();
if (projectId === undefined || batchId === undefined) return <NotFound />;
return <GalleryScreen projectId={projectId} batchId={batchId} />; // no onOpenAsset
}
GalleryScreen takes an optional onOpenAsset (frontend/ui-core/src/screens/GalleryScreen.tsx:54) and renders each Tile disabled when it is absent (line 134):
{...(onOpenAsset === undefined ? {} : { onOpen: () => onOpenAsset(asset) })}
Every sibling route wires its callbacks — Projects passes onOpenProject (line 111), Project passes onIngest / onOpenBatch / onOpenDataset (lines 122-124). Gallery passing none looks like an omission rather than a decision, and the ui-core-takes-navigation-as-a-callback contract documented at lines 101-108 is exactly what makes it a one-line class of fix.
The design question that has to be answered first
It is not purely a missing callback, because the two screens are keyed on different things:
- the gallery lists assets in a batch (
/projects/:projectId/batches/:batchId)
- the annotator is keyed on a job (
/jobs/:jobId)
A batch is partitioned into jobs at approval, so an asset belongs to exactly one job — but the gallery's BatchAsset rows carry job_id only once the batch leaves draft (this is #29's deliberate shape: job_id/progress are null exactly while the batch is a draft). So:
- For a draft batch there is no job to open, and tiles being inert is arguably correct — but they should say so rather than look broken.
- For an approved batch,
asset.job_id is present and the navigation is available without a new endpoint.
Recommended: navigate to the annotator positioned on the clicked asset, not merely to its job — otherwise clicking the fifth tile opens the job at its first asset, which reads as the click being ignored. That needs the annotator to accept an initial asset, e.g. /jobs/:jobId?asset={assetId} read through useSearchParams (already imported in routes.tsx:52). Confirm against AnnotationPage's current props before assuming it can be told where to start.
The reverse direction ("Open the gallery") needs the annotator to know its batch — GET /jobs/{job_id} returns batch_id, which is exactly what JobService.batch() was added for in #29 — and the project id to build the path.
Acceptance criteria
- Clicking a gallery tile in an approved batch opens the annotator on that asset.
- "Open the gallery" in the annotator returns to that batch's gallery, enabled whenever the job resolves.
- Tiles in a
draft batch are inert and explain why (tooltip or disabled reason), rather than looking like a broken control.
- A Playwright spec walks project → batch → tile → annotate → back to the gallery entirely by clicking, with no direct navigation to a
/jobs/ URL anywhere in the spec — that is the assertion that this cannot regress.
- Deep-linking straight to
/ui/jobs/{jobId} still works.
Scope notes
Found by manual testing of the shipped
0.0.1b1wheel.What happens
There is no way to reach the annotator from inside the app. The whole point of the product — open an asset and label it — is reachable only by typing a URL.
Three facts, each verified in the running app:
disabled, so clicking one does nothing./jobs/:jobId. Grepping the frontend for navigation to that path returns only API calls infrontend/ui-core/src/annotator/jobQueries.ts.I reached the annotator during testing only by pasting
/ui/jobs/{jobId}into the address bar, having read the job id out of the REST API. A user has no way to discover it.Root cause
frontend/app/src/routes.tsx:129-133:GalleryScreentakes an optionalonOpenAsset(frontend/ui-core/src/screens/GalleryScreen.tsx:54) and renders eachTiledisabled when it is absent (line 134):Every sibling route wires its callbacks —
ProjectspassesonOpenProject(line 111),ProjectpassesonIngest/onOpenBatch/onOpenDataset(lines 122-124).Gallerypassing none looks like an omission rather than a decision, and theui-core-takes-navigation-as-a-callback contract documented at lines 101-108 is exactly what makes it a one-line class of fix.The design question that has to be answered first
It is not purely a missing callback, because the two screens are keyed on different things:
/projects/:projectId/batches/:batchId)/jobs/:jobId)A batch is partitioned into jobs at approval, so an asset belongs to exactly one job — but the gallery's
BatchAssetrows carryjob_idonly once the batch leavesdraft(this is #29's deliberate shape:job_id/progressare null exactly while the batch is a draft). So:asset.job_idis present and the navigation is available without a new endpoint.Recommended: navigate to the annotator positioned on the clicked asset, not merely to its job — otherwise clicking the fifth tile opens the job at its first asset, which reads as the click being ignored. That needs the annotator to accept an initial asset, e.g.
/jobs/:jobId?asset={assetId}read throughuseSearchParams(already imported inroutes.tsx:52). Confirm againstAnnotationPage's current props before assuming it can be told where to start.The reverse direction ("Open the gallery") needs the annotator to know its batch —
GET /jobs/{job_id}returnsbatch_id, which is exactly whatJobService.batch()was added for in #29 — and the project id to build the path.Acceptance criteria
draftbatch are inert and explain why (tooltip or disabled reason), rather than looking like a broken control./jobs/URL anywhere in the spec — that is the assertion that this cannot regress./ui/jobs/{jobId}still works.Scope notes
openapi.jsonuntouched.ui-coremust not import the router — navigation stays a callback prop, per the contract inroutes.tsx:101-108.Tile's disabled rule — an asset with genuinely nowhere to go must stay inert.