Class AdminWebService
SystemMonitorController DWR calls
(getCacheStatus, clearAllCaches, clearCache,
getHibernateStatus) with structured JSON-returning endpoints.
All endpoints require GROUP_ADMIN authority.
Note: the legacy CacheMonitor.enableStatistics /
disableStatistics entry points are stubs on the current post-EhCache-2
build (see CacheMonitorImpl) and are not exposed here. The legacy
resetHibernateStatus is also not exposed pending a UX decision on
whether the admin panel needs a Hibernate-stats reset button at all.
- Author:
- phase 3 admin-panel wiring
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic classstatic classstatic classstatic classstatic classstatic classstatic classstatic classstatic classResponse shape forgetCurationStatus().static classstatic classstatic classstatic classstatic classDry-run response.static classstatic classstatic classstatic classstatic classstatic classstatic classstatic classstatic classstatic classstatic classstatic classstatic classBody shape forrefreshOntology(String, boolean)returns.static classstatic classstatic final classWire shape forreindexSearchIndices(String): list of entity classes whose reindex was queued.static classstatic classstatic classstatic classstatic classstatic classstatic classstatic classstatic classstatic classstatic classstatic classstatic classstatic classstatic classPayload ofPOST /admin/tasks/platform-reports: the id of the task that was queued.static classstatic classstatic classstatic classstatic classstatic class -
Constructor Summary
ConstructorsConstructorDescriptionAdminWebService(org.springframework.cache.CacheManager cacheManager, SessionFactory sessionFactory, TaskRunningService taskRunningService, org.springframework.security.core.session.SessionRegistry sessionRegistry, List<OntologyService> ontologies, OntologyService ontologyFacade, DataSource dataSource, UserManager userManager, AnnotationSetService annotationSetService, TicketService ticketService, TaxonArgService taxonArgService, BlacklistedEntityService blacklistedEntityService, ExternalDatabaseReadService externalDatabaseReadService, GeoScrapeService geoScrapeService, IndexerService indexerService, PlatformArgService platformArgService, ArrayDesignReportService arrayDesignReportService) -
Method Summary
Modifier and TypeMethodDescriptionjakarta.ws.rs.core.ResponseAdds a single blacklist entry.jakarta.ws.rs.core.ResponseapplyObsoleteTermCorrections(Boolean dryRun, List<String> uris, Integer timeoutSeconds) Rewrite annotations that use an obsolete ontology term to the successor its ontology asserts.jakarta.ws.rs.core.ResponseClears every registered cache.jakarta.ws.rs.core.ResponseclearCache(String cacheName) Clears a single named cache.jakarta.ws.rs.core.ResponseCreate a new active user with a server-generated one-time temporary password.jakarta.ws.rs.core.ResponsedeleteBlacklistEntry(String accession) Removes a blacklist entry by its accession.jakarta.ws.rs.core.ResponsedeleteUser(String username) Soft delete — marks the account as deleted, disables it, and preserves the row so dependent references (ACL sids, audit-event authorship FKs) don't dangle.Lists the registered Spring caches by name.Out-of-process liveness probe for the gemma-curation-agents Python service.Snapshot of the annotation-set -> ticket lifecycle: per-roleAnnotationSetcounts in the recent windows, open-ticket counts byTicketType, distinct agent run id count, and latest-createdAt timestamp.jakarta.ws.rs.core.ResponseHikariCP pool snapshot.Returns a structured snapshot of Hibernate statistics.getJobs()Aggregated admin view of the in-memory background task queue.Deprecated.Reads the watermark written by the deprecated in-Gemma scrape.getObsoleteTerms(Integer timeoutSeconds) In-application port ofFindObsoleteTermsCli: which obsolete ontology terms do Gemma's annotations still use, and what does each owning ontology say should replace them.getOntologies(boolean includeTermCount) Per-ontology load status.Per-@Indexed-entity Hibernate Search 7 index status.Authenticated session listing.Process-level memory / GC / thread / load snapshot.getUsers(boolean includeDeleted) Admin user listing.Scrape GEO record metadata by accession without importing into Gemma.jakarta.ws.rs.core.ResponseBatch GEO accession import.listBlacklistEntries(int limit, int offset) Lists current blacklist entries.patchUser(String username, AdminWebService.UpdateUserRequest req) Partial update — toggle the enabled flag (lock/unlock) and/or admin role.jakarta.ws.rs.core.ResponserebuildOntologySlim(String name) Rebuild the slim-cache OWL for an ontology that supports it (currently CHEBI only).jakarta.ws.rs.core.ResponserefreshOntology(String name, boolean forceIndexing) Refresh a single ontology in-process: re-runinitialize(forceLoad=true)on a background thread so the source is re-fetched, the model is rebuilt, and the in-memory state is atomically swapped without a container restart.regeneratePlatformReport(PlatformArg<?> platformArg) Regenerate the cached report for ONE platform, synchronously.jakarta.ws.rs.core.ResponsereindexSearchIndices(String entity) Trigger a Hibernate Search 7 mass-reindex for one entity (or all of them).jakarta.ws.rs.core.ResponseResets the Hibernate statistics counters to zero.resetUserPassword(String username) Administrative password reset — set a user's password to a fresh server-generated one-time temporary password.jakarta.ws.rs.core.ResponseDeprecated.The curation agent scrapes GEO itself (scrape_geo_and_open_triage.py) and opens its own triage ticket.jakarta.ws.rs.core.ResponsesubmitMultifunctionalityRecompute(TaxonArg<?> taxonArg) Async port ofMultifunctionalityCli: recompute per-gene multifunctionality scores for a single taxon.jakarta.ws.rs.core.ResponseSubmit an async regeneration of the cached reports for EVERY platform.
-
Constructor Details
-
AdminWebService
@Autowired public AdminWebService(org.springframework.cache.CacheManager cacheManager, SessionFactory sessionFactory, TaskRunningService taskRunningService, org.springframework.security.core.session.SessionRegistry sessionRegistry, List<OntologyService> ontologies, OntologyService ontologyFacade, DataSource dataSource, UserManager userManager, AnnotationSetService annotationSetService, TicketService ticketService, TaxonArgService taxonArgService, BlacklistedEntityService blacklistedEntityService, ExternalDatabaseReadService externalDatabaseReadService, GeoScrapeService geoScrapeService, IndexerService indexerService, PlatformArgService platformArgService, ArrayDesignReportService arrayDesignReportService)
-
-
Method Details
-
getCaches
@GET @Path("/caches") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.CacheListResponse> getCaches()Lists the registered Spring caches by name. Replaces the HTML-returning legacySystemMonitorController.getCacheStatus(). -
clearAllCaches
@DELETE @Path("/caches") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response clearAllCaches()Clears every registered cache. Replaces the legacySystemMonitorController.clearAllCaches()DWR call. -
clearCache
@DELETE @Path("/caches/{cacheName}") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response clearCache(@PathParam("cacheName") String cacheName) Clears a single named cache. Replaces the legacySystemMonitorController.clearCache(name)DWR call. -
getHibernateStats
@GET @Path("/hibernate/stats") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.HibernateStatsResponse> getHibernateStats()Returns a structured snapshot of Hibernate statistics. Replaces the HTML-returning legacySystemMonitorController.getHibernateStatus(). -
resetHibernateStats
@POST @Path("/hibernate/reset") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response resetHibernateStats()Resets the Hibernate statistics counters to zero. Replaces the legacySystemMonitorController.resetHibernateStatus()DWR call. -
getJobs
@GET @Path("/jobs") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.JobsListResponse> getJobs()Aggregated admin view of the in-memory background task queue. Returns the per-taskTaskStatusValueObjectsnapshots plus counts of tasks in each status. The underlying task store is in-memory only and tasks are evicted ~10 minutes after completion. -
importGeoBatch
@POST @Path("/tasks/import-geo") @Consumes("application/json") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public jakarta.ws.rs.core.Response importGeoBatch(@Nullable AdminWebService.ImportGeoBatchRequest body) Batch GEO accession import. Ports the bulk path ofubic.gemma.apps.LoadExpressionDataCli: iterate the accession list and submit oneExpressionExperimentLoadTaskCommandper accession, returning the resulting task-id list so the caller can poll each one through/tasks/{taskId}.The optional flags on the request body (loadPlatformOnly, suppressMatching, etc.) are applied to every accession in the batch. For one-off imports use
POST /datasets/import. -
submitMultifunctionalityRecompute
@POST @Path("/tasks/multifunctionality") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public jakarta.ws.rs.core.Response submitMultifunctionalityRecompute(@QueryParam("taxon") TaxonArg<?> taxonArg) Async port ofMultifunctionalityCli: recompute per-gene multifunctionality scores for a single taxon. Submits aMultifunctionalityTaskCommand; the caller polls/tasks/{taskId}for completion.Taxon identifier may be the common name (e.g.
human), scientific name, NCBI ID, or Gemma taxon ID — same shape as elsewhere in the REST API (TaxonArg.valueOf(String)). -
regeneratePlatformReport
@POST @Path("/platforms/{platform}/report") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public ResponseDataObject<ArrayDesignValueObject> regeneratePlatformReport(@PathParam("platform") PlatformArg<?> platformArg) Regenerate the cached report for ONE platform, synchronously.The report holds the per-platform element / sequence / alignment / gene counts that
GET /platformsserves asnumberOfGenesandnumberOfMappedElements. They are never computed per request — counting distinct genes for one large platform measures ~1.7s against production — so they are read from a file that something has to write. On a production node nothing does: the Quartz trigger that refreshes them monthly (SchedulerConfig.arrayDesignReportTrigger) is gated on theschedulerprofile, which production does not run.Synchronous because a single platform is a couple of seconds and the caller wants the new numbers back. Use
POST /admin/tasks/platform-reportsfor the whole corpus. -
submitPlatformReportsRegeneration
@POST @Path("/tasks/platform-reports") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public jakarta.ws.rs.core.Response submitPlatformReportsRegeneration()Submit an async regeneration of the cached reports for EVERY platform.The bulk counterpart of
regeneratePlatformReport(PlatformArg); the corpus-wide run is far too long to hold a request open. Mirrors the other admin task endpoints: returns 202 with the job id, poll/tasks/{taskId}. -
getSearchIndices
@GET @Path("/search/indices") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.SearchIndicesResponse> getSearchIndices()Per-@Indexed-entity Hibernate Search 7 index status. Replaces the legacy gemma-webindexer.jsflow that pinged the indexer directly to discover what was indexable. The new UI uses this read-only view to surface index sizes / on-disk paths; rebuild actions stay in the CLI (IndexGemmaCLI). -
reindexSearchIndices
@POST @Path("/search/indices") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response reindexSearchIndices(@QueryParam("entity") @Nullable String entity) Trigger a Hibernate Search 7 mass-reindex for one entity (or all of them).Destructive: HS 7's mass-indexer purges the existing on-disk Lucene index for the entity before rebuilding (
purgeAllOnStart(true)). Runs asynchronously on a background thread; this endpoint returns202 Acceptedas soon as the work is queued. UsegetSearchIndices()to monitor doc-count progress and thereindexStatusfield per entity.Concurrent reindex requests are rejected with
409 Conflict— the mass-indexer is single-flight per JVM so two parallel calls would purge each other's just-written segments.- Parameters:
entity- user-facing entity name (one ofgenes,datasets,platforms,bibliographicReferences,probes,sequences,datasetGroups,geneSets); or omit to reindex all indexable roots sequentially.
-
getSystem
@GET @Path("/system") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.SystemSnapshotResponse> getSystem()Process-level memory / GC / thread / load snapshot. Complements the anonymous/infoendpoint (build + JVM identity + OS identity) with the live, admin-only resource numbers the legacysystemStats.jsphand-rolled. Single read; no historical series — that's what/metricsis for. -
getSessions
@GET @Path("/sessions") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.SessionsResponse> getSessions()Authenticated session listing. The legacyactiveUsers.jspsurfaced a count viaSecurityController.getAuthenticatedUserCountand a JSP comment promising a table of users that was never built. This endpoint delivers that table: distinct authenticated principals (across both browser and basic-auth callers), each with the count of currently-tracked sessions, the most recent request time, and any granted GROUP_* authorities. -
getOntologies
@GET @Path("/ontologies") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.OntologiesResponse> getOntologies(@QueryParam("includeTermCount") @DefaultValue("false") boolean includeTermCount) Per-ontology load status. Enumerates everyOntologyServicebean (Mondo, PATO, CHEBI, Uberon, CellType, the unified TDB, etc.) and reports each one's enable / load / initialization-thread state plus its inference and search settings. Term counts are skipped by default becausegetAllURIs()can be expensive on large ontologies; opt in with?includeTermCount=true. -
refreshOntology
@POST @Path("/ontologies/{name}/refresh") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response refreshOntology(@PathParam("name") String name, @QueryParam("forceIndexing") @DefaultValue("false") boolean forceIndexing) Refresh a single ontology in-process: re-runinitialize(forceLoad=true)on a background thread so the source is re-fetched, the model is rebuilt, and the in-memory state is atomically swapped without a container restart. Returns 202 immediately; the caller pollsgetOntologies(boolean)to watch theinitializingflag flip back to false.Matches the ontology through
OntologyServiceResolver, which accepts the well-known abbreviation (CLO, HPO, TGEMO, …), theidentifier, the implementing class name, or thedc:title, ignoring case and punctuation. Every ontology is therefore refreshable, including the ones whosedc:titleis absent or contains spaces. 404 if no bean matches, 409 if a refresh is already in flight on that bean.For the slim-CHEBI path the refresh re-runs the
loadModeloverride, which checks the seed-hash sidecar and re-extracts the slim if the corpus has drifted. -
rebuildOntologySlim
@POST @Path("/ontologies/{name}/rebuild-slim") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response rebuildOntologySlim(@PathParam("name") String name) Rebuild the slim-cache OWL for an ontology that supports it (currently CHEBI only). The service must already be loaded so the extractor can read the on-disk source. Returns 202 immediately and the extraction runs on a daemon thread; pollgetOntologies(boolean)to watch the result land (a fresh slim file at${ontology.cache.dir}/ontology/chebiOntology-slim.owl).Memory note: STAR module extraction via OWL-API holds the full CHEBI in heap during the run (~3 GB peak after this commit's source-release fix). Invoke on a host with that headroom, and not during another resource-intensive operation.
-
getObsoleteTerms
@GET @Path("/ontologies/obsolete-terms") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<List<ObsoleteTermUsage>> getObsoleteTerms(@QueryParam("timeoutSeconds") @DefaultValue("120") Integer timeoutSeconds) In-application port ofFindObsoleteTermsCli: which obsolete ontology terms do Gemma's annotations still use, and what does each owning ontology say should replace them.The CLI existed because the check needed ontologies in memory and a CLI had to load them itself — which is why it refuses to run unless
load.ontologies=falseand spends its first stretch warming up. A running application already holds them, so the only work left here is one grouped query over CHARACTERISTIC plus a lookup per distinct URI.Read-only. Correcting the terms is a separate, deliberate action: see
autoCorrectableon each row for whether a correction could be derived from the ontology at all. -
applyObsoleteTermCorrections
@POST @Path("/ontologies/obsolete-terms/apply") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public jakarta.ws.rs.core.Response applyObsoleteTermCorrections(@QueryParam("dryRun") @DefaultValue("true") Boolean dryRun, @QueryParam("uris") List<String> uris, @QueryParam("timeoutSeconds") @DefaultValue("600") Integer timeoutSeconds) Rewrite annotations that use an obsolete ontology term to the successor its ontology asserts.Dry run unless
dryRun=falseis passed explicitly. The default is the safe one because this writes to production annotations, and a dry run returns the counts a live run would produce, so there is no reason to skip the rehearsal.Only
autoCorrectableterms are touched — those whose replacement was derived from the ontology rather than decided by a person. Terms offering onlyoboInOwl:considercandidates are never corrected here; seeGET /admin/ontologies/obsolete-termsfor what they are and why. -
getDbPool
@GET @Path("/db/pool") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response getDbPool()HikariCP pool snapshot. Reports the live connection census plus the configured upper bound, so the admin panel can show "12 / 50 active" at a glance and surface "threads awaiting" when the pool is saturated. -
getCurationAgentHealth
@GET @Path("/curation-agent/health") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public ResponseDataObject<AdminWebService.CurationAgentHealthResponse> getCurationAgentHealth()Out-of-process liveness probe for the gemma-curation-agents Python service. Configured viagemma.curationAgent.healthUrl(unset = endpoint reports "not configured" with 200, so the admin UI can render a neutral pill instead of an alarming red one). -
grabGeoRecords
@POST @Path("/tasks/geo-grab") @Consumes("application/json") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public ResponseDataObject<AdminWebService.GeoGrabResponse> grabGeoRecords(AdminWebService.GeoGrabRequest req) Scrape GEO record metadata by accession without importing into Gemma. Port ofGeoGrabberCli's -e / --acc mode. Synchronous: NCBI E-utilities responses are sub-second per accession in the typical case, so the curation-UI can call this on-demand to preview a GEO record before triggering a full import.Returns one
AdminWebService.GeoRecordValueObjectper requested accession that GEO successfully returns; accessions GEO doesn't know about are silently dropped (matching the CLI's behavior). -
submitGeoScrape
@POST @Deprecated @Path("/tasks/geo-scrape") @Consumes("application/json") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public jakarta.ws.rs.core.Response submitGeoScrape(@Nullable AdminWebService.GeoScrapeRequest body) Deprecated.The curation agent scrapes GEO itself (scrape_geo_and_open_triage.py) and opens its own triage ticket. Still functional; seeGeoScrapeServicefor what an agent-side replacement has to reproduce -- the preboarded rows, the watermark, and ONE batch ticket. -
getLastGeoScrape
@GET @Deprecated @Path("/geo-scrape/last") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.GeoScrapeWatermarkValueObject> getLastGeoScrape()Deprecated.Reads the watermark written by the deprecated in-Gemma scrape. An agent that scrapes on its own side is the author of its own run records; this only ever sees runs Gemma performed. -
getCurationStatus
@GET @Path("/curation-status") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public ResponseDataObject<AdminWebService.CurationStatusResponse> getCurationStatus()Snapshot of the annotation-set -> ticket lifecycle: per-roleAnnotationSetcounts in the recent windows, open-ticket counts byTicketType, distinct agent run id count, and latest-createdAt timestamp.Backs the curation-UI "what's the Python agent doing right now" indicator. Counts are computed with bounded aggregates against the ANNOTATION_SET and TICKET tables — no per-row fetch.
-
getUsers
@GET @Path("/users") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.UsersListResponse> getUsers(@QueryParam("includeDeleted") @DefaultValue("false") boolean includeDeleted) Admin user listing. Soft-deleted users (DELETED_AT IS NOT NULL) are hidden by default; pass?includeDeleted=trueto surface them. -
createUser
@POST @Path("/users") @Consumes("application/json") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response createUser(AdminWebService.CreateUserRequest req) Create a new active user with a server-generated one-time temporary password. The plaintext password is returned in the response body — pass it to the new user out-of-band. It is not stored anywhere recoverable; if lost, an admin must reset it. -
patchUser
@PATCH @Path("/users/{username}") @Consumes("application/json") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.UserValueObject> patchUser(@PathParam("username") String username, AdminWebService.UpdateUserRequest req) Partial update — toggle the enabled flag (lock/unlock) and/or admin role. Other User fields (email, password, name) are not touched by this endpoint; those go through the user-profile flow. -
resetUserPassword
@POST @Path("/users/{username}/password") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public ResponseDataObject<AdminWebService.ResetPasswordResponse> resetUserPassword(@PathParam("username") String username) Administrative password reset — set a user's password to a fresh server-generated one-time temporary password. Does not require the user's current password (this is the recovery path for a locked-out or forgetful user). The plaintext temp password is returned once; pass it to the user out-of-band. The user should then change it via the self-servicePUT /users/me/passwordflow. Leaves the account enabled; distinct from the email-confirmation reset flow. -
deleteUser
@DELETE @Path("/users/{username}") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_ADMIN')") public jakarta.ws.rs.core.Response deleteUser(@PathParam("username") String username) Soft delete — marks the account as deleted, disables it, and preserves the row so dependent references (ACL sids, audit-event authorship FKs) don't dangle. Hard delete is intentionally not exposed via REST. -
addBlacklistEntry
@POST @Path("/blacklist") @Consumes("application/json") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public jakarta.ws.rs.core.Response addBlacklistEntry(@Nullable AdminWebService.BlacklistRequest body) Adds a single blacklist entry. Port of the-accession/-reasonarm ofBlacklistCli.doAuthenticatedWork(): validates the accession, looks up the GEOExternalDatabase, and creates either aBlacklistedPlatform(GPL*) orBlacklistedExperiment(GSE*) row with the supplied reason. -
deleteBlacklistEntry
@DELETE @Path("/blacklist/{accession}") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public jakarta.ws.rs.core.Response deleteBlacklistEntry(@PathParam("accession") String accession) Removes a blacklist entry by its accession. Port of the-accession -undoarm ofBlacklistCli. Returns 204 on success, 404 when the accession is not on the blacklist. -
listBlacklistEntries
@GET @Path("/blacklist") @Produces("application/json") @PreAuthorize("hasAuthority('GROUP_CURATOR')") public ResponseDataObject<AdminWebService.BlacklistListResponse> listBlacklistEntries(@QueryParam("limit") @DefaultValue("100") int limit, @QueryParam("offset") @DefaultValue("0") int offset) Lists current blacklist entries. Convenience sibling for the curation-UI; the CLI has no equivalent. The underlying service exposes onlyloadAll(), so pagination is applied in-process: results are sorted by accession (alphabetic, nulls last) and sliced byoffset/limit.
-