Skip to content

HTTP API

github-actions[bot] edited this page Oct 7, 2026 · 10 revisions

HTTP API contract

This is the current route, access, transport, and error contract. The API is an internal contract between the bundled Labello clients and server. It has no URL version prefix and does not promise compatibility for independent external clients. Coordinate contract changes across labello-client, labello-api, all UI callers, and tests. Route tables follow the router and handler tests; update them with route, role, header, limit, or response changes.

Representation sources

JSON field names and enum wire values are defined by:

  • crates/labello-client/src/dto/ for access, workflow, media, and offline request/response DTOs;
  • crates/labello-client/src/import.rs for import and migration DTOs;
  • crates/labello-client/src/export.rs for export DTOs; and
  • versioned labello-domain types for persisted workflow, dataset, snapshot, and offline artifacts.

Type names in the route tables refer to those definitions. JSON uses each type's Serde contract; callers must not infer field names from Rust identifiers. Binary image/preview and snapshot-file responses are identified explicitly.

Authentication, authorization, and CSRF

The browser authenticates with the HttpOnly session cookie created by local development login or GitHub OAuth. Session below means a valid cookie.

Dataset access is role-based:

  • Any role: at least one role in the dataset.
  • Data admin: the dataset's data_admin role.
  • Assigned actor: the authenticated user owns the exact live assignment; its kind requires annotator or reviewer authority.
  • Bootstrap admin: a server-configured bootstrap administrator. Import additionally limits job access to the creating owner and filters server roots by configured owner.

All unsafe methods (POST, PUT, PATCH, and DELETE) pass CSRF middleware. When a session cookie is present, the request must have exactly one x-csrf-token matching the session. A supplied Origin must match the configured allowlist; native clients may omit it. /auth/local-admin is the middleware exception, but independently requires an allowed Origin. OAuth state and its flow cookie are validated at the callback. The flow cookie is created and expired at the parent path of the validated public OAuth callback URI, including any proxy prefix. See callback configuration for the accepted path syntax. Internal authentication route paths remain unchanged.

CORS allows only configured origins, credentials, the implemented HTTP method set, and these non-simple request headers: content-type, x-csrf-token, idempotency-key, upload-offset, upload-length, and digest.

Common limits and responses

The default body limit is 128 MiB. Import control JSON is limited to 1 MiB, browser file registration JSON to 8 MiB, and an import chunk to the advertised uploadChunkBytes limit. These nested limits replace the default for their routes.

Every response carries x-request-id. A supplied ID is retained only when it is a single, nonempty value of at most 128 ASCII letters, digits, hyphens or underscores. Missing or invalid IDs are replaced with a generated UUID before logging and response propagation. Successful JSON responses use the status selected by the handler, normally 200. Logout returns 204. OAuth endpoints redirect. Binary endpoints return their media type instead of JSON.

Errors have this JSON shape:

{"error":"safe public message"}

The stable status mapping is:

Status Meaning
400 Malformed request, invalid ID/path/assignment, or domain validation failure mapped as bad input
401 Missing/invalid session, CSRF/origin failure, dataset-role denial, create-dataset bootstrap denial, or identity mismatch
403 Authenticated actor lacks bootstrap-administrator authority for import
404 Route-owned resource or storage artifact was not found
409 Existing destination, state/assignment conflict, unavailable import capability, or keybinding conflict
413 Request exceeds the applicable body or storage limit
422 Structurally valid import input cannot be processed
500 Internal, unexpected storage, upstream HTTP, or serialization failure

Internal failures return internal server error; diagnostics remain in redacted logs. Clients must display the x-request-id, not raw internal state.

Session and dataset routes

