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
+364
View File
@@ -679,6 +679,76 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/v1/trees/{tree_id}/cleanup/deceased": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Preview Deceased */
get: operations["preview_deceased_api_v1_trees__tree_id__cleanup_deceased_get"];
put?: never;
/** Apply Deceased */
post: operations["apply_deceased_api_v1_trees__tree_id__cleanup_deceased_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/trees/{tree_id}/cleanup/gender/preview": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Preview Gender */
post: operations["preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/trees/{tree_id}/cleanup/gender": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Apply Gender */
post: operations["apply_gender_api_v1_trees__tree_id__cleanup_gender_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/v1/trees/{tree_id}/cleanup/names": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Preview Names */
get: operations["preview_names_api_v1_trees__tree_id__cleanup_names_get"];
put?: never;
/** Apply Names */
post: operations["apply_names_api_v1_trees__tree_id__cleanup_names_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
@@ -713,6 +783,11 @@ export interface components {
/** File */
file: string;
};
/** Body_preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post */
Body_preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post: {
/** File */
file: string;
};
/** Body_upload_media_api_v1_trees__tree_id__media_post */
Body_upload_media_api_v1_trees__tree_id__media_post: {
/** File */
@@ -796,6 +871,28 @@ export interface components {
detail?: string | null;
confidence?: components["schemas"]["CitationConfidence"] | null;
};
/** CleanupResult */
CleanupResult: {
/** Updated */
updated: number;
};
/** DeceasedApply */
DeceasedApply: {
/** Person Ids */
person_ids: string[];
};
/** DeceasedCandidate */
DeceasedCandidate: {
/**
* Person Id
* Format: uuid
*/
person_id: string;
/** Name */
name: string;
/** Birth Year */
birth_year: number;
};
/** DuplicateMatch */
DuplicateMatch: {
/** Xref */
@@ -905,6 +1002,33 @@ export interface components {
/** Notes */
notes?: string | null;
};
/** GenderApply */
GenderApply: {
/** Updates */
updates: components["schemas"]["GenderUpdate"][];
};
/** GenderProposal */
GenderProposal: {
/**
* Person Id
* Format: uuid
*/
person_id: string;
/** Name */
name: string;
/** Proposed Gender */
proposed_gender: string;
};
/** GenderUpdate */
GenderUpdate: {
/**
* Person Id
* Format: uuid
*/
person_id: string;
/** Gender */
gender: string;
};
/** HTTPValidationError */
HTTPValidationError: {
/** Detail */
@@ -984,6 +1108,11 @@ export interface components {
/** Source Id */
source_id?: string | null;
};
/** NameApply */
NameApply: {
/** Edits */
edits: components["schemas"]["NameEdit"][];
};
/** NameCreate */
NameCreate: {
/**
@@ -1007,6 +1136,37 @@ export interface components {
*/
is_primary?: boolean;
};
/** NameEdit */
NameEdit: {
/**
* Name Id
* Format: uuid
*/
name_id: string;
/** Given */
given?: string | null;
/** Surname */
surname?: string | null;
};
/** NameIssue */
NameIssue: {
/**
* Name Id
* Format: uuid
*/
name_id: string;
/**
* Person Id
* Format: uuid
*/
person_id: string;
/** Given */
given?: string | null;
/** Surname */
surname?: string | null;
/** Issue */
issue: string;
};
/** NameRead */
NameRead: {
/**
@@ -3218,4 +3378,208 @@ export interface operations {
};
};
};
preview_deceased_api_v1_trees__tree_id__cleanup_deceased_get: {
parameters: {
query?: {
born_on_or_before?: number;
};
header?: never;
path: {
tree_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["DeceasedCandidate"][];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
apply_deceased_api_v1_trees__tree_id__cleanup_deceased_post: {
parameters: {
query?: never;
header?: never;
path: {
tree_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["DeceasedApply"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CleanupResult"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post: {
parameters: {
query?: never;
header?: never;
path: {
tree_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"multipart/form-data": components["schemas"]["Body_preview_gender_api_v1_trees__tree_id__cleanup_gender_preview_post"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["GenderProposal"][];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
apply_gender_api_v1_trees__tree_id__cleanup_gender_post: {
parameters: {
query?: never;
header?: never;
path: {
tree_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["GenderApply"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CleanupResult"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
preview_names_api_v1_trees__tree_id__cleanup_names_get: {
parameters: {
query?: never;
header?: never;
path: {
tree_id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["NameIssue"][];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
apply_names_api_v1_trees__tree_id__cleanup_names_post: {
parameters: {
query?: never;
header?: never;
path: {
tree_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["NameApply"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["CleanupResult"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
}