Tree Cleanup tool: bulk fixes with preview → approve

A new per-tree Cleanup page (and cleanup_service + endpoints), each fix
preview-first per the propose-then-approve rule:

- Mark deceased by birth year: lists people born ≤ a cutoff (default 1930) not
  already deceased; apply sets is_living=false for the ones you keep checked.
- Set sex from a source GEDCOM: upload the source .ged (it carries SEX); matches
  by name and proposes sex only where it's missing — far more accurate than
  guessing from first names. Review, then apply.
- Names that look broken: flags date-in-surname / date-in-given / no-surname /
  packed given names, with inline editable given+surname; fix the checked ones.

No migration (uses existing columns). 55 backend tests pass (preview+apply for
all three); frontend builds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 10:17:01 -04:00
parent 97f7a9e0ff
commit aa62ca490e
9 changed files with 1737 additions and 0 deletions
+542
View File
@@ -2701,6 +2701,322 @@
}
}
}
},
"/api/v1/trees/{tree_id}/cleanup/deceased": {
"get": {
"tags": [
"cleanup"
],
"summary": "Preview Deceased",
"operationId": "preview_deceased_api_v1_trees__tree_id__cleanup_deceased_get",
"parameters": [
{
"name": "tree_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Tree Id"
}
},
{
"name": "born_on_or_before",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"default": 1930,
"title": "Born On Or Before"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/DeceasedCandidate"
},
"title": "Response Preview Deceased Api V1 Trees Tree Id Cleanup Deceased Get"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"post": {
"tags": [
"cleanup"
],
"summary": "Apply Deceased",
"operationId": "apply_deceased_api_v1_trees__tree_id__cleanup_deceased_post",
"parameters": [
{
"name": "tree_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Tree Id"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DeceasedApply"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CleanupResult"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/v1/trees/{tree_id}/cleanup/gender/preview": {
"post": {
"tags": [
"cleanup"
],
"summary": "Preview Gender",
"operationId": "preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post",
"parameters": [
{
"name": "tree_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Tree Id"
}
}
],
"requestBody": {
"required": true,
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/Body_preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/GenderProposal"
},
"title": "Response Preview Gender Api V1 Trees Tree Id Cleanup Gender Preview Post"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/v1/trees/{tree_id}/cleanup/gender": {
"post": {
"tags": [
"cleanup"
],
"summary": "Apply Gender",
"operationId": "apply_gender_api_v1_trees__tree_id__cleanup_gender_post",
"parameters": [
{
"name": "tree_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Tree Id"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GenderApply"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CleanupResult"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/api/v1/trees/{tree_id}/cleanup/names": {
"get": {
"tags": [
"cleanup"
],
"summary": "Preview Names",
"operationId": "preview_names_api_v1_trees__tree_id__cleanup_names_get",
"parameters": [
{
"name": "tree_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Tree Id"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/NameIssue"
},
"title": "Response Preview Names Api V1 Trees Tree Id Cleanup Names Get"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"post": {
"tags": [
"cleanup"
],
"summary": "Apply Names",
"operationId": "apply_names_api_v1_trees__tree_id__cleanup_names_post",
"parameters": [
{
"name": "tree_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"format": "uuid",
"title": "Tree Id"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/NameApply"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CleanupResult"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
}
},
"components": {
@@ -2770,6 +3086,20 @@
],
"title": "Body_preview_gedcom_api_v1_trees__tree_id__gedcom_preview_post"
},
"Body_preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post": {
"properties": {
"file": {
"type": "string",
"contentMediaType": "application/octet-stream",
"title": "File"
}
},
"type": "object",
"required": [
"file"
],
"title": "Body_preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post"
},
"Body_upload_media_api_v1_trees__tree_id__media_post": {
"properties": {
"file": {
@@ -3091,6 +3421,60 @@
"type": "object",
"title": "CitationUpdate"
},
"CleanupResult": {
"properties": {
"updated": {
"type": "integer",
"title": "Updated"
}
},
"type": "object",
"required": [
"updated"
],
"title": "CleanupResult"
},
"DeceasedApply": {
"properties": {
"person_ids": {
"items": {
"type": "string",
"format": "uuid"
},
"type": "array",
"title": "Person Ids"
}
},
"type": "object",
"required": [
"person_ids"
],
"title": "DeceasedApply"
},
"DeceasedCandidate": {
"properties": {
"person_id": {
"type": "string",
"format": "uuid",
"title": "Person Id"
},
"name": {
"type": "string",
"title": "Name"
},
"birth_year": {
"type": "integer",
"title": "Birth Year"
}
},
"type": "object",
"required": [
"person_id",
"name",
"birth_year"
],
"title": "DeceasedCandidate"
},
"DuplicateMatch": {
"properties": {
"xref": {
@@ -3526,6 +3910,65 @@
"type": "object",
"title": "EventUpdate"
},
"GenderApply": {
"properties": {
"updates": {
"items": {
"$ref": "#/components/schemas/GenderUpdate"
},
"type": "array",
"title": "Updates"
}
},
"type": "object",
"required": [
"updates"
],
"title": "GenderApply"
},
"GenderProposal": {
"properties": {
"person_id": {
"type": "string",
"format": "uuid",
"title": "Person Id"
},
"name": {
"type": "string",
"title": "Name"
},
"proposed_gender": {
"type": "string",
"title": "Proposed Gender"
}
},
"type": "object",
"required": [
"person_id",
"name",
"proposed_gender"
],
"title": "GenderProposal"
},
"GenderUpdate": {
"properties": {
"person_id": {
"type": "string",
"format": "uuid",
"title": "Person Id"
},
"gender": {
"type": "string",
"title": "Gender"
}
},
"type": "object",
"required": [
"person_id",
"gender"
],
"title": "GenderUpdate"
},
"HTTPValidationError": {
"properties": {
"detail": {
@@ -3774,6 +4217,22 @@
"type": "object",
"title": "MediaUpdate"
},
"NameApply": {
"properties": {
"edits": {
"items": {
"$ref": "#/components/schemas/NameEdit"
},
"type": "array",
"title": "Edits"
}
},
"type": "object",
"required": [
"edits"
],
"title": "NameApply"
},
"NameCreate": {
"properties": {
"name_type": {
@@ -3845,6 +4304,89 @@
"type": "object",
"title": "NameCreate"
},
"NameEdit": {
"properties": {
"name_id": {
"type": "string",
"format": "uuid",
"title": "Name Id"
},
"given": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Given"
},
"surname": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Surname"
}
},
"type": "object",
"required": [
"name_id"
],
"title": "NameEdit"
},
"NameIssue": {
"properties": {
"name_id": {
"type": "string",
"format": "uuid",
"title": "Name Id"
},
"person_id": {
"type": "string",
"format": "uuid",
"title": "Person Id"
},
"given": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Given"
},
"surname": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Surname"
},
"issue": {
"type": "string",
"title": "Issue"
}
},
"type": "object",
"required": [
"name_id",
"person_id",
"issue"
],
"title": "NameIssue"
},
"NameRead": {
"properties": {
"id": {