Method and path Access Input → output
GET /stats Session; each included dataset requires any role No input → DatasetStatistics[], each with dataset identity/name, task/class definitions, imbalance configuration, and unchanged DatasetStats; Cache-Control: no-store
GET /keybindings Session, own account No input → global KeybindingSet, defaults when not saved; Cache-Control: no-store
PUT /keybindings Session, same user; CSRF KeybindingSet → normalized global KeybindingSet
GET /keybindings/legacy Session, own account; any role in source dataset No input → saved LegacyKeybindings[] for explicit migration; empty after a global save; Cache-Control: no-store
GET /presence Authenticated, server-wide No input → active lease holders, optional GitHub logins/account IDs, and their dataset IDs/names; ServerPresence, Cache-Control: no-store
GET /health Public No input → {"ok":true,"service":"labello"}
GET /build-information Public, no session or CSRF token No input → compiled artifact releaseTag and sourceCommit, independently of readiness; Cache-Control: no-store
GET /deployment/readiness Public; production API is loopback-bound No input → bounded release identity, schema version, dataset-root traversal, and authentication-store load state; HTTP 503 when a probe fails
GET /auth/options Public No input → AuthOptions
POST /auth/local-admin Public, allowed origin, feature enabled No body → SessionInfo plus session cookie
GET /auth/github/login Public, OAuth enabled OAuthLoginRequest query → temporary provider redirect plus flow cookie
GET /auth/github/callback Public, valid OAuth state/flow cookie OAuthCallbackRequest query → configured browser redirect plus session cookie
GET /me Session No input → SessionInfo, Cache-Control: no-store
POST /logout Session optional; CSRF when cookie present No body → 204 plus expired session cookie
GET /datasets Session No input → DatasetSummary[] filtered to accessible datasets
POST /datasets Bootstrap admin CreateDatasetRequest → DatasetMetadata
GET /datasets/{dataset_id} Any role No input → role-sanitized DatasetMetadata
GET /datasets/{dataset_id}/users Data admin No input → DatasetUser[]
PUT /datasets/{dataset_id}/roles Data admin SetDatasetRolesRequest → DatasetUser
GET /datasets/{dataset_id}/admin Data admin No input → full configuration DatasetMetadata
PUT /datasets/{dataset_id}/admin Data admin UpdateDatasetConfigRequest → full configuration DatasetMetadata
POST /datasets/{dataset_id}/ingest Data admin No body → IngestReport
POST /datasets/{dataset_id}/ingest-jobs Data admin No body → IngestJob
GET /datasets/{dataset_id}/ingest-jobs/{job_id} Data admin No input → IngestJob for the same dataset
POST /datasets/{dataset_id}/uploads Data admin multipart/form-data, root and ingest query → IngestReport
GET /datasets/{dataset_id}/snapshots Data admin No input → DatasetSnapshot[]
POST /datasets/{dataset_id}/snapshots Data admin No body → DatasetSnapshot
GET /datasets/{dataset_id}/snapshots/{snapshot_id}/files/{*file_path} Data admin No input → attachment bytes listed by the snapshot manifest
GET /datasets/{dataset_id}/tasks Any role No input → TaskDefinition[]
POST /datasets/{dataset_id}/tasks Data admin TaskDefinition → TaskDefinition
GET /datasets/{dataset_id}/prelabels Any role No input → PrelabelConfig[]
POST /datasets/{dataset_id}/prelabels Data admin PrelabelConfig → PrelabelConfig
GET /datasets/{dataset_id}/images Any role ImageExplorerQuery → ImageExplorerPage
GET /datasets/{dataset_id}/stats Any role No input → DatasetStats
GET /datasets/{dataset_id}/stats/me Any role No user selector → CurrentUserActivity for the authenticated account and server UTC day; Cache-Control: no-store
GET /datasets/{dataset_id}/keybindings Any role Compatibility alias for the authenticated user's global KeybindingSet
PUT /datasets/{dataset_id}/keybindings Any role, same user Compatibility alias saving the global KeybindingSet
POST /datasets/{dataset_id}/prelabel-model-check Data admin Unsaved { location } managed filename → PrelabelModelInspection; control JSON limited to 4 KiB; no inference image, configuration save, or hint mutation
GET /datasets/{dataset_id}/prelabel-management Data admin No input → PrelabelAdminState
POST /datasets/{dataset_id}/prelabel-management Data admin Preflight/start/cancel/retry/reset/resume command → PrelabelAdminState

KeybindingSet.bindings contains the primary chord for every active action. panDragModifier selects the modifier used with primary-button drag to pan and defaults to control. Middle-button drag is always available. Older requests that omit panDragModifier remain valid and receive the default during deserialization.

GET /stats enumerates managed datasets and applies the same role check as the individual statistics route. Bootstrap-admin status alone grants no statistics access. It returns a complete authorized set or an error; unreadable dataset configuration or statistics cannot silently reduce the total. This is a sequential read of dataset projections, not a transactionally synchronized server snapshot. The client aggregates additive totals and UTC contributor days by stable user ID. Task/class breakdowns, assignment balance, daily scoring multipliers and focus remain dataset-scoped. Images present in two datasets count in both.

Global shortcut saving needs no dataset membership. Before the first global save, the legacy route lists only the user's saved bindings in currently authorized datasets. The UI offers an explicit source selection or saving new settings; GET never chooses one dataset's preferences or overwrites old files. Supported legacy records normalize through the existing storage migration. Once saved, the global value wins on every route; see persistence.

Optional DatasetStats.contributors maps user IDs to displayName, optional githubUserId for avatars, and chronological UTC history rows containing day, labeled, reviewed, accepted, and rejected. An empty map means no activity; an absent field means contributor statistics are unavailable.

DatasetStats.scoringVersion = 1 advertises contribution scoring. Each history row adds a defaultable score object: labels is a count, and labeling, reviewing, deductions, and corrections are integer hundredths of a point. Net points are labeling + reviewing + corrections - deductions. Optional scoringFocus contains startsAt, endsAt, and nullable taskId; an expired selection must not be advertised as active. Missing scoring support is distinct from zero points. Policy and historical credit are defined in Contribution scoring. These fields use the existing authenticated, dataset-authorized statistics endpoint.

Counts derive from existing events: labeling counts completed Objects and Overview items per person, image and task. A whole skeleton is one item. Legacy image/task submissions occupy the Overview slot, so compatibility events and later receipts cannot double count them. Item reviews count once per person, image, task and item; legacy reviews retain distinct-decision counting. Partial saves, Skip, retries and unchanged history reconfirmations add no credit. Accepted/rejected counts belong to the reviewed human work. Corrections count as rejections; imports and automatic work earn no human credit. Activity uses the submission/review date; revised decisions count on commit and retain earlier activity. Clients calculate periods, ranks, and acceptance percentages. Existing response fields, authorization, and persisted formats remain unchanged.

