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:
@@ -0,0 +1,307 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
import { api } from "@/lib/api/client";
|
||||
import type { components } from "@/lib/api/schema";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
||||
type Deceased = components["schemas"]["DeceasedCandidate"];
|
||||
type GenderProp = components["schemas"]["GenderProposal"];
|
||||
type NameIssue = components["schemas"]["NameIssue"];
|
||||
|
||||
const ISSUE_LABEL: Record<string, string> = {
|
||||
date_in_surname: "date in surname",
|
||||
date_in_given: "date in given name",
|
||||
no_surname: "no surname",
|
||||
packed_given: "long given name",
|
||||
};
|
||||
|
||||
export default function CleanupPage() {
|
||||
const params = useParams<{ id: string }>();
|
||||
const treeId = params.id;
|
||||
|
||||
// 1) Deceased by birth year
|
||||
const [year, setYear] = useState(1930);
|
||||
const [deceased, setDeceased] = useState<Deceased[] | null>(null);
|
||||
const [decSel, setDecSel] = useState<Set<string>>(new Set());
|
||||
const [decMsg, setDecMsg] = useState<string | null>(null);
|
||||
|
||||
// 2) Gender from source GEDCOM
|
||||
const [gender, setGender] = useState<GenderProp[] | null>(null);
|
||||
const [genSel, setGenSel] = useState<Set<string>>(new Set());
|
||||
const [genMsg, setGenMsg] = useState<string | null>(null);
|
||||
const genFile = useRef<HTMLInputElement>(null);
|
||||
|
||||
// 3) Name issues
|
||||
const [issues, setIssues] = useState<NameIssue[] | null>(null);
|
||||
const [edits, setEdits] = useState<Record<string, { given: string; surname: string; on: boolean }>>({});
|
||||
const [nameMsg, setNameMsg] = useState<string | null>(null);
|
||||
|
||||
async function previewDeceased() {
|
||||
setDecMsg(null);
|
||||
const { data } = await api.GET("/api/v1/trees/{tree_id}/cleanup/deceased", {
|
||||
params: { path: { tree_id: treeId }, query: { born_on_or_before: year } },
|
||||
});
|
||||
setDeceased(data ?? []);
|
||||
setDecSel(new Set((data ?? []).map((d) => d.person_id)));
|
||||
}
|
||||
async function applyDeceased() {
|
||||
const ids = [...decSel];
|
||||
const { data } = await api.POST("/api/v1/trees/{tree_id}/cleanup/deceased", {
|
||||
params: { path: { tree_id: treeId } },
|
||||
body: { person_ids: ids },
|
||||
});
|
||||
setDecMsg(`Marked ${data?.updated ?? 0} people deceased.`);
|
||||
setDeceased(null);
|
||||
}
|
||||
|
||||
async function previewGender(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const file = e.target.files?.[0];
|
||||
if (genFile.current) genFile.current.value = "";
|
||||
if (!file) return;
|
||||
setGenMsg(null);
|
||||
const fd = new FormData();
|
||||
fd.append("file", file);
|
||||
const resp = await fetch(`/api/v1/trees/${treeId}/cleanup/gender/preview`, {
|
||||
method: "POST",
|
||||
body: fd,
|
||||
credentials: "include",
|
||||
});
|
||||
if (resp.ok) {
|
||||
const data: GenderProp[] = await resp.json();
|
||||
setGender(data);
|
||||
setGenSel(new Set(data.map((g) => g.person_id)));
|
||||
}
|
||||
}
|
||||
async function applyGender() {
|
||||
const updates = (gender ?? [])
|
||||
.filter((g) => genSel.has(g.person_id))
|
||||
.map((g) => ({ person_id: g.person_id, gender: g.proposed_gender }));
|
||||
const { data } = await api.POST("/api/v1/trees/{tree_id}/cleanup/gender", {
|
||||
params: { path: { tree_id: treeId } },
|
||||
body: { updates },
|
||||
});
|
||||
setGenMsg(`Set gender on ${data?.updated ?? 0} people.`);
|
||||
setGender(null);
|
||||
}
|
||||
|
||||
const loadNames = useCallback(async () => {
|
||||
setNameMsg(null);
|
||||
const { data } = await api.GET("/api/v1/trees/{tree_id}/cleanup/names", {
|
||||
params: { path: { tree_id: treeId } },
|
||||
});
|
||||
setIssues(data ?? []);
|
||||
const init: Record<string, { given: string; surname: string; on: boolean }> = {};
|
||||
for (const i of data ?? []) {
|
||||
init[i.name_id] = { given: i.given ?? "", surname: i.surname ?? "", on: false };
|
||||
}
|
||||
setEdits(init);
|
||||
}, [treeId]);
|
||||
|
||||
useEffect(() => {
|
||||
loadNames();
|
||||
}, [loadNames]);
|
||||
|
||||
async function applyNames() {
|
||||
const chosen = (issues ?? []).filter((i) => edits[i.name_id]?.on);
|
||||
const body = {
|
||||
edits: chosen.map((i) => ({
|
||||
name_id: i.name_id,
|
||||
given: edits[i.name_id].given,
|
||||
surname: edits[i.name_id].surname,
|
||||
})),
|
||||
};
|
||||
if (!body.edits.length) return;
|
||||
const { data } = await api.POST("/api/v1/trees/{tree_id}/cleanup/names", {
|
||||
params: { path: { tree_id: treeId } },
|
||||
body,
|
||||
});
|
||||
setNameMsg(`Fixed ${data?.updated ?? 0} names.`);
|
||||
loadNames();
|
||||
}
|
||||
|
||||
const toggle = (set: Set<string>, id: string, setter: (s: Set<string>) => void) => {
|
||||
const n = new Set(set);
|
||||
if (n.has(id)) n.delete(id);
|
||||
else n.add(id);
|
||||
setter(n);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">Cleanup</h1>
|
||||
<p className="mt-1 text-sm text-[var(--muted)]">
|
||||
Fix common import messes in bulk. Each tool previews its changes — nothing is saved
|
||||
until you apply.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 1) Deceased by year */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Mark deceased by birth year</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex flex-wrap items-end gap-2">
|
||||
<label className="flex flex-col gap-1 text-sm">
|
||||
<span className="text-xs text-[var(--muted)]">Born on or before</span>
|
||||
<Input
|
||||
type="number"
|
||||
className="w-28"
|
||||
value={year}
|
||||
onChange={(e) => setYear(Number(e.target.value))}
|
||||
/>
|
||||
</label>
|
||||
<Button variant="outline" onClick={previewDeceased}>
|
||||
Preview
|
||||
</Button>
|
||||
</div>
|
||||
{decMsg && <p className="text-sm text-bronze">{decMsg}</p>}
|
||||
{deceased && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-[var(--muted)]">
|
||||
{deceased.length} people born ≤ {year} (not already marked deceased).
|
||||
</p>
|
||||
<ul className="max-h-64 divide-y divide-[var(--border)] overflow-auto rounded-lg border border-[var(--border)]">
|
||||
{deceased.map((d) => (
|
||||
<li key={d.person_id} className="flex items-center gap-3 px-3 py-1.5 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={decSel.has(d.person_id)}
|
||||
onChange={() => toggle(decSel, d.person_id, setDecSel)}
|
||||
/>
|
||||
<span className="flex-1">{d.name}</span>
|
||||
<span className="text-xs text-[var(--muted)]">b. {d.birth_year}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{deceased.length > 0 && (
|
||||
<Button onClick={applyDeceased}>Mark {decSel.size} deceased</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 2) Gender from source */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Set sex from a source GEDCOM</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<p className="text-sm text-[var(--muted)]">
|
||||
Upload your source <code>.ged</code> (it carries each person’s sex). We match by
|
||||
name and propose sex only for people who don’t have it set.
|
||||
</p>
|
||||
<input
|
||||
ref={genFile}
|
||||
type="file"
|
||||
accept=".ged,.gedcom,text/plain"
|
||||
onChange={previewGender}
|
||||
className="hidden"
|
||||
/>
|
||||
<Button variant="outline" onClick={() => genFile.current?.click()}>
|
||||
Choose source GEDCOM
|
||||
</Button>
|
||||
{genMsg && <p className="text-sm text-bronze">{genMsg}</p>}
|
||||
{gender && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-[var(--muted)]">{gender.length} matches with a sex to set.</p>
|
||||
<ul className="max-h-64 divide-y divide-[var(--border)] overflow-auto rounded-lg border border-[var(--border)]">
|
||||
{gender.map((g) => (
|
||||
<li key={g.person_id} className="flex items-center gap-3 px-3 py-1.5 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={genSel.has(g.person_id)}
|
||||
onChange={() => toggle(genSel, g.person_id, setGenSel)}
|
||||
/>
|
||||
<span className="flex-1">{g.name}</span>
|
||||
<span
|
||||
className="text-xs"
|
||||
style={{
|
||||
color:
|
||||
g.proposed_gender === "male"
|
||||
? "rgb(120,159,172)"
|
||||
: "rgb(196,138,146)",
|
||||
}}
|
||||
>
|
||||
{g.proposed_gender}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{gender.length > 0 && (
|
||||
<Button onClick={applyGender}>Set sex on {genSel.size} people</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 3) Name issues */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-base">Names that look broken</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{nameMsg && <p className="text-sm text-bronze">{nameMsg}</p>}
|
||||
{issues === null ? (
|
||||
<p className="text-sm text-[var(--muted)]">Scanning…</p>
|
||||
) : issues.length === 0 ? (
|
||||
<p className="text-sm text-[var(--muted)]">No obvious name problems found.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-[var(--muted)]">
|
||||
{issues.length} flagged. Edit given/surname, tick the ones to fix, then apply.
|
||||
</p>
|
||||
<ul className="space-y-2">
|
||||
{issues.map((i) => {
|
||||
const e = edits[i.name_id] ?? { given: "", surname: "", on: false };
|
||||
return (
|
||||
<li key={i.name_id} className="flex flex-wrap items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={e.on}
|
||||
onChange={() =>
|
||||
setEdits((p) => ({ ...p, [i.name_id]: { ...e, on: !e.on } }))
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
className="h-9 w-40"
|
||||
placeholder="Given"
|
||||
value={e.given}
|
||||
onChange={(ev) =>
|
||||
setEdits((p) => ({ ...p, [i.name_id]: { ...e, given: ev.target.value } }))
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
className="h-9 w-40"
|
||||
placeholder="Surname"
|
||||
value={e.surname}
|
||||
onChange={(ev) =>
|
||||
setEdits((p) => ({
|
||||
...p,
|
||||
[i.name_id]: { ...e, surname: ev.target.value },
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<span className="rounded bg-[var(--border)]/50 px-1.5 py-0.5 text-xs text-[var(--muted)]">
|
||||
{ISSUE_LABEL[i.issue] ?? i.issue}
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<Button onClick={applyNames}>Fix selected</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
LogOut,
|
||||
Network,
|
||||
Settings,
|
||||
Sparkles,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -128,6 +129,12 @@ export function AppSidebar({ onNavigate }: { onNavigate?: () => void }) {
|
||||
icon={ArrowDownUp}
|
||||
active={pathname.startsWith(`/trees/${treeId}/gedcom`)}
|
||||
/>
|
||||
<Item
|
||||
href={`/trees/${treeId}/cleanup`}
|
||||
label="Cleanup"
|
||||
icon={Sparkles}
|
||||
active={pathname.startsWith(`/trees/${treeId}/cleanup`)}
|
||||
/>
|
||||
<Item
|
||||
href={`/trees/${treeId}/recovery`}
|
||||
label="Recovery"
|
||||
|
||||
Vendored
+364
@@ -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"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user