Merge branch 'feat/restore-background-job' into release/v1.39.1

This commit is contained in:
Marc-André Bombeck committed 2026-09-25 08:10:06 +02:00
commit 934131ee3e
52 files changed
+5684 -2439

No files matched your search

+322 -83
View File
@@ -20564,12 +20564,14 @@ paths:
post:
tags:
- Admin
summary: Restore one stored backup over its owner's record
description: "Replaces the snapshot owner's data tables from the stored copy, in one transaction, under the ids the file
carries. Replacing is not merging: every row the account gained after the snapshot was taken is deleted with the
rest of its class. The target is the account the snapshot was taken for, never the admin running it, and a
payload declaring a different owner than the stored row is refused. Admin session cookie required; Bearer tokens
cannot reach admin endpoints."
summary: Queue the restore of one stored backup over its owner's record
description: "Queues a restore that replaces the snapshot owner's data tables from the stored copy, in one transaction,
under the ids the file carries, and answers 202 with the job's id. Replacing is not merging: every row the
account gained after the snapshot was taken is deleted with the rest of its class. The target is the account the
snapshot was taken for, never the admin running it. The request refuses a missing confirmation, an unknown
backup and a copy that cannot be decrypted; the file's own checks (schema, declared owner, manifest, documents)
run in the job before anything is deleted, and a refusal there ends the job as `failed` with the same reason.
One restore per account at a time. Admin session cookie required; Bearer tokens cannot reach admin endpoints."
parameters:
- in: path
name: id
@@ -20586,12 +20588,12 @@ paths:
schema:
$ref: "#/components/schemas/AdminBackupRestoreRequest"
responses:
"200":
description: The restore ran.
"202":
description: The restore is queued. Its progress and outcome are at `statusUrl`.
content:
application/json:
schema:
$ref: "#/components/schemas/AdminBackupRestoreResponse"
$ref: "#/components/schemas/AdminBackupRestoreQueuedResponse"
"400":
description: Body was not JSON.
content:
@@ -20601,8 +20603,9 @@ paths:
"403": *a50
"404": *a51
"409":
description: Either the payload declares a different owner than the stored row, or a request under the same
`Idempotency-Key` is still in flight. Nothing was changed.
description: A restore of the same account is already queued or running (`meta.errorCode` = `backup.restore.active`,
`meta.jobId` names it), or a request under the same `Idempotency-Key` is still in flight. Nothing was
changed.
content:
application/json:
schema:
@@ -20614,10 +20617,59 @@ paths:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"422":
description: "The request or the file was refused before anything was written: `confirm` missing, the copy undecryptable
(`meta.errorCode` = `backup.payload.undecryptable`), the payload failing schema validation, an unsupported
`schemaVersion`, a section the file's own manifest says it carries and does not (`meta.errorCode` =
`backup.section.missing`), a metadata-only document entry, or an owner who no longer exists here."
description: "The request was refused before anything was queued: `confirm` missing, or the copy undecryptable
(`meta.errorCode` = `backup.payload.undecryptable`)."
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
"503":
description: The restore could not be handed to the background worker. Nothing was changed.
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorEnvelope"
/api/admin/backups/restores:
get:
tags:
- Admin
summary: List the restores the backups console shows
description: Every queued or running restore, and those created in the last day, newest first, at most 20. What the
console reads when it opens, so a reload during a restore picks its progress up again. Admin session cookie
required; Bearer tokens cannot reach admin endpoints.
responses:
"200":
description: The restore jobs.
content:
application/json:
schema:
$ref: "#/components/schemas/AdminBackupRestoreJobListResponse"
"403": *a50
/api/admin/backups/restores/{jobId}:
get:
tags:
- Admin
summary: Read one restore job
description: The status, phase and counts of one restore, and its report or its failure once it finished. Admin session
cookie required; Bearer tokens cannot reach admin endpoints.
parameters:
- in: path
name: jobId
schema:
type: string
description: The `jobId` the restore request answered with.
required: true
description: The `jobId` the restore request answered with.
responses:
"200":
description: The restore job.
content:
application/json:
schema:
$ref: "#/components/schemas/AdminBackupRestoreJobResponse"
"403": *a50
"404":
description: No restore job with this id.
content:
application/json:
schema:
@@ -30377,39 +30429,40 @@ components:
anamnesis.fact.invalidValue; auth — auth.admin.required, auth.already_authenticated,
auth.mfa.code_invalid, auth.missing, auth.refresh.invalid, auth.refresh.reuse, auth.refresh.revoked,
auth.scope.insufficient, auth.stepup.required, auth.token.expired, auth.token.invalid; backup —
backup.payload.undecryptable, backup.section.missing; coach — coach.budget.exceeded,
coach.conversation.invalidTitle, coach.conversation.notFound, coach.fenced.attachmentConflict,
coach.fenced.attachmentLimit, coach.fenced.attachmentNotFound, coach.fenced.attachmentNotIndexed,
coach.fenced.attachmentRequired, coach.fenced.attachmentUnavailable, coach.fenced.invalid,
coach.request.invalid; coach-prefs — coach-prefs.body.invalid_json, coach-prefs.body.invalid_shape;
consent — consent.ai.required; cycle — cycle.bulk.invalid, cycle.bulk.too_large, cycle.calendar.invalid,
cycle.calendar.range, cycle.cycles.invalid, cycle.day-log.conflict, cycle.day-log.invalid,
cycle.disabled, cycle.period.invalid, cycle.symptom.custom.invalid, cycle.symptom.custom.limit;
cycle-prefs — cycle-prefs.body.invalid_json, cycle-prefs.invalid; disable-coach —
disable-coach.body.invalid_json; documents — documents.chat.conversationNotFound,
documents.chat.invalid, documents.inbound.alreadyConfirmed, documents.inbound.alreadyPartlyConfirmed,
documents.inbound.budgetExceeded, documents.inbound.decryptFailed, documents.inbound.encounterNotFound,
documents.inbound.episodeNotFound, documents.inbound.extractFailed, documents.inbound.factNotFound,
documents.inbound.factNotPending, documents.inbound.factTypeMismatch, documents.inbound.fileTooLarge,
documents.inbound.fileType, documents.inbound.invalidMetadata, documents.inbound.invalidQuery,
documents.inbound.invalidUpdate, documents.inbound.localOcrDisabled, documents.inbound.notFound,
documents.inbound.notIndexed, documents.inbound.pdfNeedsAnthropic,
documents.inbound.providerUnsupported, documents.inbound.quotaExceeded, documents.inbound.rateLimited,
documents.inbound.restoreDuplicate, documents.inbound.restoreGone, documents.inbound.uploadBusy,
documents.inbound.uploadTimeout, documents.inbound.vaccinationNotFound; encounter — encounter.invalid,
encounter.link.invalid, encounter.practitioner-not-found, encounter.reminder-conflict,
encounter.reminder-not-found; environment — environment.invalid, environment.no_home,
environment.range_too_large, environment.travel.not_found; export — export.selection.empty,
export.selection.unknown_leaf; familyHistory — familyHistory.invalid; feedback — feedback.body.invalid;
health_score_config — health_score_config.invalid, health_score_config.too_narrow; illness —
illness.day-log.invalid, illness.disabled, illness.episode.chronic-no-resolve, illness.episode.invalid,
illness.episode.invalid-window, illness.episode.parent-not-found, illness.insights.invalid; import —
import.write_failed; ingest — ingest.rate_limited; insights — insights.generate.budgetExceeded,
insights.generate.outboundScreened, insights.generate.profileScopeChanged,
insights.generate.scopeChanged; labs — labs.biomarker.create.invalid, labs.biomarker.duplicate,
labs.biomarker.notFound, labs.biomarker.update.invalid, labs.biomarker.update.referenceRangeInvalid,
labs.create.invalid, labs.ocr.budgetExceeded, labs.ocr.extractFailed, labs.ocr.fileTooLarge,
labs.ocr.fileType, labs.ocr.localOcrDisabled, labs.ocr.pdfNeedsAnthropic, labs.ocr.providerUnsupported,
backup.payload.undecryptable, backup.restore.active, backup.section.missing; coach —
coach.budget.exceeded, coach.conversation.invalidTitle, coach.conversation.notFound,
coach.fenced.attachmentConflict, coach.fenced.attachmentLimit, coach.fenced.attachmentNotFound,
coach.fenced.attachmentNotIndexed, coach.fenced.attachmentRequired, coach.fenced.attachmentUnavailable,
coach.fenced.invalid, coach.request.invalid; coach-prefs — coach-prefs.body.invalid_json,
coach-prefs.body.invalid_shape; consent — consent.ai.required; cycle — cycle.bulk.invalid,
cycle.bulk.too_large, cycle.calendar.invalid, cycle.calendar.range, cycle.cycles.invalid,
cycle.day-log.conflict, cycle.day-log.invalid, cycle.disabled, cycle.period.invalid,
cycle.symptom.custom.invalid, cycle.symptom.custom.limit; cycle-prefs — cycle-prefs.body.invalid_json,
cycle-prefs.invalid; disable-coach — disable-coach.body.invalid_json; documents —
documents.chat.conversationNotFound, documents.chat.invalid, documents.inbound.alreadyConfirmed,
documents.inbound.alreadyPartlyConfirmed, documents.inbound.budgetExceeded,
documents.inbound.decryptFailed, documents.inbound.encounterNotFound, documents.inbound.episodeNotFound,
documents.inbound.extractFailed, documents.inbound.factNotFound, documents.inbound.factNotPending,
documents.inbound.factTypeMismatch, documents.inbound.fileTooLarge, documents.inbound.fileType,
documents.inbound.invalidMetadata, documents.inbound.invalidQuery, documents.inbound.invalidUpdate,
documents.inbound.localOcrDisabled, documents.inbound.notFound, documents.inbound.notIndexed,
documents.inbound.pdfNeedsAnthropic, documents.inbound.providerUnsupported,
documents.inbound.quotaExceeded, documents.inbound.rateLimited, documents.inbound.restoreDuplicate,
documents.inbound.restoreGone, documents.inbound.uploadBusy, documents.inbound.uploadTimeout,
documents.inbound.vaccinationNotFound; encounter — encounter.invalid, encounter.link.invalid,
encounter.practitioner-not-found, encounter.reminder-conflict, encounter.reminder-not-found; environment
— environment.invalid, environment.no_home, environment.range_too_large, environment.travel.not_found;
export — export.selection.empty, export.selection.unknown_leaf; familyHistory — familyHistory.invalid;
feedback — feedback.body.invalid; health_score_config — health_score_config.invalid,
health_score_config.too_narrow; illness — illness.day-log.invalid, illness.disabled,
illness.episode.chronic-no-resolve, illness.episode.invalid, illness.episode.invalid-window,
illness.episode.parent-not-found, illness.insights.invalid; import — import.write_failed; ingest —
ingest.rate_limited; insights — insights.generate.budgetExceeded, insights.generate.outboundScreened,
insights.generate.profileScopeChanged, insights.generate.scopeChanged; labs —
labs.biomarker.create.invalid, labs.biomarker.duplicate, labs.biomarker.notFound,
labs.biomarker.update.invalid, labs.biomarker.update.referenceRangeInvalid, labs.create.invalid,
labs.ocr.budgetExceeded, labs.ocr.extractFailed, labs.ocr.fileTooLarge, labs.ocr.fileType,
labs.ocr.localOcrDisabled, labs.ocr.pdfNeedsAnthropic, labs.ocr.providerUnsupported,
labs.ocr.rateLimited, labs.restore.invalid, labs.result.notFound, labs.update.invalid,
labs.update.linkedFieldsImmutable, labs.update.numericExpected, labs.update.qualitativeExpected,
labs.update.referenceRangeInvalid, labs.update.sourceReferenceRangeInvalid; managed_profile —
@@ -71214,11 +71267,11 @@ components:
additionalProperties: {}
description: "The backup payload as the writer serialised it: `schemaVersion`, `exportedAt`, `userId`, and one array or
object per serialised class."
AdminBackupRestoreResponse:
AdminBackupRestoreQueuedResponse:
type: object
properties:
data:
$ref: "#/components/schemas/AdminBackupRestoreResult"
$ref: "#/components/schemas/AdminBackupRestoreQueued"
error:
type: "null"
meta:
@@ -71231,49 +71284,196 @@ components:
- data
- error
additionalProperties: false
AdminBackupRestoreResult:
AdminBackupRestoreQueued:
type: object
properties:
restored:
type: boolean
const: true
summary:
type: object
propertyNames:
type: string
additionalProperties: {}
skipped:
jobId:
type: string
status:
type: string
const: queued
statusUrl:
type: string
required:
- jobId
- status
- statusUrl
additionalProperties: false
description: The restore was accepted and queued. Poll `statusUrl` (`GET /api/admin/backups/restores/{jobId}`) for its
progress and outcome.
AdminBackupRestoreJobListResponse:
type: object
properties:
data:
type: object
properties:
catalogueKeys:
jobs:
type: array
items:
$ref: "#/components/schemas/AdminBackupRestoreSkipEntry"
links:
type: integer
$ref: "#/components/schemas/AdminBackupRestoreJob"
required:
- jobs
additionalProperties: false
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
AdminBackupRestoreJob:
type: object
properties:
id:
type: string
userId:
type: string
username:
anyOf:
- type: string
- type: "null"
backupId:
type: string
restoreInstanceSettings:
type: boolean
status:
type: string
enum:
- queued
- running
- succeeded
- failed
phase:
anyOf:
- $ref: "#/components/schemas/AdminBackupRestorePhase"
- type: "null"
progress:
anyOf:
- $ref: "#/components/schemas/AdminBackupRestoreProgress"
- type: "null"
result:
anyOf:
- type: object
properties:
summary:
type: object
propertyNames:
type: string
additionalProperties: {}
skipped:
type: object
properties:
catalogueKeys:
type: array
items:
$ref: "#/components/schemas/AdminBackupRestoreSkipEntry"
links:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
required:
- catalogueKeys
- links
additionalProperties: false
cleared:
type: object
propertyNames:
type: string
additionalProperties:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
required:
- summary
- skipped
- cleared
additionalProperties: false
- type: "null"
failure:
anyOf:
- $ref: "#/components/schemas/AdminBackupRestoreFailure"
- type: "null"
attempts:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
createdAt:
type: string
startedAt:
anyOf:
- type: string
- type: "null"
completedAt:
anyOf:
- type: string
- type: "null"
required:
- id
- userId
- username
- backupId
- restoreInstanceSettings
- status
- phase
- progress
- result
- failure
- attempts
- createdAt
- startedAt
- completedAt
additionalProperties: false
description: "One restore job. `phase` and `progress` describe a running job; `result` is set once it `succeeded` (the
same report the synchronous restore used to answer with, less `restored`); `failure` once it `failed`.
`attempts` counts the starts: a job whose worker stopped is started again, at most twice."
AdminBackupRestorePhase:
type: string
enum:
- validating
- clearing
- measurements
- sections
- rebuilding
description: "Where a running restore is: reading and checking the file, clearing the account's current data, writing
the readings, writing the other sections, rebuilding the chart tiers after the commit."
AdminBackupRestoreProgress:
type: object
properties:
measurementsChecked:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
measurementsTotal:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
required:
- catalogueKeys
- links
additionalProperties: false
cleared:
type: object
propertyNames:
type: string
additionalProperties:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
measurementsWritten:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
sectionsDone:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
sectionsTotal:
type: integer
minimum: -9007199254740991
maximum: 9007199254740991
required:
- restored
- summary
- skipped
- cleared
- measurementsChecked
- measurementsTotal
- measurementsWritten
- sectionsDone
- sectionsTotal
additionalProperties: false
description: What the transaction wrote. `cleared` counts the rows each class held before it was rebuilt from the file,
`summary` counts what the file carried, and `skipped.links` is the number of links dropped because a catalogue
key in the file does not exist here — zero is the normal answer.
description: Counts only. `measurementsTotal` is null until the first read of the file has counted the readings.
AdminBackupRestoreSkipEntry:
type: object
properties:
@@ -71291,6 +71491,45 @@ components:
- links
additionalProperties: false
description: One catalogue key the file referenced and this instance could not resolve, with the number of links it cost.
AdminBackupRestoreFailure:
type: object
properties:
code:
type: string
description: "Stable reason: `backup_not_found`, `backup_changed`, `backup.payload.undecryptable`, `schema_invalid`,
`incompatible_schema_version`, `owner_mismatch`, `owner_not_found`, `backup.section.missing`,
`document_ciphertext_missing`, `time_budget`, `transaction_failed`, `interrupted`, `not_started`,
`enqueue_failed`, `failed_after_commit`, `unexpected`. Every code but `failed_after_commit` means the
account was not changed."
message:
type: string
sections:
type: array
items:
type: string
required:
- code
- message
additionalProperties: false
description: Why the restore did not happen, in a sentence an operator can act on. `sections` names the missing sections
for `backup.section.missing`.
AdminBackupRestoreJobResponse:
type: object
properties:
data:
$ref: "#/components/schemas/AdminBackupRestoreJob"
error:
type: "null"
meta:
type: object
properties:
requestId:
type: string
additionalProperties: false
required:
- data
- error
additionalProperties: false
__schema0:
anyOf:
- type: string