Clients derive streaks from contributor labeling and review history.

Role mutation retains bootstrap-administrator protections implemented by the handler; a data administrator cannot use this route to bypass those rules.

DatasetMetadata.imbalance and UpdateDatasetConfigRequest.imbalance use the direct maxDifference representation documented in Server configuration. The administration update route rejects ratio and tagged-policy shapes. Assignment statistics include the current annotation and review counts plus the task IDs blocked by the enforced window.

DatasetMetadata.preloadQueueSize and the administration update field of the same name are integers from 1 through 200. Missing values default to two. The existing DataAdmin authorization applies; invalid ranges return 400.

Creating a dataset from an existing schema

POST /datasets accepts optional schemaSourceDatasetId. Omitted or null creates empty classes and tasks. A source requires DataAdmin access in addition to the existing bootstrap-admin creation permission. The server reloads its current configuration and validates the copied definitions before initializing the destination. Missing, unauthorized, or invalid sources fail creation; they never fall back to an empty schema.

The copy preserves class and task IDs, names, class relationships, workflow settings, instructions text, and skeleton definitions. Manual box-guide migration configuration is cleared so copied skeleton tasks support ordinary annotation and their configured review flow without imported guide boxes or target sets. Tutorial example-image references and prelabel bindings are also cleared. Images, annotation/workflow history, source roles, ingestion paths, prelabel model configuration, and dataset balance settings are not copied. The destination keeps normal fresh-dataset identity, defaults, and initial administrator roles. The source and destination are independent after creation.

Public build information

GET /build-information returns only releaseTag and sourceCommit. Each is nullable: absent or invalid compiled metadata is null, never the shared Cargo package version. Tags contain at most 64 ASCII letters, digits, dots, underscores, plus signs or hyphens; commits contain exactly 40 or 64 hexadecimal characters. The reserved tag development represents missing release metadata and is exposed as null. The response is under 200 bytes, performs no persistence or authentication probe, and remains HTTP 200 when /deployment/readiness returns 503. Existing credentialed CORS rules also apply to this public route.

The client uses a ten-second request timeout, validates the two-field DTO and rejects responses over 1024 bytes. The response must not expose readiness, configuration, authentication state, paths, or other server details.

Server presence

Authenticated GET /presence returns ServerPresence with Cache-Control: no-store. Each PresentUser contains a stable internal userId, optional githubLogin and githubUserId from the stored account, and the IDs/names of datasets where that user holds an unexpired active annotation or review lease. githubUserId is optional public avatar metadata; clients must not derive it from the internal ID. The header displays GitHub profile photos, with initials while photos are missing, loading or unavailable. Details use @githubLogin when nonempty, otherwise userId; display names do not override GitHub handles. Older responses without these optional fields retain the same ID detail fallback and show initials instead of a photo. Migration uses those same assignment kinds. The requesting user is included under the same lease rules, even when they are the only active user. Users are deduplicated and sorted by internal ID, with datasets sorted by ID. Presentation metadata never changes identity or assignment ownership.

This endpoint deliberately exposes active usernames and dataset names across server dataset-role boundaries to authenticated users. It grants no dataset access and returns no assignment IDs, image identities, geometry or session information. Query parameters cannot select a different requesting user. Unauthenticated requests fail. An unreadable registered dataset fails the request instead of reporting a potentially false empty presence list.

Presence reads never claim, renew or release leases. Closing a browser does not release its leases automatically; the existing lease-expiry policy applies.

Current-user daily activity

CurrentUserActivity identifies datasetId, authenticated userId, the inclusive UTC window.start, exclusive window.end, server sampledAt, and two integer counts: annotationTasksSubmitted and finalTaskReviews. The route has no client-selected user or time window. It requires a current dataset role and returns a fixed-size aggregate, never another user's history. A scan crossing midnight retries once for the new day, then returns a retryable error if the window changes again. The HTTP client uses the existing 20-second statistics timeout and credentialed session.

Annotation activity counts committed normal or guided-migration submissions, including completion with no review stage. Review activity counts committed final task or migration-confirmation decisions, approved or rejected. Each counter deduplicates dataset/image/task/user within that UTC day. Saves, imports, skips and object decisions do not count. A committed correction submission counts its final rejection, once per user/image/task/day. Historical immediate reviewer corrections alone do not count. Reopening, later rejection and superseding a decision do not retract historical activity. Same-day replacement commits count once; a later-day submission or final-review replacement counts on its new commit day. Compound review revisions use the event's server commit time, not timestamps of locally staged review records. Dataset-wide DatasetStats semantics remain unchanged.

Review configuration and task statistics

Current task review configuration contains workflow, either none or approval, and allowReviewerCorrections. Approval requires one reviewer. Historical-only configuration fields, roles, task states and assignment kinds cannot be introduced through current mutation endpoints.

DatasetStats reports pendingTasks, inProgressTasks, awaitingReviewTasks, needsCorrectionTasks, and completedTasks. Each perTask entry uses pending, inProgress, awaitingReview, needsCorrection, and completed, plus its existing provenance and migration statistics. Each eligible image/task occupies one state count. The task table and summary use this same contract. Audit review records and contributor activity remain available through their existing APIs.

Saved workflow reasons

