Duplicate detection (the "merge / skip / overwrite" the user asked for):
- New POST /gedcom/preview dry-runs the file and flags incoming people that
resemble existing ones (name similarity via difflib + birth-year guard;
high/medium score). No writes.
- /gedcom/import takes default_action (new|skip|merge|overwrite) + per-xref
resolutions {xref: {action, target_id}}:
new create as a new person (current behavior)
skip link families to the existing person, copy nothing
merge attach the incoming names (as alternates), events, citations,
and notes onto the existing person
overwrite soft-delete the existing person, import the incoming one fresh
Relationship creation is deduped so a merge can't double an edge.
Richer record mapping (covers the user's repo's GEDCOM):
- Multiple NAME records honor their TYPE; _MARNM (and NICK) import as typed
alternate names — maiden stays primary, married becomes a "married" Name.
- RELI -> a "religion" event with the value in detail; OCCU/EDUC values too.
- NOTE -> person notes (and event notes); NOTE/RELI are no longer "unmapped".
- Export round-trips name TYPE.
Verified against the user's 2185-person export: 0 unmapped tags. 48 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Names (the genealogy standard: maiden name primary, married/alias as typed
alternates):
- Name model already supported multiple typed names; expose full CRUD —
NameCreate/Read/Update schemas, name_service (one-primary invariant,
promote-on-delete), nested /persons/{id}/names routes.
- Person page gains a Names card: add/edit/delete + "make primary", with a
curated name_type dropdown (birth/maiden, married, alias, nickname, …).
Self-person ("who am I"):
- users.self_person_id FK (use_alter for the users<->persons<->trees cycle)
+ migration; PATCH /users/me/self-person; "This is me" / "This is you"
on the person page. Soft-deleting the linked person clears it.
Deletion integrity (fixes the broken tree view):
- delete_person now soft-deletes the relationships touching the person, so no
dangling edges remain; family-chart also filters links to missing people.
- Optional cascade=true recursively deletes descendants (GEDCOM cleanup);
the person page asks "only this person" vs "with all descendants".
- DELETE returns {deleted: n}.
Family view surfaces "Not connected to anyone" so dangling people aren't lost.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Tree page: add a "Find a person" search box that jumps the chart to a
match and rebuilds the hourglass (parents/grandparents/partner/children)
around them. Clicking any card recenters via family-chart's default
behavior (setAncestryDepth 3 / setProgenyDepth 2), syncing focus through
setAfterUpdate for the "Open profile" link.
- Person detail: replace the relationship "add" <select> with a
type-to-filter PersonCombobox so long people lists are searchable.
- Person detail: gender is now a Male/Female dropdown, not free text.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the rule #8 gap at the API layer: PATCH endpoints + service updates for Tree (name/description/visibility), Source, Citation (page/detail/confidence), Relationship (qualifier/notes), and Media (title/attachment) — editor-gated and audited. Every core entity now has create/read/update/delete. Edit UIs for these land in the frontend batch. 37 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
Person detail: an Edit form for name + gender + living status + privacy, and inline edit of each life event (type + structured date). Family view: the add-relative buttons now search existing people (link the real person) or create new — preventing duplicate spouses/parents — and adding a child to someone with one spouse links both parents.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
Events and people are now editable, not write-once: PATCH /events/{id} (type, structured date, place, notes) and PATCH /persons/{id} (vitals, privacy, and the primary name's given/surname). CLAUDE.md gains rule #8: every stored object must support full CRUD in API and UI — historical research is constant correction. Tests cover both updates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
Tree page gets Landscape/Portrait/Fan toggles: landscape & portrait via family-chart's orientation; a hand-rolled radial Fan chart of ancestors (rings per generation, click to recenter). Clicking a card recenters and updates an 'Open <name> →' link to that person's profile. The People directory search now hits the server-side pg_trgm fuzzy endpoint (debounced) so it spans the whole tree, not just the loaded page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
Fuzzy search: pg_trgm extension + trigram GIN indexes on name parts and a GET /trees/{id}/persons?q= search ranked by trigram similarity (finds Mueller for 'muller'), privacy-filtered. Living-person protection: the privacy engine now derives possibly-living status (explicit flag, else no death fact + birth within ~100y or unknown) and returns 'redacted' for non-members of public/unlisted trees; the service minimises those records ('Living person', no vitals). Members are unaffected. 31 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
Researched how FamilySearch/Geni/MyHeritage lay out trees (switchable pedigree/portrait/fan, an interactive canvas with pan/zoom + click-to-recenter, gender colors, birth-death years) and built a real Tree page on the MIT d3 library family-chart instead of a flat list. Ancestors + descendants around a focus person, click any card to recenter, drag to pan, scroll to zoom — scales to large imported trees. Tree is now the first per-tree sidebar item and the default when opening a tree; People keeps the searchable directory + add/edit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
A flat wrap of every person didn't scale to imported trees. Replace it with a bounded (max-height, scrollable) searchable directory: clean name + birth–death-year rows, focus highlight, a result count, and a 200-row cap with a 'refine your search' notice so a thousand-person tree stays fast and usable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
An Import/Export page (sidebar) that defaults to importing into a NEW tree to avoid duplicating existing people, with an explicit 'append to this tree' option (warned), a mapping-report display (counts + skipped tags), and a one-click .ged export download.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
A pragmatic GEDCOM parser + mapper: import reads INDI/FAM/SOUR and creates people, names, life events, partnership + qualified parent-child relationships, marriage events, places (deduped), sources, and citations from SOUR refs — returning a mapping report (counts + unmapped tags). Export serializes the tree back to GEDCOM (families derived from the edge model). Import is additive (no merge) and runs inline for now. Round-trip test passes; 29 tests total.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
Rebuilds the family view's pedigree as a recursive bracket chart with CSS connector lines — focus links to its two parents (2 lines), and each parent links to its two parents (4 lines to grandparents). Fixes the prior ambiguity where grandparent slots weren't tied to a specific parent: now every parent shows its own two parent slots, so a person clearly has up to four grandparents grouped by lineage. Height-robust connectors (each leaf draws its own spine half + stub).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
The People page is no longer a flat list: it's a focus-person family view with a pedigree of ancestors (parents + grandparents), a spouse/partner panel, and a children panel — with inline 'add parent/child/spouse' (creates the person + the relationship), click-to-refocus, birth–death years, and a searchable people index. Modeled on how real genealogy tools center on a person and let you walk the graph.
Adds delete/restore UI: a Delete on the person page, per-tree delete + a 'Recently deleted' restore section on the trees list, and a Recovery page (sidebar) for deleted people.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
Tree and person soft-delete + restore (owner-only for trees, editor for people) with recovery listings (?deleted=true); the worker already purges past the 30-day window. Adds tree-wide GET /relationships and /events so the family/pedigree view loads the whole graph in a few calls. 27 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
Replaces the centered single-column of full-width cards with a proper application layout: a persistent left sidebar (Trees, and per-tree People/Sources/Media, with the tree name and sign-out) and a constrained content column. Marketing landing and auth pages are split out (own header/footer; centered auth with the logo).
Adds a Media gallery (upload + image thumbnails / file tiles, served via the backend content endpoint). Events are no longer free-text: a curated event-type list (+ custom) and a structured date (qualifier + day/month/year) that composes a proper genealogical date. Regenerated the OpenAPI client.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
Presigned URLs point at the internal minio:9000 host a browser can't reach. Add ObjectStore.get_object and a GET /media/{id}/content endpoint that resolves visibility and streams the bytes; MediaRead.url now points there. Keeps the object store private and downloads behind the privacy engine.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
Media model + migration; an ObjectStore interface with an S3/MinIO (boto3) implementation behind the service layer. Upload (multipart) stores bytes in object storage + a metadata row (checksum, size, content-type, optional attach to person/event/source); list returns presigned URLs; delete is soft. Editor-gated, privacy-filtered, audited. 24 tests pass (object store faked).
Introduces the worker container (same image, 'python -m app.worker'): its first job is the scheduled 30-day soft-delete purge across tables + media object cleanup. Compose gains worker + S3 env on backend/worker; dev override builds the worker too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
Lifts the UI from wireframe to a finished heritage look: Fraunces (display serif) + Inter (sans) via next/font; a proper hero landing with a feature triad and the Origin mark; a warm bronze-tinted background gradient for depth; a sticky branded header and refined footer. Polished button (sizes + bronze focus ring + shadow), card (rounded-xl, soft layered shadow), and input (bronze focus) primitives that carry across every page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
New /trees/[id]/sources page (list + create sources). Person-detail page now loads tree sources + citations and shows a '✓ N sourced' badge with an inline cite picker (source + page) on each event and on the person. Tree view links to Sources. Regenerated the OpenAPI client.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
Source CRUD (reusable, tree-scoped) and Citation create/list/soft-delete linking one source to exactly one fact (person/event/name/relationship). Editor-gated writes, privacy-filtered reads, audit throughout; tenant + existence validation on source and target. list_citations returns all tree citations so the UI can render 'sourced' indicators in one round-trip. 22 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
New /trees/[id]/persons/[personId] view: life-events timeline with add/remove, and relationships grouped into parents/children/partners/siblings with an add form (kind + person picker + qualifier). People in the tree list now link here. Regenerated the OpenAPI client for the new endpoints.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
Events (create/list-per-person/soft-delete) and relationships (create/list-per-person/soft-delete) through the layered stack: editor-gated writes, privacy-engine reads, audit on every change. Events carry exactly one subject (person XOR partnership); relationships are typed qualified edges (parent_child gets a biological/adoptive/step/foster/donor/guardian qualifier). Adds a single-person GET. 18 tests pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
ripper already runs a single global nickfedor/watchtower (label-enabled) that watches every stack; the bundled containrrr/watchtower was redundant and crash-looped (its Docker API client is too old for Docker 29). Keep the watchtower.enable labels on backend/frontend so the host instance auto-deploys them; remove the per-stack service and profile.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
Watchtower (profile-gated) watches only the label-enabled backend/frontend containers and recreates them when a new :test-main digest lands in the registry, polling every 120s. Scoped by label so it never touches Postgres/MinIO/Caddy/cloudflared. Reads registry creds from the host docker config. Lab host runs COMPOSE_PROFILES=tunnel,watchtower.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
Replaces the default black/gray with the docs/brand palette: warm ink text on paper surfaces, bronze accent, serif headings and the Origin-mark wordmark in the header, favicon, and the 'where it came from matters' tagline. Light/dark adapt via CSS vars (ink/paper flip); bronze and paper are constant. Tailwind v4 @theme exposes bronze/paper/ink tokens and the serif stack. Buttons/inputs/cards restyled to match; brand SVGs vendored into public/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
A cloudflared service (opt-in via the 'tunnel' compose profile, token from CLOUDFLARE_TUNNEL_TOKEN) connects the lab to Cloudflare. One public hostname -> http://caddy:80 is sufficient because Caddy does the internal path routing. Mirrors the drawbar tunnel setup.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
Split the registry endpoints like the drawbar containers. Per-component Gitea Actions workflows (build-backend, build-frontend; runs-on docker, path-filtered) push images to the LAN endpoint 192.168.0.2:1234 over plain HTTP (buildx insecure/http) to bypass Cloudflare's request-body limit, then link each package to the repo via the Gitea API. Auth via the REGISTRY_TOKEN Actions secret (the same token drawbar uses). Tag scheme: test-main / test-sha-<long> / version / latest (v* tags).
The deploy compose now PULLS git.jpaul.io/justin/provenance-{backend,frontend}:${IMAGE_TAG:-test-main} (no host build); docker-compose.dev.yml is a local-build override for dev / pre-CI. Replaces the previous single build.yml. Docs + memory updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
Records the landed frontend scaffold and that Phase 0 (backend, data model, local auth, deploy stack, CI, frontend) is complete and running live; Phase 1 (core tree features) is the new current target.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>