diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index f3ddafd..9c8ad77 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -1,10 +1,8 @@
import type { Metadata } from "next";
import { Fraunces, Inter } from "next/font/google";
-import Link from "next/link";
import "./globals.css";
-// Heritage display serif + clean humanist sans (per docs/brand typography).
const serif = Fraunces({
subsets: ["latin"],
variable: "--font-fraunces",
@@ -23,36 +21,7 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
-
-
-
- {children}
-
-
-
+ {children}
);
}
diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx
index b79f6fb..5985211 100644
--- a/frontend/app/login/page.tsx
+++ b/frontend/app/login/page.tsx
@@ -31,7 +31,13 @@ export default function LoginPage() {
}
return (
-
+
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+
+
Sign in
@@ -70,5 +76,7 @@ export default function LoginPage() {
+
+
);
}
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
index 9a7a7b2..f05ad9b 100644
--- a/frontend/app/page.tsx
+++ b/frontend/app/page.tsx
@@ -23,55 +23,83 @@ const features = [
export default function Home() {
return (
-
-
-
-
- Family · Land · Provenance
-
-
- Where it came from{" "}
- matters .
-
-
- Trace your family and your land in one place — every name, every parcel, every claim
- linked to the record it came from. Self-hosted, sourced, and yours to keep.
-
-
-
- Create your account
-
-
-
- Sign in
-
-
-
-
-
-
-
+
-
+
-
- {features.map((f) => (
-
-
-
+
+
+
+
+ Family · Land · Provenance
+
+
+ Where it came from matters .
+
+
+ Trace your family and your land in one place — every name, every parcel, every claim
+ linked to the record it came from. Self-hosted, sourced, and yours to keep.
+
+
+
+ Create your account
+
+
+
+ Sign in
+
+
-
{f.title}
-
{f.body}
- ))}
-
+
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+
+
+
+
+
+
+ {features.map((f) => (
+
+
+
+
+
{f.title}
+
{f.body}
+
+ ))}
+
+
+
+
);
}
diff --git a/frontend/app/register/page.tsx b/frontend/app/register/page.tsx
index fe67991..fb0083a 100644
--- a/frontend/app/register/page.tsx
+++ b/frontend/app/register/page.tsx
@@ -34,7 +34,13 @@ export default function RegisterPage() {
}
return (
-
+
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+
+
Create your account
@@ -78,5 +84,7 @@ export default function RegisterPage() {
+
+
);
}
diff --git a/frontend/app/trees/[id]/media/page.tsx b/frontend/app/trees/[id]/media/page.tsx
new file mode 100644
index 0000000..30db481
--- /dev/null
+++ b/frontend/app/trees/[id]/media/page.tsx
@@ -0,0 +1,134 @@
+"use client";
+
+import { useParams, useRouter } from "next/navigation";
+import { useCallback, useEffect, useRef, useState } from "react";
+
+import { api } from "@/lib/api/client";
+import type { components } from "@/lib/api/schema";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+
+type Media = components["schemas"]["MediaRead"];
+
+function humanSize(bytes: number) {
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
+}
+
+export default function MediaPage() {
+ const router = useRouter();
+ const params = useParams<{ id: string }>();
+ const treeId = params.id;
+
+ const [items, setItems] = useState([]);
+ const [ready, setReady] = useState(false);
+ const [uploading, setUploading] = useState(false);
+ const fileRef = useRef(null);
+
+ const load = useCallback(async () => {
+ const { data, response } = await api.GET("/api/v1/trees/{tree_id}/media", {
+ params: { path: { tree_id: treeId } },
+ });
+ if (response.status === 401) {
+ router.push("/login");
+ return;
+ }
+ setItems(data ?? []);
+ setReady(true);
+ }, [router, treeId]);
+
+ useEffect(() => {
+ load();
+ }, [load]);
+
+ async function onFile(e: React.ChangeEvent) {
+ const file = e.target.files?.[0];
+ if (!file) return;
+ setUploading(true);
+ const fd = new FormData();
+ fd.append("file", file);
+ // Plain fetch for multipart (same origin → cookie auth via Caddy).
+ await fetch(`/api/v1/trees/${treeId}/media`, {
+ method: "POST",
+ body: fd,
+ credentials: "include",
+ });
+ setUploading(false);
+ if (fileRef.current) fileRef.current.value = "";
+ load();
+ }
+
+ async function remove(id: string) {
+ await api.DELETE("/api/v1/trees/{tree_id}/media/{media_id}", {
+ params: { path: { tree_id: treeId, media_id: id } },
+ });
+ load();
+ }
+
+ if (!ready) return Loading…
;
+
+ return (
+
+
+
Media
+
+
+ fileRef.current?.click()} disabled={uploading}>
+ {uploading ? "Uploading…" : "Upload file"}
+
+
+
+
+ {items.length === 0 ? (
+
+ No media yet — upload scans, photos, or documents and attach them to facts.
+
+ ) : (
+
+ )}
+
+ );
+}
diff --git a/frontend/app/trees/[id]/page.tsx b/frontend/app/trees/[id]/page.tsx
index 1d54034..2a46cc2 100644
--- a/frontend/app/trees/[id]/page.tsx
+++ b/frontend/app/trees/[id]/page.tsx
@@ -56,14 +56,7 @@ export default function TreeDetailPage() {
return (
-
-
- ← All trees
-
-
- Sources →
-
-
+
People
diff --git a/frontend/app/trees/[id]/persons/[personId]/page.tsx b/frontend/app/trees/[id]/persons/[personId]/page.tsx
index cf5ac97..b615063 100644
--- a/frontend/app/trees/[id]/persons/[personId]/page.tsx
+++ b/frontend/app/trees/[id]/persons/[personId]/page.tsx
@@ -22,6 +22,17 @@ type CitationCreate = components["schemas"]["CitationCreate"];
const fieldCls = "h-9 rounded-md border border-[var(--border)] bg-[var(--surface)] px-2 text-sm";
const QUALIFIERS: Qualifier[] = ["biological", "adoptive", "step", "foster", "donor", "guardian"];
+// Curated genealogical event vocabulary (with an escape hatch).
+const EVENT_TYPES = [
+ "birth", "death", "marriage", "divorce", "engagement", "baptism", "burial",
+ "residence", "census", "immigration", "emigration", "occupation", "education",
+ "military service", "naturalization", "other",
+];
+const MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
+const GED_MON = ["", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
+const DATE_QUALS: Record = { exact: "", about: "ABT", before: "BEF", after: "AFT" };
+const pad = (n: number, len: number) => String(n).padStart(len, "0");
+
export default function PersonDetailPage() {
const router = useRouter();
const params = useParams<{ id: string; personId: string }>();
@@ -37,7 +48,11 @@ export default function PersonDetailPage() {
const [ready, setReady] = useState(false);
const [evType, setEvType] = useState("birth");
- const [evDate, setEvDate] = useState("");
+ const [evTypeOther, setEvTypeOther] = useState("");
+ const [dateQual, setDateQual] = useState("exact");
+ const [dateDay, setDateDay] = useState("");
+ const [dateMonth, setDateMonth] = useState("");
+ const [dateYear, setDateYear] = useState("");
const [relKind, setRelKind] = useState<"parent" | "child" | "partner" | "sibling">("parent");
const [relOther, setRelOther] = useState("");
@@ -97,15 +112,40 @@ export default function PersonDetailPage() {
const eventCites = (id: string) => citations.filter((c) => c.event_id === id);
const personCites = citations.filter((c) => c.person_id === personId);
+ function buildDate() {
+ const year = dateYear.trim();
+ if (!year || Number.isNaN(Number(year))) {
+ return { date_value: null, date_start: null, date_precision: null };
+ }
+ const m = dateMonth ? Number(dateMonth) : null;
+ const d = dateDay.trim() ? Number(dateDay) : null;
+ const parts: string[] = [];
+ if (d && m) parts.push(String(d));
+ if (m) parts.push(GED_MON[m]);
+ parts.push(year);
+ const prefix = DATE_QUALS[dateQual];
+ return {
+ date_value: (prefix ? `${prefix} ` : "") + parts.join(" "),
+ date_start: `${pad(Number(year), 4)}-${pad(m ?? 1, 2)}-${pad(d ?? 1, 2)}`,
+ date_precision: dateQual,
+ };
+ }
+
async function addEvent(e: React.FormEvent) {
e.preventDefault();
- if (!evType.trim()) return;
+ const event_type = evType === "other" ? evTypeOther.trim() : evType;
+ if (!event_type) return;
+ const { date_value, date_start, date_precision } = buildDate();
const { error } = await api.POST("/api/v1/trees/{tree_id}/events", {
params: { path: { tree_id: treeId } },
- body: { event_type: evType, person_id: personId, date_value: evDate || null },
+ body: { event_type, person_id: personId, date_value, date_start, date_precision },
});
if (!error) {
- setEvDate("");
+ setDateDay("");
+ setDateMonth("");
+ setDateYear("");
+ setDateQual("exact");
+ setEvTypeOther("");
load();
}
}
@@ -305,9 +345,68 @@ export default function PersonDetailPage() {
))}
)}
-
diff --git a/frontend/app/trees/layout.tsx b/frontend/app/trees/layout.tsx
new file mode 100644
index 0000000..c1956f1
--- /dev/null
+++ b/frontend/app/trees/layout.tsx
@@ -0,0 +1,28 @@
+import Link from "next/link";
+
+import { AppSidebar } from "@/components/app-sidebar";
+
+export default function TreesLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+ {/* Compact bar for small screens (full sidebar is md+). */}
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+
+
+ Trees
+
+
+
+
{children}
+
+
+ );
+}
diff --git a/frontend/app/trees/page.tsx b/frontend/app/trees/page.tsx
index 691ad23..b2d9e52 100644
--- a/frontend/app/trees/page.tsx
+++ b/frontend/app/trees/page.tsx
@@ -42,21 +42,11 @@ export default function TreesPage() {
}
}
- async function logout() {
- await api.POST("/api/v1/auth/logout");
- router.push("/login");
- }
-
if (!ready) return Loading…
;
return (
-
-
Your trees
-
- Sign out
-
-
+
Your trees
diff --git a/frontend/components/app-sidebar.tsx b/frontend/components/app-sidebar.tsx
new file mode 100644
index 0000000..4631227
--- /dev/null
+++ b/frontend/components/app-sidebar.tsx
@@ -0,0 +1,102 @@
+"use client";
+
+import { BookText, FolderTree, Image as ImageIcon, LogOut, Users } from "lucide-react";
+import Link from "next/link";
+import { usePathname, useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+import { api } from "@/lib/api/client";
+import { cn } from "@/lib/utils";
+
+export function AppSidebar() {
+ const pathname = usePathname();
+ const router = useRouter();
+ const segs = pathname.split("/").filter(Boolean); // ["trees", "", ...]
+ const treeId = segs[0] === "trees" && segs[1] ? segs[1] : null;
+ const [treeName, setTreeName] = useState(null);
+
+ useEffect(() => {
+ if (!treeId) {
+ setTreeName(null);
+ return;
+ }
+ api
+ .GET("/api/v1/trees/{tree_id}", { params: { path: { tree_id: treeId } } })
+ .then((r) => setTreeName(r.data?.name ?? null));
+ }, [treeId]);
+
+ async function logout() {
+ await api.POST("/api/v1/auth/logout");
+ router.push("/login");
+ }
+
+ const Item = ({
+ href,
+ label,
+ icon: Icon,
+ active,
+ }: {
+ href: string;
+ label: string;
+ icon: typeof Users;
+ active: boolean;
+ }) => (
+
+
+ {label}
+
+ );
+
+ return (
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+
+
+
+
+ {treeId && (
+
+
+ {treeName ?? "Tree"}
+
+
+
+
+
+ )}
+
+
+
+ Sign out
+
+
+ );
+}
diff --git a/frontend/lib/api/schema.d.ts b/frontend/lib/api/schema.d.ts
index 0234c29..22d8765 100644
--- a/frontend/lib/api/schema.d.ts
+++ b/frontend/lib/api/schema.d.ts
@@ -400,10 +400,75 @@ export interface paths {
patch?: never;
trace?: never;
};
+ "/api/v1/trees/{tree_id}/media": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List Media */
+ get: operations["list_media_api_v1_trees__tree_id__media_get"];
+ put?: never;
+ /** Upload Media */
+ post: operations["upload_media_api_v1_trees__tree_id__media_post"];
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/trees/{tree_id}/media/{media_id}/content": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Media Content */
+ get: operations["media_content_api_v1_trees__tree_id__media__media_id__content_get"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/api/v1/trees/{tree_id}/media/{media_id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ get?: never;
+ put?: never;
+ post?: never;
+ /** Delete Media */
+ delete: operations["delete_media_api_v1_trees__tree_id__media__media_id__delete"];
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
}
export type webhooks = Record;
export interface components {
schemas: {
+ /** Body_upload_media_api_v1_trees__tree_id__media_post */
+ Body_upload_media_api_v1_trees__tree_id__media_post: {
+ /** File */
+ file: string;
+ /** Title */
+ title?: string | null;
+ /** Person Id */
+ person_id?: string | null;
+ /** Event Id */
+ event_id?: string | null;
+ /** Source Id */
+ source_id?: string | null;
+ };
/**
* CitationConfidence
* @enum {string}
@@ -546,6 +611,42 @@ export interface components {
/** Password */
password: string;
};
+ /** MediaRead */
+ MediaRead: {
+ /**
+ * Id
+ * Format: uuid
+ */
+ id: string;
+ /**
+ * Tree Id
+ * Format: uuid
+ */
+ tree_id: string;
+ /** Original Filename */
+ original_filename: string;
+ /** Content Type */
+ content_type: string;
+ /** Byte Size */
+ byte_size: number;
+ /** Checksum Sha256 */
+ checksum_sha256: string;
+ /** Title */
+ title: string | null;
+ /** Person Id */
+ person_id: string | null;
+ /** Event Id */
+ event_id: string | null;
+ /** Source Id */
+ source_id: string | null;
+ /**
+ * Created At
+ * Format: date-time
+ */
+ created_at: string;
+ /** Url */
+ url?: string | null;
+ };
/**
* ParentChildQualifier
* @description Qualifies a parent_child edge so adoption/donor/blended families are
@@ -1666,4 +1767,132 @@ export interface operations {
};
};
};
+ list_media_api_v1_trees__tree_id__media_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"]["MediaRead"][];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ upload_media_api_v1_trees__tree_id__media_post: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ tree_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody: {
+ content: {
+ "multipart/form-data": components["schemas"]["Body_upload_media_api_v1_trees__tree_id__media_post"];
+ };
+ };
+ responses: {
+ /** @description Successful Response */
+ 201: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["MediaRead"];
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ media_content_api_v1_trees__tree_id__media__media_id__content_get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ tree_id: string;
+ media_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": unknown;
+ };
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
+ delete_media_api_v1_trees__tree_id__media__media_id__delete: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ tree_id: string;
+ media_id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Successful Response */
+ 204: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content?: never;
+ };
+ /** @description Validation Error */
+ 422: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["HTTPValidationError"];
+ };
+ };
+ };
+ };
}
diff --git a/frontend/openapi.json b/frontend/openapi.json
index 76534b9..2cbfbcc 100644
--- a/frontend/openapi.json
+++ b/frontend/openapi.json
@@ -1188,10 +1188,266 @@
}
}
}
+ },
+ "/api/v1/trees/{tree_id}/media": {
+ "post": {
+ "tags": [
+ "media"
+ ],
+ "summary": "Upload Media",
+ "operationId": "upload_media_api_v1_trees__tree_id__media_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_upload_media_api_v1_trees__tree_id__media_post"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/MediaRead"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ },
+ "get": {
+ "tags": [
+ "media"
+ ],
+ "summary": "List Media",
+ "operationId": "list_media_api_v1_trees__tree_id__media_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/MediaRead"
+ },
+ "title": "Response List Media Api V1 Trees Tree Id Media Get"
+ }
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/trees/{tree_id}/media/{media_id}/content": {
+ "get": {
+ "tags": [
+ "media"
+ ],
+ "summary": "Media Content",
+ "operationId": "media_content_api_v1_trees__tree_id__media__media_id__content_get",
+ "parameters": [
+ {
+ "name": "tree_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid",
+ "title": "Tree Id"
+ }
+ },
+ {
+ "name": "media_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid",
+ "title": "Media Id"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {}
+ }
+ }
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/trees/{tree_id}/media/{media_id}": {
+ "delete": {
+ "tags": [
+ "media"
+ ],
+ "summary": "Delete Media",
+ "operationId": "delete_media_api_v1_trees__tree_id__media__media_id__delete",
+ "parameters": [
+ {
+ "name": "tree_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid",
+ "title": "Tree Id"
+ }
+ },
+ {
+ "name": "media_id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid",
+ "title": "Media Id"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "Successful Response"
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ }
+ }
+ }
+ }
}
},
"components": {
"schemas": {
+ "Body_upload_media_api_v1_trees__tree_id__media_post": {
+ "properties": {
+ "file": {
+ "type": "string",
+ "contentMediaType": "application/octet-stream",
+ "title": "File"
+ },
+ "title": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Title"
+ },
+ "person_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "uuid"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Person Id"
+ },
+ "event_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "uuid"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Event Id"
+ },
+ "source_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "uuid"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Id"
+ }
+ },
+ "type": "object",
+ "required": [
+ "file"
+ ],
+ "title": "Body_upload_media_api_v1_trees__tree_id__media_post"
+ },
"CitationConfidence": {
"type": "string",
"enum": [
@@ -1716,6 +1972,114 @@
],
"title": "LoginRequest"
},
+ "MediaRead": {
+ "properties": {
+ "id": {
+ "type": "string",
+ "format": "uuid",
+ "title": "Id"
+ },
+ "tree_id": {
+ "type": "string",
+ "format": "uuid",
+ "title": "Tree Id"
+ },
+ "original_filename": {
+ "type": "string",
+ "title": "Original Filename"
+ },
+ "content_type": {
+ "type": "string",
+ "title": "Content Type"
+ },
+ "byte_size": {
+ "type": "integer",
+ "title": "Byte Size"
+ },
+ "checksum_sha256": {
+ "type": "string",
+ "title": "Checksum Sha256"
+ },
+ "title": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Title"
+ },
+ "person_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "uuid"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Person Id"
+ },
+ "event_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "uuid"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Event Id"
+ },
+ "source_id": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "uuid"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Source Id"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time",
+ "title": "Created At"
+ },
+ "url": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "title": "Url"
+ }
+ },
+ "type": "object",
+ "required": [
+ "id",
+ "tree_id",
+ "original_filename",
+ "content_type",
+ "byte_size",
+ "checksum_sha256",
+ "title",
+ "person_id",
+ "event_id",
+ "source_id",
+ "created_at"
+ ],
+ "title": "MediaRead"
+ },
"ParentChildQualifier": {
"type": "string",
"enum": [