GET /datasets/{dataset_id}/images/{image_id}/reasons requires a session and any role in that dataset, matching image-state access. It returns WorkflowReasonEntry[] projected from one authoritative event-log snapshot. Each entry identifies the image, event sequence/ID, actor, timestamp, action, optional workflow and object identities, optional text and structured exclusion category. Optional reviewDecision preserves the recorded decision for review and revised-review comments; absent values remain neutral comments rather than inferred rejections. The existing reason fields remain at the top level. Each entry additionally has an optional author with githubLogin and githubUserId, resolved from current server account data for that event's actor. Missing accounts produce null; local accounts can have null GitHub fields. No account timestamps, display names, roles, or unrelated accounts are included. Older responses without author remain readable. This read-only transport metadata changes no persisted event. currentRound, superseded and currentExclusion describe that snapshot's relevance. Historical entries remain ordered by source event, with copied exclusion sources and the legacy correction's identical review comment deduplicated. Blank text and known internal annotation markers are omitted. The route does not claim assignments, mutate history, or expose annotation geometry. Missing images return 404; role and authentication failures follow the normal image-route rules.

The browser loads this resource under the assignment request's existing stale response gate. A failed request fails the image load and remains retryable; it does not masquerade as an empty reason history. No persisted event or state shape changes. Correction explanations for several objects use explicit object labels inside the existing optional submission reason and retain its 2000-byte limit.

Assignment and image routes

Method and path Access Input → output
POST /datasets/{dataset_id}/work-items/claim Annotator or reviewer for requested kind ClaimWorkflowRequest { selection: { taskId, kind, variant }, prefetch, excluded } → Assignment?; variant is objects or overview
GET /datasets/{dataset_id}/work-items/availability Requested kind authorized by storage kind query → WorkflowAvailability[], including each variant's split flag and blocking reason
POST /datasets/{dataset_id}/work-items/display Exact active item owner AssignmentActionRequest → ImageState; records durable seen state and renews the lease
POST /datasets/{dataset_id}/work-items/draft Exact active displayed item owner SaveWorkflowDraftRequest { assignment, geometry, expectedSequence }, or { assignment, edits: { changes, reason }, expectedSequence } → ImageState; geometry is an annotation Objects draft, edits are annotation Overview or reviewer proposals
GET /datasets/{dataset_id}/work-items/history Requested kind authorized by storage taskId, kind, variant query → newest-first WorkflowHistoryEntry[] within the configured window
POST /datasets/{dataset_id}/work-items/reopen Owner of an eligible history visit AssignmentActionRequest → renewed or reacquired Assignment
POST /datasets/{dataset_id}/work-items/leave Requested kind authorized by storage WorkflowSelection → success; releases the caller's matching reservations
GET /datasets/{dataset_id}/assignments/availability Session; requested kind authorized by storage AssignmentAvailabilityRequest query → AssignmentAvailability
POST /datasets/{dataset_id}/images/next Session; requested kind authorized by storage AssignNextRequest → Assignment?
POST /datasets/{dataset_id}/images/{image_id}/assignments/revalidate Owner of exact active assignment AssignmentActionRequest → AssignmentRevalidation?
POST /datasets/{dataset_id}/assignments/release Assigned actor AssignmentActionRequest → Assignment
POST /datasets/{dataset_id}/assignments/complete Assigned annotator AssignmentActionRequest → Assignment
POST /datasets/{dataset_id}/assignments/reopen Owner of exact prior annotation or eligible review assignment AssignmentActionRequest → Assignment
GET /datasets/{dataset_id}/images/{image_id} Any role No input → ImageState
GET /datasets/{dataset_id}/images/{image_id}/review-submitters Any role No input → ReviewSubmitter[]: image/task/user IDs and optional public GitHub login/account ID for current review-round or migration-confirmation authors; no private account fields or history mutation
GET /datasets/{dataset_id}/images/{image_id}/reasons Any role No input → WorkflowReasonEntry[]; see saved reasons
POST /datasets/{dataset_id}/images/{image_id}/return-to-review Reviewer or data admin ReturnToReviewRequest → ImageState; exact sequence, atomic selected workflows
GET /datasets/{dataset_id}/images/{image_id}/record Any role No input → ImageRecord
GET /datasets/{dataset_id}/images/{image_id}/file Any role No input → original image bytes and stored media type
GET /datasets/{dataset_id}/images/{image_id}/preview Any role max query clamped to 256–4096 → raw RGBA bytes (application/octet-stream) plus x-image-width and x-image-height; bounded legacy fallback
GET /datasets/{dataset_id}/images/{image_id}/encoded-preview Any role profile=standard_v1 (default), data_saver_v1 or thumbnail_v1 → bounded image/webp, x-image-width, x-image-height, x-original-width, x-original-height, x-preview-profile
GET /datasets/{dataset_id}/images/{image_id}/detail Any role Explicit bounded original-detail display; original encoded bytes and decoder-format MIME, private, no-store
POST /datasets/{dataset_id}/images/{image_id}/events Assigned annotator; role also derived from allowed payload AssignmentActionRequest query plus AppendEventRequest → EventLogEntry
POST /datasets/{dataset_id}/images/{image_id}/annotation-batch Assigned annotator AssignmentActionRequest query plus AnnotationBatchRequest → ImageState
POST /datasets/{dataset_id}/images/{image_id}/admin/events Data admin AppendEventRequest with permitted repair payload → EventLogEntry
POST /datasets/{dataset_id}/images/{image_id}/rebuild Any role No body → replayed ImageState
POST /datasets/{dataset_id}/images/{image_id}/reviews Assigned reviewer AssignmentActionRequest query plus ReviewRecord → ImageState
POST /datasets/{dataset_id}/images/{image_id}/missing-object-rejections Reviewer; retired command Returns 400; historical evidence remains readable
POST /datasets/{dataset_id}/images/{image_id}/review-revisions Owner of active decision-revision lease; reviewer role AssignmentActionRequest query plus ReviewRevisionCommit → ImageState
POST /datasets/{dataset_id}/images/{image_id}/review-corrections Assigned reviewer, including revision owner AssignmentActionRequest query plus ReviewCorrectionSubmission → ImageState
POST /datasets/{dataset_id}/images/{image_id}/corrections Assigned reviewer AssignmentActionRequest query plus CorrectionRequest → EventLogEntry
GET /datasets/{dataset_id}/offline-bundle Annotator OfflineBundleRequest query → OfflineBundle
POST /datasets/{dataset_id}/offline-sync Annotator; same authenticated user and dataset versioned OfflineSyncRequest → OfflineSyncResult

