mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
Merge branch 'feat/restore-background-job' into release/v1.39.1
This commit is contained in:
commit
934131ee3e
52 files changed
+5684
-2439
No files matched your search
+322
-83
@@ -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
|
||||
|
||||
Reference in new issue
Block a user