Work-item geometry and edit drafts are provisional; neither confirms work nor awards points. Edit proposals contain at most 10000 typed changes and 2000 bytes of reason. The sequence is compare-and-swap across reassignments; an exact retry is idempotent. Item-aware annotation batches and review/correction commands enforce the captured object or Overview scope. Raw event ingress cannot manufacture workflow events.

Per-user inference, retained-hint discovery, generation polling, browser-result certification and model-byte routes have been removed. Their paths return 404. Administrator configuration and generation routes remain available. New/changed task bindings permit at most one prelabel configuration; unchanged legacy lists remain readable. Preparation is server-owned and includes both boxes and poses.

The assignment ID, image ID, task ID, actor, kind, current sequence, and live state are validated at the transaction boundary. Possessing an ID is not authorization.

Availability and direct claims apply the current-count completion-balance decision. AssignNextRequest.prefetch, default false, adds prospective balance admission for upcoming work. It cannot be combined with assignmentId reclaim. excludedImageIds accepts at most 202 distinct validated IDs, covering the maximum queue, current image, and one skipped image. An imbalance-limited prefetch returns {"reason":"imbalance_limit"} with status 200 before the client fetches image resources. Other unavailable work still returns null; a successful claim retains the assignment-object response. The reason comes from the claim's admission decision, without another balance request.

Availability responses include queue.size and queue.eligibleAssignments. The latter contains the caller's live, eligible reservation IDs for the requested kind, filtered by projected balance when enforcement is enabled. Clients also accept null from older or demonstration adapters as an omitted reservation filter. It is advisory and never renews leases. Clients must not apply an older response to reservations created after that request. The complete count, denominator, disabled-peer, zero-count, and exact-boundary contract is maintained in Assignment.

Review reopening and replacement follow the strict queue history contract. ReviewRevisionCommit contains reviews, from 1 to 10001 ReviewRecord values. Each record must belong to the caller, have a unique ID and captured exact target, and contain at most 2000 bytes of comment. The final record is the task or migration-confirmation decision. Approval must include every captured target without a rejected object. Foreign actors or missing reviewer authority return 401. Invalid syntax or assignment-kind input returns 400; changed context, expired ownership, malformed replacement targets, and conflicting retries return 409. Ordinary review commands cannot mutate a task held by an exclusive revision lease. Its owner may submit corrections through review-corrections.

Workflow, ReviewAssignmentOpened, ReviewAssignmentFinished, and ReviewRevisionCommitted, ReviewCorrectionSubmitted, and MissingObjectEvidenceRecorded are server-owned events. Raw event, annotation batch, admin repair, and offline sync ingress cannot publish them. Clients submit commands to the dedicated endpoints and never choose superseded review IDs.

ReviewCorrectionSubmission contains correctionId, captured round, targetFingerprint, 1–10000 changes, and optional reason (at most 2000 bytes). Changes are tagged edit, add, remove, or migration_object and use snake-case variant fields: exact annotation_id/expected_version, new class_id and geometry, or object_group_id/expected_disposition_version with a replacement (skeleton or exclude). The server supplies reviewer attribution and provenance. It rejects empty or unchanged corrections, invalid geometry and identifiers, stale targets, foreign ownership, and conflicting retries. The full batch, rejection of the old round, assignment completion and fresh submitted round are atomic. An identical retry does not append events. Saving does not finalize work.

corrections is a compatibility adapter for one geometry edit; it now returns the ReviewCorrectionSubmitted receipt event and has the same fresh-round semantics. The allowReviewerCorrections flag no longer disables corrections. Ordinary, migration and revision commands reject decision-only rejection. Final approval requires approval of every current object by this reviewer.

missing-object-rejections is retired and rejects writes. Nonempty ReviewRevisionCommit.missingObjects is rejected as well. Historical evidence remains in ImageState, snapshots and event replay. Raw event and admin repair commands cannot bypass substantive-correction requirements with ReviewRecorded rejections.

Assignment availability reasons

The workflows entries cover the requested kind and every authorized related kind, so cached view changes retain Objects/Overview eligibility. Each entry includes its kind in selection.

AssignmentAvailability and each authorized related entry retain their tasks map of task IDs to booleans. An additive reasons map contains a bounded reason code for each unavailable task and no entries for available tasks. Older responses without this map remain readable; missing or unknown codes display a generic unavailable explanation. The codes are balance_limit, review_disabled, empty_dataset, annotation_finished, nothing_awaiting_review, claimed_by_others, review_revision, import_excluded, review_finalized, and unavailable.

Task-level configuration and balance restrictions take precedence. Otherwise, any eligible image makes the task available. A specific image-level reason is reported only when all images have the same first restriction under the claim policy; mixed results use unavailable. Submitted and completed work needs no further annotation. Completed review work uses review_finalized; pending, in-progress and correction work uses nothing_awaiting_review. Availability is advisory and does not reserve work. Reasons share the same role checks, bounded scan, cache lifetime, and mutation invalidation as the boolean result. No actor, image, annotation, or import-source details are exposed by these codes.

Dataset inspection and return to review

All dataset members may list indexed images and read their previews and current annotations, including completed images outside their assignment queue. Search, workflow, class and workflow-status predicates apply before pagination. With no workflow/class/status filter, only the requested page loads annotation state; search and ordering use the image index. State-dependent filters evaluate rebuildable, process-local per-image summaries before selecting the page. A cold summary loads authoritative image state once; subsequent queries reuse it until that image changes. Configuration defaults and image records come from the current request, not cached workflow definitions. A workflow filter identifies configured workflow status, including Pending on unannotated images; selecting a workflow alone need not reduce the image count. With All workflows, Completed requires every currently configured workflow (including disabled workflows) to be Completed, and a dataset without workflows matches no completed images. Historical workflow IDs do not affect this check. With a specific workflow selected, Completed checks only that workflow. Other status filters match any workflow when no workflow is selected. Browsing and changing overlay visibility require no assignment and append no events.

POST /datasets/{dataset_id}/images/{image_id}/return-to-review requires a reviewer or data-admin role and the normal CSRF checks. Its JSON body is ReturnToReviewRequest: requestId, expectedSequence, taskIds and reason. Select 1–100 distinct configured workflows and supply a nonblank reason of at most 2000 UTF-8 bytes. The server requires an exact image sequence and enabled, completed approval workflows without an unexpired assignment. It rejects the entire selection if any workflow is ineligible. Active leases are not stolen.

Success returns the replayed ImageState with the selected workflows submitted for fresh review rounds. Geometry and previous decisions remain unchanged. Stale state, active work, ineligible workflows and conflicting request-ID reuse return 409. An identical request by the same actor is idempotent across restart and later work; it returns current state without appending another event. Request identity is scoped to the dataset image. Other roles cannot mutate through this route, and ordinary event ingress cannot forge its server event.

Encoded working previews

Standard v1 resizes to at most 1600 pixels on the longest edge and encodes lossless WebP. Data Saver v1 uses at most 1280 pixels and lossy WebP quality 80 (on libwebp's 0–100 scale). Thumbnail v1 uses a 256-pixel maximum edge and the same lossy quality for gallery proxies. No profile upscales. All preserve the existing Triangle resize at the decoder's native channel depth followed by RGBA8 conversion, first-frame behavior, and no EXIF orientation or ICC conversion. Standard decoded RGBA, including transparent RGB, is identical to the legacy 1600 preview. Original record dimensions remain authoritative for geometry.

Both routes authenticate and authorize each request, including cache hits, and recheck session, roles, and image index after worker completion. Source content is verified against its authoritative BLAKE3 hash before every cache read. Responses use Cache-Control: private, no-store; encoded dimension and profile headers are exposed by credentialed CORS. Encoded responses are at most 16 MiB. Limits are configured under previews. Oversized source/pixel/decoder requests return 413; full preview admission queues, cache ownership conflicts, exhausted cache quota, or stale source identity return 409; unsupported/unavailable sources and decoding failures return 422; unavailable cache/encoder failures return 500. Errors never include source paths or decoder text.

ImageApi::get_encoded_image_preview returns EncodedImagePreview, separate from ImagePreview::rgba. HTTP clients bound streaming response bytes and validate MIME/profile/metadata and check encoded WebP dimensions before pixel allocation. Native clients use the bounded Rust WebP decoder. The browser UI uses asynchronous createImageBitmap decoding and an offscreen canvas, checks the decoded dimensions and RGBA length, and closes the bitmap after copying its pixels. This keeps prefetch decoding from blocking browser input. Browser decode errors use bounded messages without decoder details. The UI always requests Data Saver v1 for working-image loads, reloads, retries and prefetch. Generation, transfer and decode errors propagate without requesting Standard, legacy RGBA or original bytes. The encoded route retains its Standard v1 default for API callers that omit the profile. Image assignment, annotation geometry and draft state are independent of the representation. Existing request/auth/workspace epochs reject stale image replies and clear account-scoped texture/prefetch state.

Explicit original detail

ImageApi::get_original_detail and GET .../detail remain explicit API capabilities; the working UI does not call them or expose an original-detail action. Ordinary original-file download keeps its existing separate contract. Detail reads share preview source-byte, pixel, decoder-header/allocation and worker limits, secure source opening/hash verification, and final session/role/index checks. They return original source bytes without a derived cache entry. The client bounds streaming bytes to 64 MiB and decoding to 32 million original pixels / 256 MiB decoder allocation, checks declared MIME and authoritative original dimensions, and applies no EXIF/ICC transform. Larger server configuration limits do not raise these browser detail limits.

Every working-image load and prefetch uses the encoded Data Saver v1 route. Saved browser quality preferences are ignored; preview failure never chooses original detail. Aborting superseded image transfers does not abort assignment claims: their replies must still be received so obsolete reservations can be released. Server workers already started retain their configured bounds through completion/cleanup.

Manual migration routes

Every route requires a valid session, exact owned assignment, the role shown, and exactly one idempotency-key containing 1–200 visible ASCII characters other than comma or semicolon. Responses are ManualMigrationCommandResult.

Method and path Role Request
POST /datasets/{dataset_id}/images/{image_id}/migration/skeleton Annotator SaveMigrationSkeletonRequest
POST /datasets/{dataset_id}/images/{image_id}/migration/skeletons Annotator AddMigrationSkeletonRequest
POST /datasets/{dataset_id}/images/{image_id}/migration/skeletons/edit Annotator EditMigrationSkeletonRequest
POST /datasets/{dataset_id}/images/{image_id}/migration/skeletons/delete Annotator DeleteMigrationSkeletonRequest
POST /datasets/{dataset_id}/images/{image_id}/migration/skeletons/reconcile Annotator ReconcileMigrationCompanionRequest
POST /datasets/{dataset_id}/images/{image_id}/migration/exclude Annotator ExcludeMigrationTargetRequest
POST /datasets/{dataset_id}/images/{image_id}/migration/reopen Annotator ReopenMigrationTargetRequest
POST /datasets/{dataset_id}/images/{image_id}/migration/revisit Annotator RevisitMigrationTargetRequest
POST /datasets/{dataset_id}/images/{image_id}/migration/passes Annotator StartMigrationPassRequest
POST /datasets/{dataset_id}/images/{image_id}/migration/keep Annotator KeepMigrationTargetRequest
POST /datasets/{dataset_id}/images/{image_id}/migration/confirm Annotator ConfirmMigrationRequest
POST /datasets/{dataset_id}/images/{image_id}/migration/review Reviewer ReviewMigrationRequest

Adding, editing, or removing a missing-object skeleton is allowed only at the full-image cursor. Edit and delete requests identify an active, native, group-less skeleton for the selected migration task and supply its exact expected version; stale or non-migration annotations are rejected.

New save, add, and edit requests must contain at least one visible or hidden keypoint with coordinates. A skeleton containing only absent keypoints returns HTTP 400 with the safe invalid-assignment message manual migration skeleton requires at least one positioned keypoint. Idempotent replay is checked first, so an identical retry of a successfully recorded historical all-absent command still replays without appending events.

Each newly discovered skeleton is saved with one ordinary bounding box in the configured guide task, in the same per-image event transaction. The box bounds include positioned visible and hidden keypoints and span at least 15% of each image dimension, retaining a one-original-pixel floor per axis for tiny images. Expansion is centered on the keypoint bounds and shifted inside the image at edges and corners. This derivation rule also applies to automatic regeneration and explicit reconciliation, not to independent human box edits. Existing persisted geometry is not resized when loaded or replayed. The server chooses its task, class and stable identity; clients cannot supply another guide task. The guide task enters needs_correction with its terminal outcome cleared. Existing history and review records remain.

Still-derived companions follow skeleton edits and deletion. An independent box edit or review, active guide-task assignment, stale version, or changed guide configuration rejects the whole operation. Explicit reconciliation requires the same active migration annotation assignment and full-image cursor. Its request contains assignmentId, passId, taskId, annotationId, expectedVersion, and nullable expectedBoxVersion. A missing box uses null; regenerating an existing box requires its exact current version. Reconciliation creates or regenerates one box from the saved skeleton and returns the ordinary migration command result. Identical command retries do not append or duplicate objects.

Historical reconciliation requires an active group-less native skeleton with a recorded full-image discovery creation event. Ordinary skeletons without that provenance, coordinate-less skeletons, inconsistent links and competing work remain unresolved with an actionable conflict. Each committed link is a durable unit of progress: after interruption, inspect current links and reconcile the remaining objects. Reads never perform reconciliation.

Migration review defaults to canonical dispositions, then every active discovered skeleton in annotation-ID order, then the full-image confirmation. Individual items may be approved in any order under the exact active review assignment, binding their current disposition or skeleton version. This permits approval of a later unchanged item while an earlier correction remains local until overview submission. Final confirmation still requires this reviewer to have approved every current item in the current submission round. A discovered review target is { "targetType": "discovered", "annotationId": "…", "version": 1 }; it binds the exact current skeleton version. A rejected discovery must receive a new version or be removed before submission. Companion boxes retain the ordinary guide-task review flow and are never approved by migration review.

Dataset import routes

All import routes require a bootstrap-administrator session. Job routes also require ownership of the import. A missing/disabled import service returns 409. Mutating routes marked "Idempotent" require exactly one idempotency-key with the same syntax as migration routes. Reusing a key for a different operation or request is a conflict; a completed identical request replays its response.

Method and path Additional contract Input → output
GET /import-capabilities Authorized roots are filtered per actor No input → ImportCapabilities
POST /import-roots/{root_id}/browse Root must be configured for actor BrowseServerImportRootRequest → ImportBrowsePage
GET /imports Owned jobs only No input → ImportJob[]
POST /imports Idempotent CreateImportRequest → ImportJob
GET /imports/{import_id} Owned job No input → ImportJob
POST /imports/{import_id}/files/register Idempotent; 8 MiB body RegisterImportFilesRequest → RegisterImportFilesResult
POST /imports/{import_id}/files/{file_id}/chunks Idempotent; advertised chunk limit Raw bytes plus upload-offset, upload-length, and full BLAKE3 digest headers → ImportChunkResult
POST /imports/{import_id}/source/browse Owned job BrowseImportSourceRequest → ImportBrowsePage
POST /imports/{import_id}/yolo-descriptor/inspect Owned job InspectYoloDescriptorRequest → YoloDescriptorInspection
POST /imports/{import_id}/seal Idempotent SealImportRequest → SealImportResult
POST /imports/{import_id}/preflight Idempotent StartImportPreflightRequest → ImportJob
GET /imports/{import_id}/plan Current accepted/preflight plan required No input → ImportPlan
PUT /imports/{import_id}/plan Idempotent UpdateImportPlanRequest → ImportPlan
GET /imports/{import_id}/diagnostics Limit bounded by capabilities ImportDiagnosticsQuery → ImportDiagnosticsPage
POST /imports/{import_id}/commit Idempotent; reauthorizes every attempt CommitImportRequest → CommitImportResult
POST /imports/{import_id}/cancel Idempotent; not committing/succeeded CancelImportRequest → CancelImportResult

Dataset export routes

Every route requires a current authenticated dataset DataAdmin. POST requests retain the session CSRF requirement. Control JSON is limited to 1 MiB. Capabilities report availability and limits; unavailable operations return 409. Jobs are private to their dataset, and current role checks apply to retained jobs too. Transport DTOs live in labello-client/src/export.rs.

Route Request and response
GET /datasets/{dataset_id}/exports/capabilities ExportCapabilities
GET /datasets/{dataset_id}/exports Retained ExportJob[]
POST /datasets/{dataset_id}/exports ExportOptions → capturing ExportJob
GET /datasets/{dataset_id}/exports/{job_id} Current ExportJob
POST /datasets/{dataset_id}/exports/{job_id}/start Ready job → building job
POST /datasets/{dataset_id}/exports/{job_id}/cancel Cancellation state
GET /datasets/{dataset_id}/exports/{job_id}/download Verified streamed ZIP attachment
HEAD /datasets/{dataset_id}/exports/{job_id}/download Same authorization and integrity checks, without a body

Options include profile, task/class identities, a nonempty splits filter, a required fallback split, and optional per-image split choices. Omitting splits preserves the previous all-splits behavior. Capabilities advertise absent size quotas as null; explicitly configured numeric limits remain supported. Completed downloads send application/zip, a safe generated filename, content length, private, no-store, and nosniff. Download checks authorization again after checksum I/O. The browser client uses HEAD before returning the attachment URL and does not buffer the archive.

Missing jobs return 404; busy workers, source changes, and invalid lifecycle operations return 409; limits return 413; incompatible selections and invalid source geometry return 422; storage and verification failures return a safe 500 category. Error messages exclude source paths and geometry. Preflight blockers and omissions are recorded in the job summary; see export.

Prelabel evidence

SessionInfo from GET /me and POST /auth/local-admin includes prelabelAvailable, true only when the server has a configured prelabel service. This capability applies to server and browser execution. The UI reads it before showing prelabel controls or making hint requests; false is a normal disabled state. Clients default an absent field to false. Session responses remain no-store; reload the web app after changing server configuration. Dataset-role and CSRF checks still apply to prelabel operations.

All unsafe prelabel requests use the normal session/CSRF/origin checks. PrelabelResponse includes generation, raw validated candidates, execution mode, prepared-result status, and an optional signed browser grant. The shared client filters candidates against its current annotation draft before display. AnnotationBatchRequest.prelabelAcceptances maps new annotation IDs to exact signed evidence. The server verifies it, constructs immutable prelabel origin, and writes a human accepted/edited revision through the existing assignment transaction. Raw client provenance cannot substitute for evidence. Historical prelabel revision sources remain readable. See Model prelabels for binding, reset, trust, resource limits, and failure behavior.

Bounding-box visibility configuration

Dataset metadata and UpdateDatasetConfigRequest include boundingBoxVisibility: { "iouThreshold": 0.9 }, defaulting to 0.9 when omitted. The existing data-admin configuration route owns updates. Values must be finite and within [0, 1]; invalid JSON values fail extraction. Image state responses retain all annotation records and carry the current visibility policy so shared clients can filter consistently. The policy does not authorize mutation of hidden records or bypass assignment ownership. See configuration and workflow policy.

Clone this wiki locally