{"openapi":"3.1.0","info":{"title":"OriLife API — định danh bằng ảnh","description":"Định danh cây, quả và con vật bằng chính ảnh chụp chúng — không tem, không QR, không mã gắn lên vật.\n\n**Xác thực** — `POST /api/signup` hoặc `POST /api/login` trả về `token`; gửi kèm mọi lượt gọi\ncần quyền bằng header `Authorization: Bearer <token>`. Token sống mặc định 12 giờ.\n\n**Hai lối vào** — lối công khai (không cần tài khoản): `/api/fruit/lookup`, `/api/fruit/scan`,\n`/api/resolve/{code}`, `/api/species/catalog`, `/api/health`. Lối chủ vườn (cần token):\n`/api/enroll`, `/api/identify`, `/api/fruit/*`, `/api/animal/*`, dòng thời gian và neo bằng chứng.\n\n**Khuôn lỗi** — mọi cửa trả JSON có trường `ok`; hỏng thì `ok:false` kèm `error` là câu tiếng Việt\nđưa thẳng ra màn hình được. `429` luôn kèm header `Retry-After` tính bằng giây.\n\n**Trần tải lên** — 20 MB một tệp, 64 MB một lô ảnh, 80 MB video.\n\nHướng dẫn cho người tích hợp: [trang chủ API](/). Giao diện dùng thử tại vườn: [/TreeIdentity](/TreeIdentity).\n","version":"2026.07.01"},"paths":{"/api/signup":{"post":{"summary":"Signup","operationId":"signup_api_signup_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_signup_api_signup_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/login":{"post":{"summary":"Login","operationId":"login_api_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_login_api_login_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/logout":{"post":{"summary":"Logout","operationId":"logout_api_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/logout-all":{"post":{"summary":"Logout All","operationId":"logout_all_api_logout_all_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/me":{"get":{"summary":"Me","operationId":"me_api_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/auth/did/challenge":{"get":{"summary":"Did Challenge","description":"Phát challenge nonce. Rate-limit nhẹ theo IP (chống bơm-cạn bộ-nhớ challenge).","operationId":"did_challenge_api_auth_did_challenge_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/auth/did/verify":{"post":{"summary":"Did Verify","description":"Verify chữ-ký DID trên challenge → cấp token field-reid.","operationId":"did_verify_api_auth_did_verify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Body_did_verify_api_auth_did_verify_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/population/count":{"post":{"tags":["population"],"summary":"Api Record Count","description":"Ghi số lượng đếm, trả về alerts phát sinh (nếu có). Chỉ chủ vườn ghi được.","operationId":"api_record_count_api_population_count_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CountRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CountResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/population/farm/{farm_id}":{"get":{"tags":["population"],"summary":"Api Farm Dashboard","description":"Dashboard tổng hợp: tổng đầu con + trạng thái từng zone (vườn của mình).","operationId":"api_farm_dashboard_api_population_farm__farm_id__get","parameters":[{"name":"farm_id","in":"path","required":true,"schema":{"type":"string","title":"Farm Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/population/alerts/{farm_id}":{"get":{"tags":["population"],"summary":"Api Get Alerts","description":"Danh sách alerts chưa resolved trong 48h (vườn của mình).","operationId":"api_get_alerts_api_population_alerts__farm_id__get","parameters":[{"name":"farm_id","in":"path","required":true,"schema":{"type":"string","title":"Farm Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertOut"},"title":"Response Api Get Alerts Api Population Alerts  Farm Id  Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/care/match":{"post":{"tags":["care"],"summary":"Nhận-diện sản phẩm từ chữ/ảnh bao bì","description":"Trả danh sách ứng viên (tên + thông tin), KHÔNG có score. Có text → match thẳng;\nchỉ có ảnh + OCR khả dụng → OCR ra chữ rồi match.\n\nDanh sách rỗng LUÔN kèm `reason` nói vì đâu rỗng, vì bốn lý do rỗng đòi bốn hành động\nkhác nhau: `ocr_unavailable` (máy chủ chưa đọc được nhãn — chờ cũng không ra, ghi tay),\n`ocr_no_text` (đọc rồi mà không ra chữ — chụp lại gần hơn, đủ sáng), `no_match` (đọc ra\nchữ nhưng kho chưa có nhãn này), `no_input` (chưa gửi gì). Trước đây cả bốn cùng ra\n`candidates: []` nên màn hình phải tự đoán, và nó đoán sai về phía có lợi cho máy.\n\n── BA TRƯỜNG MÀ MÀN HÌNH PHẢI ĐỌC, VÀ BA CÂU HỎI KHÁC NHAU ────────────────────────\n\n`reason` trả lời **\"vì sao danh sách rỗng\"**. Nó CHỈ có mặt khi danh sách rỗng, cộng\nthêm giá trị `ambiguous` cho ca danh sách không rỗng nhưng thứ tự không đáng tin. Đừng\ndùng nó làm tín hiệu nguy hiểm: nhãn mang chất cấm mà kho vẫn khớp được sản phẩm thì\ndanh sách KHÔNG rỗng, nên `reason` vắng mặt — mà đó đúng là lượt nguy hiểm nhất.\n\n`banned` trả lời **\"thứ này có chất cấm không\"**. Danh sách không rỗng ⟹ NGUY HIỂM, bất\nkể `reason` mang gì và bất kể `candidates` có bao nhiêu thẻ. Đây là trường màn hình phải\nrẽ nhánh theo khi muốn biết có cảnh báo hay không.\n\n`banned_check` trả lời **\"phần chất cấm có chạy được không\"** — BA giá trị, LUÔN có mặt\nở mọi lượt trả lời kể cả các lối thoát sớm:\n\n- `\"ok\"` — đã tra danh mục. `banned: []` lúc này nghĩa là SẠCH.\n- `\"unavailable\"` — danh mục nạp không được, phần chất cấm chưa chạy. KHÔNG phải lỗi\n  mạng và không phải lý do thử lại: đó là trạng thái suy giảm của máy chủ, lượt tra vẫn\n  có giá trị cho phần nhận-diện sản phẩm.\n- `\"not_run\"` — không có chữ nào để đem đi tra (không gửi gì, hoặc đọc chữ trên ảnh\n  không ra). Danh mục có thể đang hoàn toàn lành.\n\nBa trạng thái này `banned: []` một mình không tách nổi, và hai trạng thái sau KHÔNG được\nnhập vào nhau: một bên bảo người dùng chờ máy chủ, một bên bảo họ chụp lại.\n\n**Bên đọc rẽ nhánh bằng phép so BẰNG với `\"ok\"`, đừng liệt kê các giá trị còn lại.** Mọi\nthứ khác `\"ok\"` đọc thành \"chưa tra được\", kể cả giá trị mà bản app đang chạy chưa từng\nthấy. Ngược lại — liệt kê những giá trị đã biết rồi để `else` rơi vào \"sạch\" — thì mỗi\nlần trường này mọc thêm một trạng thái là một lần app cũ nói sai theo chiều nguy hiểm,\nvà không có gì báo.\n\nQuan hệ giữa chúng, viết ra vì nó KHÔNG đối xứng: `reason == \"banned_not_in_catalog\"`\nkéo theo `banned` khác rỗng VÀ `banned_check == \"ok\"` — kho hỏng thì `check_text` trả\nrỗng nên lý do sẽ là `no_match`. Chiều ngược lại KHÔNG đúng: `banned` khác rỗng mà\n`reason` vắng hẳn là chuyện bình thường.\n\n`message` là câu hoàn chỉnh cho người dùng, ghép từ mọi tình huống đang đúng cùng lúc\n(chất cấm · thứ tự không đáng tin · danh mục chưa tra được), xếp theo nguy hiểm giảm\ndần. Hiện NGUYÊN VĂN nó, đừng dựng câu từ mã lý do: mã lý do không nói được ba việc\ncùng lúc. Nó có mặt bất cứ khi nào có điều đáng nói, kể cả lượt khớp đẹp.","operationId":"care_match_api_care_match_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_care_match_api_care_match_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/care/log":{"post":{"tags":["care"],"summary":"Ghi nhật ký chăm sóc (lưu ảnh bao bì nếu có)","operationId":"care_log_api_care_log_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_care_log_api_care_log_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/care/events":{"get":{"tags":["care"],"summary":"Nhật ký chăm sóc của 1 đối tượng","operationId":"care_events_api_care_events_get","parameters":[{"name":"target_type","in":"query","required":true,"schema":{"type":"string","title":"Target Type"}},{"name":"target_id","in":"query","required":true,"schema":{"type":"string","title":"Target Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/care/withdrawal":{"get":{"tags":["care"],"summary":"Trạng thái cách ly (chặn thu hoạch/bán)","operationId":"care_withdrawal_api_care_withdrawal_get","parameters":[{"name":"target_type","in":"query","required":true,"schema":{"type":"string","title":"Target Type"}},{"name":"target_id","in":"query","required":true,"schema":{"type":"string","title":"Target Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/care/products":{"get":{"tags":["care"],"summary":"Kho sản phẩm (cache offline, chọn tay)","operationId":"care_products_api_care_products_get","parameters":[{"name":"scope","in":"query","required":false,"schema":{"type":"string","title":"Scope"}},{"name":"category","in":"query","required":false,"schema":{"type":"string","title":"Category"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/care/banned":{"get":{"tags":["care"],"summary":"Danh mục hoạt chất cấm/hạn chế (toàn cầu)","description":"Danh mục hoạt chất bị cấm hoặc hạn chế trong nông nghiệp — trồng trọt, chăn nuôi và\nthuỷ sản, phạm vi toàn cầu chứ không riêng Việt Nam.\n\n`q` có chữ thì cửa này chạy đúng phép dò của `/api/care/match` trên chữ đó, nên app tra\nđược một nhãn mà không phải ghi gì vào sổ. Mỗi dòng kèm `sources` trỏ về văn bản gốc:\nngười dùng phải kiểm được, không phải tin.","operationId":"care_banned_list_api_care_banned_get","parameters":[{"name":"q","in":"query","required":false,"schema":{"type":"string","title":"Q"}},{"name":"scope","in":"query","required":false,"schema":{"type":"string","title":"Scope"}},{"name":"severity","in":"query","required":false,"schema":{"type":"string","title":"Severity"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/care/delete":{"post":{"tags":["care"],"summary":"Xoá 1 lần chăm sóc (chỉ của chính mình)","operationId":"care_delete_api_care_delete_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_care_delete_api_care_delete_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/residue/interpret":{"post":{"tags":["residue"],"summary":"Diễn-giải dư-lượng lô → verdict + cảnh-báo (read-only)","description":"Diễn-giải tập phép đo (đã đọc từ VeData) → verdict + FarmerAlert.\n\nKHÔNG đo, KHÔNG neo, KHÔNG ghi. Trả verdict 5 mức + cảnh-báo thân-thiện + NV-EXPORT\n(advisory) + (tuỳ chọn) cấp rủi-ro-vùng Cd. KHÔNG lộ score/biên kỹ-thuật (giấu nội-tạng).","operationId":"residue_interpret_api_residue_interpret_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InterpretIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/residue/interpret-by-stamps":{"post":{"tags":["residue"],"summary":"Diễn-giải dư-lượng lô theo stamp_id VeData (resolve qua Query)","description":"Diễn-giải lô theo TẬP stamp_id: server resolve qua Query rồi decode → verdict.\n\nGiảm rủi-ro INV-RES-13 (caller dựng/chọn tập-con phép đo giấu FAIL): giá-trị đo đến từ\nVeData qua resolver, không do body mang trực-tiếp. Resolve lỗi → UNKNOWN (fail-closed).\nKHÔNG đo, KHÔNG neo, KHÔNG ghi.","operationId":"residue_interpret_by_stamps_api_residue_interpret_by_stamps_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InterpretByStampsIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/animal/enroll":{"post":{"tags":["animal"],"summary":"Đăng ký cá thể mới (≥3 ảnh đa góc)","operationId":"animal_enroll_animal_enroll_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_animal_enroll_animal_enroll_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/animal/identify":{"post":{"tags":["animal"],"summary":"Nhận diện cá thể từ 1 ảnh","operationId":"animal_identify_animal_identify_post","parameters":[{"name":"channels","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"MCR-PoC-Animal L1c — A/B kênh biometric: 'body,face,bio' (subset). Mặc-định None = full 4 kênh (BYTE-IDENTICAL).","title":"Channels"},"description":"MCR-PoC-Animal L1c — A/B kênh biometric: 'body,face,bio' (subset). Mặc-định None = full 4 kênh (BYTE-IDENTICAL)."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_animal_identify_animal_identify_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/animal/identify_verdict":{"post":{"tags":["animal"],"summary":"Ghi đúng/sai cho 1 lượt scan/identify (PoC đo accuracy)","description":"MCR-PoC-Animal L2 — GHI GROUND-TRUTH cho 1 lượt /animal/identify hoặc /animal/scan\n(join bằng query_id). Mirror /api/fruit/identify_verdict (quả), nhưng cho CON VẬT.\n\nverdict ∈ {correct, wrong, unknown_ok, other}:\n  • correct    — hệ đoán ĐÚNG con (top-1).\n  • wrong      — sai; nếu biết con đúng → gửi kèm correct_did.\n  • other      — 'là con khác' (chọn con đúng qua correct_did), HOẶC con-lạ nếu không chọn được.\n  • unknown_ok — con CHƯA enroll & hệ trả \"chưa biết\" ĐÚNG (open-set true-negative).\ncorrect_did (tùy) = cá-thể ĐÚNG (client lấy từ /animal/list), CHỈ tin khi là cá-thể của\nCHÍNH CHỦ (chống ghi/dò cá-thể người khác; sai-chủ → bỏ nhãn, KHÔNG 403 vì đây là nhãn-đo).\nGhi 1 dòng ground_truth_animal.jsonl. KHÔNG lộ nội-tạng so-khớp ra client.","operationId":"animal_identify_verdict_animal_identify_verdict_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_animal_identify_verdict_animal_identify_verdict_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/animal/verify":{"post":{"tags":["animal"],"summary":"Xác thực ảnh đúng là cá thể đã đăng ký","operationId":"animal_verify_animal_verify_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_animal_verify_animal_verify_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/animal/drift/report":{"get":{"tags":["animal"],"summary":"Báo cáo drift tăng-trưởng + cảnh báo va chạm cá thể","description":"Quét trại: cá thể nào cần chụp lại (đã drift) + cặp nào sắp NHẦM (gap co lại) + kênh\nnên chụp thêm. Đo từ dữ liệu đã có (views + view_ts) — chủ động tránh nhầm khi con lớn lên.\n(path 3 đoạn → không đụng route động /{animal_did}.)","operationId":"drift_report_animal_drift_report_get","parameters":[{"name":"farm_id","in":"query","required":true,"schema":{"type":"string","title":"Farm Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/animal/list":{"get":{"tags":["animal"],"summary":"Danh sách cá thể (phân trang)","operationId":"animal_list_animal_list_get","parameters":[{"name":"farm_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Farm Id"}},{"name":"species","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Species"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/animal/demo":{"get":{"tags":["animal"],"summary":"Trang demo HTML (mở trên điện thoại)","operationId":"demo_page_animal_demo_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/animal/field":{"get":{"tags":["animal"],"summary":"Trang THỰC ĐỊA: enroll + quét nhận diện (mở trên điện thoại)","operationId":"field_page_animal_field_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/animal/detect":{"post":{"tags":["animal"],"summary":"Tự nhận loài theo hồ sơ trại (1 nút Quét)","description":"Quyết định loài cho luồng 1-nút:\n- Trại MỚI (chưa con nào) → cần chọn loài (candidates = mọi loài).\n- Trại ĐƠN-LOÀI → trả ngay loài đó (need_confirm=False, KHÔNG cần ảnh/YOLO).\n- Trại ĐA-LOÀI → YOLO detect trong tập loài của trại; chắc + không nhóm-dễ-nhầm → tự chạy;\n  ngược lại (mơ hồ / nhóm dễ nhầm / không có YOLO) → hỏi xác nhận với danh sách thu hẹp.","operationId":"animal_detect_animal_detect_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_animal_detect_animal_detect_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/animal/scan":{"post":{"tags":["animal"],"summary":"1 nút: tự nhận loài + nhận cá thể từ 1 ảnh","operationId":"animal_scan_animal_scan_post","parameters":[{"name":"channels","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"MCR-PoC-Animal L1c — A/B kênh biometric: 'body,face,bio' (subset). Mặc-định None = full 4 kênh (BYTE-IDENTICAL).","title":"Channels"},"description":"MCR-PoC-Animal L1c — A/B kênh biometric: 'body,face,bio' (subset). Mặc-định None = full 4 kênh (BYTE-IDENTICAL)."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_animal_scan_animal_scan_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/animal/{animal_did}":{"get":{"tags":["animal"],"summary":"Metadata cá thể (KHÔNG trả embeddings)","description":"?describe=1 (LAZY, mặc-định TẮT): thêm `features_vi` — câu tiếng Việt nói phần nào của\ncon này chụp lại ăn khớp nhất, phần nào mỗi lần một khác. Cùng tên trường, cùng dải\n{cao,vừa,thấp} với `/api/tree_views?describe=1` và `/api/fruit/{id}/views?describe=1`.\n\nKhối mô-tả nằm ở CẤP CÁ THỂ chứ không theo từng ảnh như cây và quả, vì thứ đo được ở con\nvật là các lần chụp có ăn khớp nhau không — một tấm đứng riêng thì không có gì để so.","operationId":"animal_get_animal__animal_did__get","parameters":[{"name":"animal_did","in":"path","required":true,"schema":{"type":"string","title":"Animal Did"}},{"name":"describe","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Describe"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["animal"],"summary":"Xóa cá thể (embeddings) — có audit log","operationId":"animal_delete_animal__animal_did__delete","parameters":[{"name":"animal_did","in":"path","required":true,"schema":{"type":"string","title":"Animal Did"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/animal/{animal_did}/model3d":{"get":{"tags":["animal"],"summary":"Dữ-liệu 3D của cá thể (mô-hình, hoặc lý-do không có)","description":"Cửa 3D của con vật — CÙNG hình-dạng với cửa cây và cửa quả, và nói ĐÚNG thứ có trên đĩa.\n\nBa câu trả lời, ba màn hình khác nhau ở phía app (đọc `available` + `meta.reason_code`):\n  • chưa từng dựng cho con này → `available: false`, `reason_code` = `chua_co_lan_dung_...`;\n  • đã dựng và **cổng chặn**   → `available: false`, `status: failed`, `reason_code` là mã\n    của cổng (`animal_recon3d.GATE_*`) kèm `meta.gate` là các số đo;\n  • có mô-hình                 → `available: true`, kèm `meta.trust` (bao nhiêu điểm nằm\n    trên mình con vật, cả cảnh bao nhiêu, có thưa quá không).\n\nVì sao khoản `trust` là BẮT BUỘC chứ không phải trang trí: `recon3d` chạy trên cảnh có con\nvật cử động KHÔNG báo lỗi — nó dựng ra cái CHUỒNG rồi khen \"đã đi gần trọn vòng\". Một cửa\ntrả đám mây điểm mà không nói nó đáng tin tới đâu chính là đường để lỗi đó lên thẳng màn\nhình người nuôi. Cổng chặn ở `animal_recon3d`, còn cửa này có nhiệm vụ KHÔNG che nó đi.\n\nCửa này chỉ ĐỌC. Chưa có route nào nạp ảnh để dựng cho con vật (đường `/animal/enroll`\nkhông giữ ảnh gốc), nên với đa số cá thể câu trả lời sẽ là câu thứ nhất — và đó là sự thật,\nkhông phải lỗi.\n\n404 đồng nhất cho 'không tồn tại' và 'không thuộc bạn', đúng luật mọi cửa GET của router này.","operationId":"animal_model3d_animal__animal_did__model3d_get","parameters":[{"name":"animal_did","in":"path","required":true,"schema":{"type":"string","title":"Animal Did"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/animal/rename":{"post":{"tags":["animal"],"summary":"Đổi tên cá thể","description":"Đổi tên một cá thể.\n\nVì sao cửa này phải có: kho đã có sẵn `rename_animal` từ lâu nhưng không route nào gọi tới,\nnên nút \"Đổi tên\" bên app chỉ ghi vào một biến trong màn hình. Nông dân đặt tên cả đàn, kéo\nlàm mới một cái là tên cũ trở lại, và không một dòng nào báo. Mất dữ liệu trong im lặng.\n\nTên rỗng sau khi vệ sinh → 200 kèm `ok:false` (không phải lỗi tầng vận chuyển), cùng khuôn\n`POST /api/rename` của cây.","operationId":"animal_rename_animal_rename_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_animal_rename_animal_rename_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/animal/fee/estimate":{"get":{"tags":["animal"],"summary":"Ước lượng phí định danh (công khai)","operationId":"fee_estimate_animal_fee_estimate_get","parameters":[{"name":"species","in":"query","required":false,"schema":{"type":"string","default":"cattle","title":"Species"}},{"name":"value_usd","in":"query","required":false,"schema":{"type":"number","default":0.0,"title":"Value Usd"}},{"name":"lifecycle_events","in":"query","required":false,"schema":{"type":"integer","default":7,"title":"Lifecycle Events"}},{"name":"anchor_tier","in":"query","required":false,"schema":{"type":"string","default":"batch_daily","title":"Anchor Tier"}},{"name":"demand_factor","in":"query","required":false,"schema":{"type":"number","default":1.0,"title":"Demand Factor"}},{"name":"lamp_usd","in":"query","required":false,"schema":{"type":"number","default":0.0,"title":"Lamp Usd"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/animal/trust/tiers":{"get":{"tags":["animal"],"summary":"Bảng độ tin cậy theo anchor tier (công khai)","operationId":"trust_tiers_animal_trust_tiers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/animal/trust/verify-inclusion":{"post":{"tags":["animal"],"summary":"Verify record thuộc root on-chain (công khai)","description":"Verify 1 record thuộc Merkle root đã neo Cardano — bất kỳ ai cũng gọi được.\n\nBody: {record_cbor_hex | leaf_hex, proof_path:[{sibling,right}], root_hex, salt_hex?}.\nTrả {verified: bool}. Đây là tính chất 'trustless verify' — không cần tin LampNet.","operationId":"trust_verify_animal_trust_verify_inclusion_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/animal/provenance/demo":{"get":{"tags":["animal"],"summary":"Chuỗi truy xuất mẫu (bò → gói thịt) cho consumer","description":"Sinh 1 chuỗi lineage mẫu + inclusion proof thật để minh hoạ luồng người tiêu dùng.","operationId":"provenance_demo_animal_provenance_demo_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/animal/enroll":{"post":{"tags":["animal"],"summary":"Đăng ký cá thể mới (≥3 ảnh đa góc)","operationId":"animal_enroll_api_animal_enroll_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_animal_enroll_api_animal_enroll_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/animal/identify":{"post":{"tags":["animal"],"summary":"Nhận diện cá thể từ 1 ảnh","operationId":"animal_identify_api_animal_identify_post","parameters":[{"name":"channels","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"MCR-PoC-Animal L1c — A/B kênh biometric: 'body,face,bio' (subset). Mặc-định None = full 4 kênh (BYTE-IDENTICAL).","title":"Channels"},"description":"MCR-PoC-Animal L1c — A/B kênh biometric: 'body,face,bio' (subset). Mặc-định None = full 4 kênh (BYTE-IDENTICAL)."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_animal_identify_api_animal_identify_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/animal/identify_verdict":{"post":{"tags":["animal"],"summary":"Ghi đúng/sai cho 1 lượt scan/identify (PoC đo accuracy)","description":"MCR-PoC-Animal L2 — GHI GROUND-TRUTH cho 1 lượt /animal/identify hoặc /animal/scan\n(join bằng query_id). Mirror /api/fruit/identify_verdict (quả), nhưng cho CON VẬT.\n\nverdict ∈ {correct, wrong, unknown_ok, other}:\n  • correct    — hệ đoán ĐÚNG con (top-1).\n  • wrong      — sai; nếu biết con đúng → gửi kèm correct_did.\n  • other      — 'là con khác' (chọn con đúng qua correct_did), HOẶC con-lạ nếu không chọn được.\n  • unknown_ok — con CHƯA enroll & hệ trả \"chưa biết\" ĐÚNG (open-set true-negative).\ncorrect_did (tùy) = cá-thể ĐÚNG (client lấy từ /animal/list), CHỈ tin khi là cá-thể của\nCHÍNH CHỦ (chống ghi/dò cá-thể người khác; sai-chủ → bỏ nhãn, KHÔNG 403 vì đây là nhãn-đo).\nGhi 1 dòng ground_truth_animal.jsonl. KHÔNG lộ nội-tạng so-khớp ra client.","operationId":"animal_identify_verdict_api_animal_identify_verdict_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_animal_identify_verdict_api_animal_identify_verdict_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/animal/verify":{"post":{"tags":["animal"],"summary":"Xác thực ảnh đúng là cá thể đã đăng ký","operationId":"animal_verify_api_animal_verify_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_animal_verify_api_animal_verify_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/animal/drift/report":{"get":{"tags":["animal"],"summary":"Báo cáo drift tăng-trưởng + cảnh báo va chạm cá thể","description":"Quét trại: cá thể nào cần chụp lại (đã drift) + cặp nào sắp NHẦM (gap co lại) + kênh\nnên chụp thêm. Đo từ dữ liệu đã có (views + view_ts) — chủ động tránh nhầm khi con lớn lên.\n(path 3 đoạn → không đụng route động /{animal_did}.)","operationId":"drift_report_api_animal_drift_report_get","parameters":[{"name":"farm_id","in":"query","required":true,"schema":{"type":"string","title":"Farm Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/animal/list":{"get":{"tags":["animal"],"summary":"Danh sách cá thể (phân trang)","operationId":"animal_list_api_animal_list_get","parameters":[{"name":"farm_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Farm Id"}},{"name":"species","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Species"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/animal/demo":{"get":{"tags":["animal"],"summary":"Trang demo HTML (mở trên điện thoại)","operationId":"demo_page_api_animal_demo_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/api/animal/field":{"get":{"tags":["animal"],"summary":"Trang THỰC ĐỊA: enroll + quét nhận diện (mở trên điện thoại)","operationId":"field_page_api_animal_field_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/api/animal/detect":{"post":{"tags":["animal"],"summary":"Tự nhận loài theo hồ sơ trại (1 nút Quét)","description":"Quyết định loài cho luồng 1-nút:\n- Trại MỚI (chưa con nào) → cần chọn loài (candidates = mọi loài).\n- Trại ĐƠN-LOÀI → trả ngay loài đó (need_confirm=False, KHÔNG cần ảnh/YOLO).\n- Trại ĐA-LOÀI → YOLO detect trong tập loài của trại; chắc + không nhóm-dễ-nhầm → tự chạy;\n  ngược lại (mơ hồ / nhóm dễ nhầm / không có YOLO) → hỏi xác nhận với danh sách thu hẹp.","operationId":"animal_detect_api_animal_detect_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_animal_detect_api_animal_detect_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/animal/scan":{"post":{"tags":["animal"],"summary":"1 nút: tự nhận loài + nhận cá thể từ 1 ảnh","operationId":"animal_scan_api_animal_scan_post","parameters":[{"name":"channels","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"MCR-PoC-Animal L1c — A/B kênh biometric: 'body,face,bio' (subset). Mặc-định None = full 4 kênh (BYTE-IDENTICAL).","title":"Channels"},"description":"MCR-PoC-Animal L1c — A/B kênh biometric: 'body,face,bio' (subset). Mặc-định None = full 4 kênh (BYTE-IDENTICAL)."}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_animal_scan_api_animal_scan_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/animal/{animal_did}":{"get":{"tags":["animal"],"summary":"Metadata cá thể (KHÔNG trả embeddings)","description":"?describe=1 (LAZY, mặc-định TẮT): thêm `features_vi` — câu tiếng Việt nói phần nào của\ncon này chụp lại ăn khớp nhất, phần nào mỗi lần một khác. Cùng tên trường, cùng dải\n{cao,vừa,thấp} với `/api/tree_views?describe=1` và `/api/fruit/{id}/views?describe=1`.\n\nKhối mô-tả nằm ở CẤP CÁ THỂ chứ không theo từng ảnh như cây và quả, vì thứ đo được ở con\nvật là các lần chụp có ăn khớp nhau không — một tấm đứng riêng thì không có gì để so.","operationId":"animal_get_api_animal__animal_did__get","parameters":[{"name":"animal_did","in":"path","required":true,"schema":{"type":"string","title":"Animal Did"}},{"name":"describe","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Describe"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["animal"],"summary":"Xóa cá thể (embeddings) — có audit log","operationId":"animal_delete_api_animal__animal_did__delete","parameters":[{"name":"animal_did","in":"path","required":true,"schema":{"type":"string","title":"Animal Did"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/animal/{animal_did}/model3d":{"get":{"tags":["animal"],"summary":"Dữ-liệu 3D của cá thể (mô-hình, hoặc lý-do không có)","description":"Cửa 3D của con vật — CÙNG hình-dạng với cửa cây và cửa quả, và nói ĐÚNG thứ có trên đĩa.\n\nBa câu trả lời, ba màn hình khác nhau ở phía app (đọc `available` + `meta.reason_code`):\n  • chưa từng dựng cho con này → `available: false`, `reason_code` = `chua_co_lan_dung_...`;\n  • đã dựng và **cổng chặn**   → `available: false`, `status: failed`, `reason_code` là mã\n    của cổng (`animal_recon3d.GATE_*`) kèm `meta.gate` là các số đo;\n  • có mô-hình                 → `available: true`, kèm `meta.trust` (bao nhiêu điểm nằm\n    trên mình con vật, cả cảnh bao nhiêu, có thưa quá không).\n\nVì sao khoản `trust` là BẮT BUỘC chứ không phải trang trí: `recon3d` chạy trên cảnh có con\nvật cử động KHÔNG báo lỗi — nó dựng ra cái CHUỒNG rồi khen \"đã đi gần trọn vòng\". Một cửa\ntrả đám mây điểm mà không nói nó đáng tin tới đâu chính là đường để lỗi đó lên thẳng màn\nhình người nuôi. Cổng chặn ở `animal_recon3d`, còn cửa này có nhiệm vụ KHÔNG che nó đi.\n\nCửa này chỉ ĐỌC. Chưa có route nào nạp ảnh để dựng cho con vật (đường `/animal/enroll`\nkhông giữ ảnh gốc), nên với đa số cá thể câu trả lời sẽ là câu thứ nhất — và đó là sự thật,\nkhông phải lỗi.\n\n404 đồng nhất cho 'không tồn tại' và 'không thuộc bạn', đúng luật mọi cửa GET của router này.","operationId":"animal_model3d_api_animal__animal_did__model3d_get","parameters":[{"name":"animal_did","in":"path","required":true,"schema":{"type":"string","title":"Animal Did"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/animal/rename":{"post":{"tags":["animal"],"summary":"Đổi tên cá thể","description":"Đổi tên một cá thể.\n\nVì sao cửa này phải có: kho đã có sẵn `rename_animal` từ lâu nhưng không route nào gọi tới,\nnên nút \"Đổi tên\" bên app chỉ ghi vào một biến trong màn hình. Nông dân đặt tên cả đàn, kéo\nlàm mới một cái là tên cũ trở lại, và không một dòng nào báo. Mất dữ liệu trong im lặng.\n\nTên rỗng sau khi vệ sinh → 200 kèm `ok:false` (không phải lỗi tầng vận chuyển), cùng khuôn\n`POST /api/rename` của cây.","operationId":"animal_rename_api_animal_rename_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_animal_rename_api_animal_rename_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/animal/fee/estimate":{"get":{"tags":["animal"],"summary":"Ước lượng phí định danh (công khai)","operationId":"fee_estimate_api_animal_fee_estimate_get","parameters":[{"name":"species","in":"query","required":false,"schema":{"type":"string","default":"cattle","title":"Species"}},{"name":"value_usd","in":"query","required":false,"schema":{"type":"number","default":0.0,"title":"Value Usd"}},{"name":"lifecycle_events","in":"query","required":false,"schema":{"type":"integer","default":7,"title":"Lifecycle Events"}},{"name":"anchor_tier","in":"query","required":false,"schema":{"type":"string","default":"batch_daily","title":"Anchor Tier"}},{"name":"demand_factor","in":"query","required":false,"schema":{"type":"number","default":1.0,"title":"Demand Factor"}},{"name":"lamp_usd","in":"query","required":false,"schema":{"type":"number","default":0.0,"title":"Lamp Usd"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/animal/trust/tiers":{"get":{"tags":["animal"],"summary":"Bảng độ tin cậy theo anchor tier (công khai)","operationId":"trust_tiers_api_animal_trust_tiers_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/animal/trust/verify-inclusion":{"post":{"tags":["animal"],"summary":"Verify record thuộc root on-chain (công khai)","description":"Verify 1 record thuộc Merkle root đã neo Cardano — bất kỳ ai cũng gọi được.\n\nBody: {record_cbor_hex | leaf_hex, proof_path:[{sibling,right}], root_hex, salt_hex?}.\nTrả {verified: bool}. Đây là tính chất 'trustless verify' — không cần tin LampNet.","operationId":"trust_verify_api_animal_trust_verify_inclusion_post","requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Payload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/animal/provenance/demo":{"get":{"tags":["animal"],"summary":"Chuỗi truy xuất mẫu (bò → gói thịt) cho consumer","description":"Sinh 1 chuỗi lineage mẫu + inclusion proof thật để minh hoạ luồng người tiêu dùng.","operationId":"provenance_demo_api_animal_provenance_demo_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/{entity_type}/{entity_id}/event":{"post":{"tags":["timeline"],"summary":"Ghi 1 sự-kiện vào timeline","description":"Ghi sự-kiện. owner TỪ auth (chống IDOR).\n\nCÓ resolver (production): chủ = resolver(entity). owner==chủ → public+approved; người NGOÀI\n→ private+pending. entity CHƯA-resolve-được-chủ (chưa enroll) → 403: KHÔNG cho tạo event\nđầu để chiếm-chủ (chốt lỗ owner-hijack). KHÔNG resolver (PoC): first-writer-binds.","operationId":"add_event_api__entity_type___entity_id__event_post","parameters":[{"name":"entity_type","in":"path","required":true,"schema":{"type":"string","title":"Entity Type"}},{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/{entity_type}/{entity_id}/timeline":{"get":{"tags":["timeline"],"summary":"Đọc timeline (rọc-phách)","description":"Khách thấy public+approved; chủ thấy hết. verify_chain báo toàn-vẹn chuỗi GỐC.\n\n`events` gồm chuỗi của CHÍNH entity + các lần chăm-sóc của phạm-vi bao (vườn), trộn theo\nthời-gian. Sự-kiện kế-thừa mang `inherited_from` và KHÔNG mang prev_hash/leaf_hash — nên\n`chain_ok` vẫn chỉ nói về chuỗi gốc của entity này, không bị pha loãng.","operationId":"get_timeline_api__entity_type___entity_id__timeline_get","parameters":[{"name":"entity_type","in":"path","required":true,"schema":{"type":"string","title":"Entity Type"}},{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/{entity_type}/{entity_id}/proof/{event_id}":{"get":{"tags":["timeline"],"summary":"Merkle proof 1 event thuộc chuỗi","description":"Audit path chứng-minh event thuộc Merkle root chuỗi HIỆN-TẠI (so root đã neo on-chain).\n\nRỌC-PHÁCH (chống rò): proof CHỈ cấp cho event mà viewer ĐƯỢC XEM (chủ, hoặc event\npublic+approved). Event private/pending của NGƯỜI KHÁC → 404 (CÙNG mã 'không thấy' cho\n'không tồn-tại' và 'riêng-tư' → không lộ tồn-tại event ẩn + không rò leaf_hash của nó).\n\nDùng chuỗi GỐC (mọi event) để dựng cây — root khớp cái đã/sẽ neo (proof path chỉ lộ hash\nanh-em, KHÔNG lộ nội-dung event khác — đúng RFC 6962). Nhưng `n`/`index` (tổng event GỒM\nevent ẩn + vị-trí trong đó) CHỈ trả cho CHỦ; khách KHÔNG nhận (rò số event riêng-tư).\n\nTrường 'anchor' (ADDITIVE, #77): nếu root HIỆN-TẠI đã được neo on-chain (tx đã lên chain,\ncallback đã ghi vết) → trả {tx_hash, explorer_url, status...}; chưa neo → None. Client so\nproof cục-bộ với tx on-chain để đóng vòng bằng-chứng bất-biến.","operationId":"get_proof_api__entity_type___entity_id__proof__event_id__get","parameters":[{"name":"entity_type","in":"path","required":true,"schema":{"type":"string","title":"Entity Type"}},{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}},{"name":"event_id","in":"path","required":true,"schema":{"type":"string","title":"Event Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/{entity_type}/{entity_id}/event/{event_id}/anchor":{"post":{"tags":["timeline"],"summary":"Neo-mốc root chuỗi lên Cardano (chủ)","description":"CHỈ chủ entity neo-mốc. Gộp toàn-bộ event tới hiện-tại thành 1 root → neo (nhãn 1455).\n\nevent_id ở path = mốc user bấm 'chốt' (ghi-vết event nào kích neo); ta vẫn neo TRỌN chuỗi\n(root đại-diện cả lịch-sử). anchorer đóng-gói mnemonic/network ở orchestrator + nhận\nentity_id/entity_type (#77) để callback ghi-vết neo về đúng thực-thể.","operationId":"anchor_api__entity_type___entity_id__event__event_id__anchor_post","parameters":[{"name":"entity_type","in":"path","required":true,"schema":{"type":"string","title":"Entity Type"}},{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}},{"name":"event_id","in":"path","required":true,"schema":{"type":"string","title":"Event Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/scan":{"post":{"tags":["scan"],"summary":"Quét tự-phát-hiện loài CÂY (1 nút) → scan_token + top_k","description":"Nhận ảnh → chạy detector loài cây → trả scan_token (giữ tạm) + species_guess + top_k.\n\nKHÔNG định-danh cá-thể ở đây (đó là /api/identify). need_confirm LUÔN True ở PoC vì\ndetector STUB không chắc — client hiển-thị top_k để người-dùng chọn rồi gọi /api/scan/confirm.","operationId":"scan_api_scan_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_scan_api_scan_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/scan/confirm":{"post":{"tags":["scan"],"summary":"Chốt loài người-dùng xác-nhận → hướng-dẫn route định-danh","description":"Chốt loài cho 1 scan_token → trả hướng-dẫn route sang định-danh cây (/api/identify + species).\n\nspecies PHẢI thuộc top_k của token đó (chống xác-nhận loài ngoài kết-quả quét). Token CONSUME\nsau confirm (1 lần dùng). Sai/hết-hạn/sai-chủ → 400 {ok:false,error}.","operationId":"scan_confirm_api_scan_confirm_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_scan_confirm_api_scan_confirm_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"summary":"Api Home","description":"Trang chủ CHO NGƯỜI VIẾT ỨNG DỤNG: base URL, cách lấy token, hai lối vào (công khai /\ncó tài khoản), khuôn lỗi, trần tải lên, đường tới /docs + /openapi.json.\n\nGốc miền là chỗ người lạ gõ vào đầu tiên khi họ có ý định TÍCH HỢP. Trước đây chỗ đó trả về\ngiao diện dùng thử tại vườn, nên ai muốn viết ứng dụng cũng phải được chỉ đường bằng miệng\nmới tìm ra /docs. Giao diện vườn KHÔNG mất — nó ở /TreeIdentity, đúng đường link cố định đã\nphát cho người thử nghiệm từ đầu.","operationId":"api_home__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/TreeIdentity/":{"get":{"summary":"Index","description":"Giao diện dùng thử tại vườn.\n\nBốn đường vào cùng một trang. `/TreeIdentity` là đường đã phát cho người thử nghiệm từ đầu và\nKHÔNG được bỏ — link đã nằm trong tin nhắn, trong ghi chú thực địa, có thể trong dấu trang của\nhọ. `/Identify` là tên đúng với thứ hệ này làm: nhận diện cá thể, không riêng cây. Cái tên cũ\ntự nó nói sai phạm vi sản phẩm với mọi người lạ đọc thanh địa chỉ, mà chi phí sửa chỉ là thêm\nmột dòng.\n\n`Cache-Control: no-cache` vì trang này đổi theo mỗi lần triển khai. Thiếu chỉ thị đó thì\n`FileResponse` chỉ gửi `Last-Modified`, và trình duyệt được phép TỰ suy hạn dùng ≈ 10% tuổi\ntài liệu (RFC 9111 §4.2.2) — một trang không đổi hai tháng thì được giữ nhiều ngày mà không\nhỏi lại máy chủ. Đo được 31/08: bản tiếng Anh đã lên máy thật mà người dùng cũ vẫn thấy bản\ntiếng Việt. `no-cache` không tắt bộ nhớ đệm, nó bắt hỏi lại — máy chủ trả 304 theo\n`Last-Modified` nên gần như không tốn gì. Cùng nếp với /capture, /q/{code}, /server/monitor.\n\nTrang này hôm nay chạm tới cây và quả của cây. Con vật đi qua API (`/api/animal/*`), chưa có\nmàn hình tại vườn.","operationId":"index_TreeIdentity__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/TreeIdentity":{"get":{"summary":"Index","description":"Giao diện dùng thử tại vườn.\n\nBốn đường vào cùng một trang. `/TreeIdentity` là đường đã phát cho người thử nghiệm từ đầu và\nKHÔNG được bỏ — link đã nằm trong tin nhắn, trong ghi chú thực địa, có thể trong dấu trang của\nhọ. `/Identify` là tên đúng với thứ hệ này làm: nhận diện cá thể, không riêng cây. Cái tên cũ\ntự nó nói sai phạm vi sản phẩm với mọi người lạ đọc thanh địa chỉ, mà chi phí sửa chỉ là thêm\nmột dòng.\n\n`Cache-Control: no-cache` vì trang này đổi theo mỗi lần triển khai. Thiếu chỉ thị đó thì\n`FileResponse` chỉ gửi `Last-Modified`, và trình duyệt được phép TỰ suy hạn dùng ≈ 10% tuổi\ntài liệu (RFC 9111 §4.2.2) — một trang không đổi hai tháng thì được giữ nhiều ngày mà không\nhỏi lại máy chủ. Đo được 31/08: bản tiếng Anh đã lên máy thật mà người dùng cũ vẫn thấy bản\ntiếng Việt. `no-cache` không tắt bộ nhớ đệm, nó bắt hỏi lại — máy chủ trả 304 theo\n`Last-Modified` nên gần như không tốn gì. Cùng nếp với /capture, /q/{code}, /server/monitor.\n\nTrang này hôm nay chạm tới cây và quả của cây. Con vật đi qua API (`/api/animal/*`), chưa có\nmàn hình tại vườn.","operationId":"index_TreeIdentity_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/identify":{"get":{"summary":"Index","description":"Giao diện dùng thử tại vườn.\n\nBốn đường vào cùng một trang. `/TreeIdentity` là đường đã phát cho người thử nghiệm từ đầu và\nKHÔNG được bỏ — link đã nằm trong tin nhắn, trong ghi chú thực địa, có thể trong dấu trang của\nhọ. `/Identify` là tên đúng với thứ hệ này làm: nhận diện cá thể, không riêng cây. Cái tên cũ\ntự nó nói sai phạm vi sản phẩm với mọi người lạ đọc thanh địa chỉ, mà chi phí sửa chỉ là thêm\nmột dòng.\n\n`Cache-Control: no-cache` vì trang này đổi theo mỗi lần triển khai. Thiếu chỉ thị đó thì\n`FileResponse` chỉ gửi `Last-Modified`, và trình duyệt được phép TỰ suy hạn dùng ≈ 10% tuổi\ntài liệu (RFC 9111 §4.2.2) — một trang không đổi hai tháng thì được giữ nhiều ngày mà không\nhỏi lại máy chủ. Đo được 31/08: bản tiếng Anh đã lên máy thật mà người dùng cũ vẫn thấy bản\ntiếng Việt. `no-cache` không tắt bộ nhớ đệm, nó bắt hỏi lại — máy chủ trả 304 theo\n`Last-Modified` nên gần như không tốn gì. Cùng nếp với /capture, /q/{code}, /server/monitor.\n\nTrang này hôm nay chạm tới cây và quả của cây. Con vật đi qua API (`/api/animal/*`), chưa có\nmàn hình tại vườn.","operationId":"index_identify_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/Identify":{"get":{"summary":"Index","description":"Giao diện dùng thử tại vườn.\n\nBốn đường vào cùng một trang. `/TreeIdentity` là đường đã phát cho người thử nghiệm từ đầu và\nKHÔNG được bỏ — link đã nằm trong tin nhắn, trong ghi chú thực địa, có thể trong dấu trang của\nhọ. `/Identify` là tên đúng với thứ hệ này làm: nhận diện cá thể, không riêng cây. Cái tên cũ\ntự nó nói sai phạm vi sản phẩm với mọi người lạ đọc thanh địa chỉ, mà chi phí sửa chỉ là thêm\nmột dòng.\n\n`Cache-Control: no-cache` vì trang này đổi theo mỗi lần triển khai. Thiếu chỉ thị đó thì\n`FileResponse` chỉ gửi `Last-Modified`, và trình duyệt được phép TỰ suy hạn dùng ≈ 10% tuổi\ntài liệu (RFC 9111 §4.2.2) — một trang không đổi hai tháng thì được giữ nhiều ngày mà không\nhỏi lại máy chủ. Đo được 31/08: bản tiếng Anh đã lên máy thật mà người dùng cũ vẫn thấy bản\ntiếng Việt. `no-cache` không tắt bộ nhớ đệm, nó bắt hỏi lại — máy chủ trả 304 theo\n`Last-Modified` nên gần như không tốn gì. Cùng nếp với /capture, /q/{code}, /server/monitor.\n\nTrang này hôm nay chạm tới cây và quả của cây. Con vật đi qua API (`/api/animal/*`), chưa có\nmàn hình tại vườn.","operationId":"index_Identify_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/health":{"get":{"summary":"Health","description":"CÔNG KHAI (KHÔNG auth) — app mobile kiểm KẾT NỐI + khám phá NĂNG LỰC + phiên bản\ncontract TRƯỚC khi đăng nhập. KHÔNG lộ dữ liệu user (chỉ trạng thái hệ). App so `version`\nđể cảnh báo nếu cũ; đọc `auth_open` để biết có cần đăng nhập; `features` để bật/tắt UI.","operationId":"health_api_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Health Api Health Get"}}}}}}},"/api":{"get":{"summary":"Api Index","description":"Liệt kê mọi endpoint /api (path + method + tóm tắt) — sinh động từ app.routes,\nkhông cần bảo trì khi thêm route. Kèm link /docs + /openapi.json.","operationId":"api_index_api_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Api Index Api Get"}}}}}}},"/api/trees":{"get":{"summary":"Trees","operationId":"trees_api_trees_get","parameters":[{"name":"farm_id","in":"query","required":false,"schema":{"type":"string","title":"Farm Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/species/catalog":{"get":{"summary":"Species Catalog","description":"Danh mục loài + hoạt động (catalog/*.json) cho web dựng bộ chọn loài/hoạt động.\n\nDữ liệu TĨNH công khai (không lộ vườn người dùng) → không cần auth. Cache module-level.\nFile thiếu → {\"species\":[], \"activities\":[], \"error\": \"...\"} (không sập).","operationId":"species_catalog_api_species_catalog_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/events":{"get":{"summary":"Events","description":"Tải toàn bộ nhật ký theo thời gian (cho phân tích thí nghiệm).\n\nCHỈ ADMIN: events.jsonl chứa nội-tạng (factors 4-yếu-tố, sim/margin,\nper-candidate _sim của MỌI chủ) — CONTRACT §6 cấm lộ ra client. Trước đây\nendpoint này KHÔNG auth → rò toàn bộ (audit 2026-06-25). Gắn require_admin\nnhư /api/admin/activity.","operationId":"events_api_events_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/feedback":{"post":{"summary":"Feedback","description":"Nông dân gửi góp ý. note bắt buộc (đã strip, chặn rỗng). Lưu 1 dòng vào feedback.jsonl.","operationId":"feedback_api_feedback_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_feedback_api_feedback_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/feedback":{"get":{"summary":"Admin Feedback","description":"ADMIN: danh sách góp ý nông dân (mới nhất trước). KHÔNG lộ owner thô/secret.","operationId":"admin_feedback_api_admin_feedback_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/activity":{"get":{"summary":"Admin Activity","description":"ADMIN: tổng hợp hoạt động TỪNG user.\n\nNguồn:\n  - accounts.json (qua ACC): danh sách user + created_at.\n  - GAL._recs: cây theo owner (số cây + tên cây) — LÀM ĐƯỢC NGAY (owner per-record).\n  - events.jsonl: identify/enroll + phân bố quyết định + hoạt động cuối, NHƯNG chỉ\n    quy được về user qua field `owner` (mới thêm ở VIỆC 4). Sự kiện CŨ chưa có owner\n    → KHÔNG bịa: bỏ qua khi tính theo-user, và gắn cờ `note` để THÀNH THẬT báo còn\n    sự kiện chưa gắn user.","operationId":"admin_activity_api_admin_activity_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/enroll":{"post":{"summary":"Enroll","operationId":"enroll_api_enroll_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_enroll_api_enroll_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/verify_add":{"post":{"summary":"Verify Add","description":"Người dùng XÁC NHẬN cây → chụp thêm góc → hệ XÁC THỰC rồi mới bổ sung vào ĐÚNG cây.","operationId":"verify_add_api_verify_add_post","parameters":[{"name":"matcher","in":"query","required":false,"schema":{"type":"string","title":"Matcher"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_verify_add_api_verify_add_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tree/{tree_id}/video":{"post":{"summary":"Tree Video","description":"GĐ1 — NẠP VIDEO bổ-sung góc cho cây đã có: clip → chắt-lọc khung-hình đại-diện\n(video_ingest) → mỗi khung jpeg đẩy qua ĐÚNG đường embed+verify_add ảnh CŨ (KHÔNG viết lại\nengine). ADDITIVE route mới — route ảnh cũ không đụng.\n\nVì sao bổ-sung-góc (verify_add) chứ không enroll-mới: video quay quanh 1 cây đã định-danh →\nnhiều khung CÙNG cây; verify_add gom đúng cây + lọc khung không-khớp (chống nhiễm gallery).\nTenancy: _guard_owner BẮT cây phải của chủ (chống IDOR). Chống OOM: extract_keyframes tự\nDOWNSCALE + chặn trần OLT_VID_MAX_SAMPLES; ta chỉ giữ list jpeg khung-đẹp (ít, đã nén).\n\nBYTE GỐC ĐẨY LampNet TRƯỚC khi chắt khung — cùng lý-do fruit_video: extract_keyframes hạ\n720px rồi XOÁ clip gốc (video_ingest.py `os.remove(tmp_path)`), không đẩy trước là mất\nfull-res VĨNH VIỄN. Trước đây route này KHÔNG đẩy nên video CÂY — đúng thứ nông dân quay —\nkhông hề có mặt trên LampNet, còn đường duy nhất tới LampNet là fruit_video (buộc phải khai\nsai loại video mới lưu được). Nay hai route cùng khuôn lưu-trữ, app không phải viết hai nhánh.\n\nĐính TIMELINE (append-only, đính-chính được) chứ KHÔNG gắn cố-định vào DID cây: media là\nbằng-chứng gắn TẠM, phải xét lại được. `engine_verified` = phán-quyết CỦA MÁY (verify_add có\nnhận khung không) — tách hẳn khỏi `link_status='unconfirmed'` là LỜI KHAI của người gửi. Hai\nthứ này khác nhau: máy nhận khung nghĩa là khung khớp cây, nhưng \"clip này quay cây X\" vẫn là\nđiều người gửi khai.","operationId":"tree_video_api_tree__tree_id__video_post","parameters":[{"name":"tree_id","in":"path","required":true,"schema":{"type":"string","title":"Tree Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_tree_video_api_tree__tree_id__video_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tree/{tree_id}/fruit_video":{"post":{"summary":"Tree Fruit Video","description":"VIDEO QUẢ gắn CÂY — nông dân quay chùm quả rồi tự gắn cho cây; hệ nhận-diện SAU.\n\nVì sao gắn CÂY chứ KHÔNG gắn vào một fruit_id (khác hẳn /api/fruit/add_view): clip lia qua\nchùm thì mỗi khung có thể là quả KHÁC nhau. add_view(fruit_id) giả-định mọi khung = CÙNG 1\nquả → nhồi góc quả A vào hồ-sơ quả B, làm bẩn gallery VĨNH VIỄN (không rút lại được vì\nembedding đã trộn). Gắn ở mức CÂY thì lời-khai của nông dân là \"chùm quả này thuộc cây này\"\n— đúng thứ họ THẬT SỰ biết — còn việc tách từng quả để engine làm sau, sai thì sửa được.\n\nQUAY MỘT QUẢ (`fruit_id`, TUỲ CHỌN — thêm 10/08/2026). Khi nông dân BIẾT mình đang quay quanh\nĐÚNG một quả thì chính họ khử được cái nhập-nhằng ở trên — đúng khuôn `tree_video` vốn đòi\n`tree_id`. Gửi `fruit_id` → mỗi khung được cắt theo bbox quả rồi đẩy qua CHÍNH cổng của\n/api/fruit/add_view (`_fruit_view_warn`: FRUIT_VIEW_MARGIN + FRUIT_VIEW_MIN), nên khung nào\ntrông giống quả khác hơn thì cổng đó loại — không cần cơ-chế mới, không nới lỏng gì.\nKHÔNG gửi `fruit_id` → hành-vi CŨ nguyên vẹn (chỉ đếm bbox, không đụng gallery quả).\nKhung có NHIỀU quả: lấy bbox có `area_frac` LỚN NHẤT (quả đang được quay là quả gần máy nhất)\nrồi vẫn để cổng phán — và ĐẾM số khung nhiều-quả vào nhật ký (`n_multi_fruit_frames`) để sau\nnày còn đo được cách chọn này sai bao nhiêu, thay vì bịa thêm một ngưỡng \"áp đảo\" chưa có số.\n\nVì sao KHÔNG gắn cố-định vào DID cây (anh Đức chốt 2026-07-17): media là bằng-chứng gắn TẠM,\nphải xét lại được. Nên đính qua TIMELINE (append-only) — gắn sai thì ghi event đính-chính,\nKHÔNG phải sửa danh-tính cây. DID cây giữ nguyên là danh-tính, không phình theo từng clip.\n\nVideo thô ĐẨY LampNet TRƯỚC khi chắt khung (issue #83): extract_keyframes hạ 720px rồi XOÁ\nclip gốc (video_ingest.py `os.remove(tmp_path)`) — không đẩy trước thì phần full-res mất\nVĨNH VIỄN, mà chính full-res mới là đòn-bẩy cho vỏ/gai (crop+resize làm bark chết trên durian).\nLAMPNET_ENABLED=false → put_bytes trả CID `local_<sha>` nên dev/offline KHÔNG vỡ.\n\nlink_status='unconfirmed' + link_source='farmer_manual': ĐÁNH DẤU đây là LỜI KHAI chưa xác\nnhận. Không có nhãn này thì cuối buổi thực-địa có N clip mà không tách được cái nào khai\nđúng/sai ⟹ VeData không có gì để chấm. CHO PHÉP khai sai là CỐ Ý — đó là dữ-liệu, không\nphải lỗi.","operationId":"tree_fruit_video_api_tree__tree_id__fruit_video_post","parameters":[{"name":"tree_id","in":"path","required":true,"schema":{"type":"string","title":"Tree Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_tree_fruit_video_api_tree__tree_id__fruit_video_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/delete":{"post":{"summary":"Delete","operationId":"delete_api_delete_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_delete_api_delete_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tree/set_species":{"post":{"summary":"Set Species","description":"Gán/đổi LOÀI cho cây đã có (để gate UI: loài không-quả ẩn nút quả). Cây cũ thiếu loài\n→ người dùng chọn loài 1 lần. Chỉ chủ vườn. species rỗng → về 'chưa biết'.","operationId":"set_species_api_tree_set_species_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_set_species_api_tree_set_species_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tree/set_farm":{"post":{"summary":"Set Farm","description":"[B2] Gán/đổi FARM cho cây ĐÃ đăng-ký — BACKFILL cây mồ-côi (tạo trước khi có vườn, hoặc\nfarm_id bị bỏ lúc enroll). Phòng-thủ lớp-2 field 07-11 'tạo vườn nhưng cây không vào vườn':\ncho phép sửa link SAU enroll thay vì phải xoá-tạo-lại. Chống IDOR ở CẢ cây LẪN farm (cây phải\nthuộc chủ → 403; farm phải là vườn CỦA CHÍNH CHỦ → 404 gộp, không lộ vườn người khác).\nfarm_id rỗng/None → mồ-côi (về null, cây vẫn truy được) — dùng để GỠ cây khỏi vườn.","operationId":"set_farm_api_tree_set_farm_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_set_farm_api_tree_set_farm_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tree/set_visibility":{"post":{"summary":"Set Visibility","description":"Đổi VISIBILITY cây đã có — KHÔNG cần nạp ảnh lại (chủ cafe bật công-khai cây đã đăng-ký).\nvisibility ∈ {private, public_readonly, public_contributable} — lạ → 400.\nexpose_location (tùy): {none, geohash_coarse, exact} — mức lộ GPS trang công-khai (§8/§12.2).\npublic_card (tùy): 1/0 — cây RIÊNG TƯ có cho người lạ cầm mã thấy THẺ TỐI THIỂU (loài, vùng\nthô ~39 km, đã neo chuỗi chưa) không. Mặc-định (không gửi, cây cũ) = CÓ, vì QR đã dán ngoài\nvườn mà 404 thì mặt tiền của hệ nói dối người mua (#195). Đặt 0 = kín hẳn, phản hồi không\nphân-biệt được với mã không tồn-tại.\n\nĐÂY LÀ CỬA VÀO PHẠM VI TÌM KIẾM CÔNG KHAI (từ 08/2026). Trước kia bật công-khai chỉ mở\nTRANG /t/{code} — ai không có mã thì không thấy. Từ khi có /api/fruit/lookup, cây công-khai\ncòn là thứ mà mọi NGƯỜI LẠ đem ảnh tới so. Đường lạm dụng rõ ràng: tạo tài-khoản (miễn phí)\n→ tải ảnh vườn người khác → enroll → bật công-khai ⟹ bơm cây giả vào tầm khớp của mọi người.\n\nCHẶN ĐÃ CHỌN: hạn mức theo CỬA SỔ TRƯỢT trên hành-động CHUYỂN SANG công-khai (PUBLISH_RL,\nmặc-định 50 cây / 24 giờ / tài-khoản). Vì sao chọn nó, và vì sao KHÔNG chọn ba cách kia:\n  • Cờ CHỜ DUYỆT: cần người trực hàng ngày. Không ai được phân việc đó ⟹ trên thực-tế nó\n    thành \"không cây nào công-khai được\", tức khoá chết tính năng bằng một hàng đợi rỗng.\n  • ĐỘ TRỄ (công-khai sau N giờ): phá đúng vòng làm việc của nông dân thật (bật xong quét\n    thử ngay để tin là chạy), mà KHÔNG chặn kẻ lạm dụng — họ chỉ cần chờ.\n  • Hạn mức: nông dân thật bật vài cây mỗi lần, có bật cả vườn thì hôm sau bật tiếp và được\n    báo rõ còn bao lâu; kẻ bơm hàng loạt bị chặn ở TỐC ĐỘ. Rẻ nhất trong ba cách và là cách\n    duy nhất không cần thêm người, thêm bảng, thêm cột dữ-liệu.\nChỉ tính lượt ĐỔI TỪ riêng-tư SANG công-khai: bấm lại nút cũ, đổi expose_location, hay hạ về\nriêng-tư đều KHÔNG tiêu hạn mức — nếu không thì nông dân thật là người bị chặn đầu tiên.\n\nGIỚI HẠN ĐÃ BIẾT, ghi ra để không ai tưởng đây là cổng kín: bộ đếm nằm trong BỘ NHỚ tiến\ntrình (như mọi hạn mức khác của repo — không có Redis), nên khởi-động lại máy chủ là về 0.\nNó chặn TỐC ĐỘ bơm, không chặn TỔNG. Muốn chặn tổng thì phải có mốc thời-gian công-khai lưu\ntrên đĩa + duyệt — việc đó cần Đức chốt, không tự làm ở đây.","operationId":"set_visibility_api_tree_set_visibility_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_set_visibility_api_tree_set_visibility_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tree/marker":{"post":{"summary":"Tree Marker","description":"Thêm 1 mốc ngữ-cảnh quanh cây (bồn xanh • trái...) cho sơ-đồ KHÔNG-GIAN. Chỉ chủ vườn.\n\nlabel: vệ-sinh _vname (bắt buộc). side ∈ {left,right,front,back} → ngoài whitelist = 400.","operationId":"tree_marker_api_tree_marker_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_tree_marker_api_tree_marker_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tree/{tree_id}/layout":{"get":{"summary":"Tree Layout","description":"Sơ-đồ KHÔNG-GIAN quả trên 1 cây: thống-kê + vị-trí từng quả + mốc ngữ-cảnh.\n\nChỉ chủ vườn (chống IDOR). Quả lấy theo owner; species lấy từ TreeRecord.","operationId":"tree_layout_api_tree__tree_id__layout_get","parameters":[{"name":"tree_id","in":"path","required":true,"schema":{"type":"string","title":"Tree Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tree/{tree_id}/profile":{"post":{"summary":"Tree Profile Update","description":"Ghi HỒ-SƠ SINH-TRƯỞNG của cây — CẢ KHỐI một lượt (giống/tuổi/sức-khỏe/ngày-hái/ghi-chú).\n\nVì sao CẢ KHỐI một cửa, không tách năm cửa lẻ: người dùng sửa MỘT LẦN thì phải là MỘT lượt\nghi — năm lượt riêng trượt lẻ-tẻ (mất mạng giữa chừng thì 2/5 trường vào, 3/5 mất) và giao\ndiện không nói được đúng cái nào đã lưu.\n\nĐây là cửa JSON (`Body(...)`), KHÔNG phải `multipart/form-data` như gần hết cửa khác trong\nkho — có chủ ý, xem đầu `tree_profile.py`: ba trạng-thái \"vắng/null/giá-trị\" của MỖI trường\nchỉ phân-biệt được trong JSON, form không có khái-niệm null.\n\nMỌI trường ở đây là LỜI NGƯỜI KHAI (`source=\"declared\"`), KHÔNG phải máy đo — máy sau này\nsuy được tuổi cây từ ảnh thì đó là nguồn KHÁC, không viết đè lên đây (xem `tree_profile.py`).\n\nCửa này KHÔNG nhận tệp ghi-âm (`voice_memo_*` — cần đường tải-lên riêng, chưa dựng): thân\nJSON có nhắc tới ghi-âm thì response mang `voice_memo:{accepted:false,...}` nêu đích-danh\nkhoá đã thấy, để app nói thật với người dùng thay vì im-lặng báo \"đã lưu\".","operationId":"tree_profile_update_api_tree__tree_id__profile_post","parameters":[{"name":"tree_id","in":"path","required":true,"schema":{"type":"string","title":"Tree Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"title":"Body"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Tree Profile Get","description":"Đọc lại HỒ-SƠ SINH-TRƯỞNG đã lưu — để app đối-chiếu với `AsyncStorage` trên máy, thay vì\ntin bộ nhớ máy là nguồn sự-thật (đúng lượt \"báo xong\" mà `POST .../profile` sinh ra để vá).\n\nChỉ chủ vườn (chống IDOR, cùng khuôn `tree_layout`). Cây chưa khai gì → `profile` đủ sáu\nkhoá, mỗi khoá `null` (không phải khoá vắng) — xem `tree_profile.to_public`.","operationId":"tree_profile_get_api_tree__tree_id__profile_get","parameters":[{"name":"tree_id","in":"path","required":true,"schema":{"type":"string","title":"Tree Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/farm/layout":{"get":{"summary":"Farm Layout","description":"Sơ-đồ KHÔNG-GIAN cây trong vườn: chiếu GPS ra mét tương-đối quanh TÂM (centroid).\n\nequirectangular quanh tâm = centroid các cây CÓ GPS:\n  x_m=(lon-lon0)*cos(lat0)*111320 ; y_m=(lat-lat0)*110540.\nCây thiếu GPS → x_m/y_m=null (web hiện 'chưa định vị'). Không cây nào có GPS → center=null.\nChỉ vườn của người đăng nhập (riêng tư + chống IDOR).","operationId":"farm_layout_api_farm_layout_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/farm/{farm_id}/map":{"get":{"summary":"Farm Map","description":"Sơ đồ vườn cho app: MÉT tương đối so với gốc vườn, kèm gốc, kèm nguồn của từng chấm.\n\nĐỌC-ONLY, chỉ thêm mới. Không cửa nào đang chạy đổi hành vi.\n\nVì sao trả mét chứ không trả toạ độ địa lý: trả toạ độ địa lý thì phép chiếu vẫn sống ở phía\nứng dụng, và ứng dụng thứ hai lại dựng lại từ đầu. Mét là đơn vị màn hình dùng thẳng.\n\nVì sao GỐC nằm trong thân trả về: máy chủ trả mét thì máy chủ phải chốt gốc. Hai bên tự tính\ngốc độc lập rồi mong trùng nhau là bẫy lệch vài mét, và cái lệch ấy không có gì báo.\n\nMỗi cây mang `{tree_id, name, species, x, z, posSource, pos_text_vi, offset_m}`:\n  • `x` mét về phía ĐÔNG, `z` mét về phía NAM (xem `geo.local_xy_m`);\n  • `posSource` ∈ `gps` · `manual` · `placeholder` — CHUỖI CÓ TÊN, không phải cờ đúng-sai;\n  • `offset_m` chỉ có ở mức `manual` và chỉ khi cây có CẢ số máy đo lẫn vị trí đặt tay. Cây\n    chưa từng có số máy thì trường này là `null` — in \"lệch 0 m\" ở đó là khẳng định một phép\n    so chưa hề xảy ra.\n\nLuật ghép hai nguồn: số máy đo là MỐC, thao tác tay là HIỆU CHỈNH CÓ SỔ. Nên vị trí đặt tay\nTHẮNG số máy khi cả hai cùng có — nhưng thắng kèm sổ (`offset_m`, `edited_by`, `edited_at`),\nkhông phải thắng lặng lẽ. Đảo thành \"máy đo luôn thắng\" thì cây có toạ độ máy sai sẽ nhảy về\nchỗ sai mỗi lần tải lại và người trong vườn mất hẳn cách sửa.","operationId":"farm_map_api_farm__farm_id__map_get","parameters":[{"name":"farm_id","in":"path","required":true,"schema":{"type":"string","title":"Farm Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tree/set_position":{"post":{"summary":"Set Tree Position","description":"GHI vị trí người trong vườn đặt tay cho một cây. Cửa THÊM MỚI (#522).\n\nVì sao cửa này phải đi cùng đợt sơ đồ vườn: vị trí đặt tay hôm nay chỉ nằm trong bộ nhớ của\ntừng máy, không có bản trên máy chủ. Đổi điện thoại, gỡ app, xoá dữ liệu app là mất sạch,\nkhông có gì báo trước. Người bỏ công đi khắp vườn kéo từng chấm cho đúng mất hết công trong\nmột thao tác họ không biết là nguy hiểm.\n\nNhận `x`/`z` là MÉT so với gốc vườn — đúng đơn vị mà `/api/farm/{id}/map` vừa trả và đúng thứ\napp có trong tay sau khi người dùng kéo chấm. Máy chủ đổi ngược về toạ độ địa lý rồi mới lưu:\nmét chỉ có nghĩa kèm một cái gốc, mà gốc đổi được.\n\nGỠ vị trí đặt tay (cây quay về số máy đo) phải khai RÕ bằng `clear=1`, KHÔNG phải bằng cách\nbỏ trống `x`/`z`. Lý do đo được: tầng truyền tải bỏ hẳn ô nhập rỗng khỏi gói gửi đi, nên\n\"gửi chuỗi rỗng\" và \"quên gửi\" tới máy chủ giống hệt nhau. Nhận cái đó làm lệnh xoá thì một\ngói dựng sai sẽ âm thầm xoá công người ta đi khắp vườn kéo từng chấm. Thiếu cả ba → 422 kèm\ncâu chỉ rõ phải gửi gì.\n\nVườn chưa dựng được gốc → **409**, KHÔNG phải 422: yêu cầu không sai, chỉ là chưa đặt được\nvào đâu. Vẽ ranh vườn hoặc đăng ký một cây có toạ độ rồi gửi lại thì cùng gói ấy chạy được.","operationId":"set_tree_position_api_tree_set_position_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_set_tree_position_api_tree_set_position_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/farm":{"post":{"summary":"Farm Create","description":"Tạo FARM mới. owner_did LẤY TỪ AUTH (không nhận từ client). name vệ-sinh ở biên (rỗng→400).\nboundary_json/center_json = chuỗi JSON '[[lat,lon],...]'.\nboundary_method ∈ {gps_walk, map_draw, mixed} (lạ→unknown), boundary_acc_m = độ chính-xác GPS\ntrung-vị (m) — nguồn-gốc ranh cho VeData/truy-xuất (ranh ĐO tại chỗ ≠ ranh VẼ từ xa).\nboundary_acc_m nhận dạng chuỗi (str) rồi để store._clean_acc tự vệ-sinh: rỗng/không-số → None\n(KHÔNG 422 — đúng triết-lý 'giá trị lạ → không từ chối' của cả PR).\nTrả {ok, farm} — farm kèm area_sqm/perimeter_m/boundary_warnings server tính.\n[#107] method_verified (máy chấm lời khai boundary_method) KHÔNG có trong chữ ký hàm này:\nnó là field DẪN-XUẤT, server tự tính lúc trả về. Client gửi kèm cũng vô-hiệu — FastAPI bỏ\nfield form không khai, nên không có đường nào để lời khai tự chấm điểm cho chính nó.","operationId":"farm_create_api_farm_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_farm_create_api_farm_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/farms":{"get":{"summary":"Farms List","description":"Liệt-kê farm CỦA owner đăng-nhập, mỗi farm kèm counts {n_trees, n_animals}.\nVườn người khác KHÔNG lộ (tenancy theo owner).","operationId":"farms_list_api_farms_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/farm/{farm_id}":{"get":{"summary":"Farm Detail","description":"Chi-tiết 1 farm + cây + con thuộc farm. Owner-guard: farm không thuộc bạn → 403\n(cùng mã 'không có'/'không thuộc' — không lộ tồn-tại farm người khác).","operationId":"farm_detail_api_farm__farm_id__get","parameters":[{"name":"farm_id","in":"path","required":true,"schema":{"type":"string","title":"Farm Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Farm Delete","description":"Xoá CHỈ farm (KHÔNG xoá cây). Cây trong farm thành farm_id=null (mồ-côi, vẫn truy được).\nOwner-guard. Con (animal) giữ farm_id cũ (animal store độc-lập — chỉ dọn cây ở đây).\n\nMỐC ĐÓNG CHUỖI. Tạo vườn ghi `enroll`, sửa vườn ghi `note`, còn xoá vườn trước bản này KHÔNG\nghi gì — nên chuỗi sự-kiện của vườn dừng đột ngột. Một chuỗi dừng đột ngột và một chuỗi ĐỌC\nHỤT (kho hỏng, chặn quyền, mã gõ sai) có CÙNG hình dạng với bên đọc: cả hai đều là một danh\nsách hết ở giữa chừng, không có gì nói tiếp. Hai thứ đó phân biệt được thì phải phân biệt, và\nchỗ duy nhất phân biệt được chúng là một mốc nói \"đến đây là hết, vì vườn đã bị xoá\".\n\nTHỨ TỰ: ghi mốc TRƯỚC, xoá SAU. Đảo lại thì đúng cái lỗ đang vá còn nguyên cho ca máy chủ\nchết giữa hai câu lệnh — vườn đi rồi mà sổ vẫn im.\n\nBa thứ chống lại rủi ro ngược lại (ghi một mốc cho lượt xoá KHÔNG xảy ra — một dòng khai sai\nvĩnh-viễn trong sổ append-only đi lên chuỗi công-khai):\n  1. `FARMS.get(owner_did=owner)` ngay dưới đây chốt quyền TRƯỚC khi ghi. Vườn của người\n     khác/không có ⟹ 403, chưa mốc nào được ghi.\n  2. `dedup_key` theo chính `farm_id` — một vườn ĐÚNG MỘT mốc xoá, hai lượt gọi song song\n     không sinh hai dòng (cùng khuôn `farm_enroll:{farm_id}` ở cửa tạo).\n  3. `FARMS.delete` chỉ trả False khi vườn không còn hoặc không thuộc chủ. Quyền không đổi\n     giữa hai câu lệnh, nên nhánh False sau khi (1) đã qua chỉ còn nghĩa \"một lượt xoá song\n     song đã làm xong\" — lúc đó vườn ĐÃ đi thật và mốc vẫn đúng.\nCòn hở, nói thẳng: `FarmStore.delete` xoá khỏi bộ nhớ rồi mới ghi đĩa, nên đĩa hỏng đúng lúc\nghi để lại một mốc xoá cho một vườn sẽ sống lại sau lần khởi-động sau.\n\nXOÁ CỨNG, và mốc TRỎ VÀO thực-thể vừa biến mất — cố ý, vì hai kho tách nhau: mốc nằm trong\n`timeline.json`, `FarmStore` chỉ giữ vườn đang sống. Cửa đọc vẫn trả mốc này cho đúng người\nđã xoá: `_timeline_owner_resolver` trả None (vườn không còn) nên `is_owner` là False, nhưng\n`timeline_store._is_owner` còn vế thứ hai là `viewer_did == author_did`, mà `author_did` của\nmốc chính là `owner` đang đọc (`_farmer_did_from_owner(o) or o` trả về đúng `o` ở cả hai loại\ntài-khoản). Chủ cũ đọc được; người khác không.","operationId":"farm_delete_api_farm__farm_id__delete","parameters":[{"name":"farm_id","in":"path","required":true,"schema":{"type":"string","title":"Farm Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/farm/{farm_id}/update":{"post":{"summary":"Farm Update","description":"Cập-nhật farm (chỉ field GỬI mới đổi). Owner-guard. name nếu gửi thì vệ-sinh (rỗng→400).\nĐổi ranh (boundary_json) NÊN gửi kèm boundary_method mới — chỉnh tay ranh GPS cũ = 'mixed'.\nNếu đổi ranh mà KHÔNG gửi method: store reset provenance về 'unknown' + acc None (ranh mới\nkhông kế-thừa nguồn-gốc ranh cũ) để VeData không chấm nhầm độ-tin.\n[#107] method_verified tính lại theo ranh MỚI ở mỗi lần trả về — không nhận từ client.","operationId":"farm_update_api_farm__farm_id__update_post","parameters":[{"name":"farm_id","in":"path","required":true,"schema":{"type":"string","title":"Farm Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_farm_update_api_farm__farm_id__update_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/farm/{farm_id}/video":{"post":{"summary":"Farm Video","description":"Gắn một CLIP vào vườn. Cặp song sinh của `/api/tree/{id}/video`, trừ phần engine.\n\nKHÔNG chắt khung, KHÔNG `verify_add`: vườn không có kho so-khớp thị-giác, nên clip vườn là\nbằng-chứng thô gắn vào dòng thời-gian chứ không phải nguồn góc chụp. Đắp thêm một bước nhận\ndạng ở đây chỉ để \"cho giống cây\" là dựng một phán-quyết máy không có cơ sở nào để đưa ra.","operationId":"farm_video_api_farm__farm_id__video_post","parameters":[{"name":"farm_id","in":"path","required":true,"schema":{"type":"string","title":"Farm Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_farm_video_api_farm__farm_id__video_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/farm/{farm_id}/photos":{"post":{"summary":"Farm Photos","description":"Gắn ẢNH vào vườn (toàn cảnh, bờ ranh, biển vườn, thiệt hại sau bão…).\n\nTrần: mỗi ảnh 20 MB và tối đa 10 ảnh một lượt; cả lô vẫn nằm dưới trần một-tệp của\nmiddleware (cửa này KHÔNG khai vào `_MULTI_IMAGE_PATHS` — hẹp hơn, có chủ ý, xem\n`test_upload_cap_identify_auto._SINGLE_CAP_ON_PURPOSE`).","operationId":"farm_photos_api_farm__farm_id__photos_post","parameters":[{"name":"farm_id","in":"path","required":true,"schema":{"type":"string","title":"Farm Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_farm_photos_api_farm__farm_id__photos_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/farm/{farm_id}/media":{"get":{"summary":"Farm Media List","description":"Media đã gắn cho vườn — CHỈ CHỦ. Mỗi mục `{cid, sha256, kind, size, at}`.\n\n`cid` ra ở ĐÂY chứ không ra ở bản-ghi neo, và đó là cả thiết-kế: cửa này kiểm chủ trước khi\ntrả, bản-ghi trên kho thì ai cũng đọc được. Đổi byte ra từ `cid` bằng `{lampnet_view}/{cid}`\n— cùng tiền-tố mà `/api/health` và `/api/{type}/{id}/timeline` đang phát.","operationId":"farm_media_list_api_farm__farm_id__media_get","parameters":[{"name":"farm_id","in":"path","required":true,"schema":{"type":"string","title":"Farm Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/account/data":{"get":{"summary":"Account Data","description":"Bạn đang giữ những gì của tôi — và cái gì sẽ mất nếu tôi xoá. CHỈ ĐỌC.\n\nTrả cả `not_erasable` để app nói đúng ranh-giới ngay ở màn mô tả, thay vì hứa sạch rồi để lại\ndấu neo trên chuỗi. Câu chữ lấy NGUYÊN VĂN từ `account_erase.NOT_ERASABLE`, đừng viết lại ở\nđây: nhãn neo 1454 mang cả mã cây (có ô lưới ~150m) lẫn địa chỉ bản khai gốc đọc công-khai,\nkhông phải \"một dãy số không đọc ngược ra được gì\" như bản trước của dòng này khai.","operationId":"account_data_api_account_data_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/account/delete":{"post":{"summary":"Account Delete","description":"Xoá TOÀN BỘ dữ-liệu của người đang đăng-nhập. BẤT-KHẢ-HỒI.\n\n`confirm` phải là CHÍNH mã chủ sở-hữu, không phải một chữ cố-định: chuỗi cố-định thì một màn\nhình gọi nhầm cửa vẫn qua được, còn bắt gõ lại mã của mình thì lời xác-nhận mang thông-tin về\nPHẠM-VI chứ không chỉ về ý-định.\n\nPhạm-vi lấy TỪ phiên đăng-nhập, KHÔNG nhận owner theo tham-số — một cửa xoá nhận id của người\nkhác là một cửa xoá dữ-liệu người khác.","operationId":"account_delete_api_account_delete_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_account_delete_api_account_delete_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/{entity_type}/{entity_id}/did":{"get":{"summary":"Entity Did Get","description":"Đối tượng này có did:phoenix chưa. Chưa có KHÔNG phải lỗi — xem `public_view`.","operationId":"entity_did_get_api__entity_type___entity_id__did_get","parameters":[{"name":"entity_type","in":"path","required":true,"schema":{"type":"string","title":"Entity Type"}},{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/{entity_type}/{entity_id}/did/request":{"post":{"summary":"Entity Did Request","description":"BƯỚC 1 — trả THÁCH-ĐỐ cho app ký bằng khoá sinh trắc của nông dân.\n\nMáy chủ dựng payload rồi giữ nguyên nó lại: bước 2 phải dùng ĐÚNG payload này, vì dựng lại thì\n`nonce` khác mà nông dân đã ký nonce cũ — chữ ký thành vô nghĩa và lỗi hiện ra dưới dạng\n\"chữ ký sai\", rất khó truy.","operationId":"entity_did_request_api__entity_type___entity_id__did_request_post","parameters":[{"name":"entity_type","in":"path","required":true,"schema":{"type":"string","title":"Entity Type"}},{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/{entity_type}/{entity_id}/did/submit":{"post":{"summary":"Entity Did Submit","description":"BƯỚC 2 — nhận chữ ký của nông dân, điền vào payload đã giữ, POST lên PhoenixKey, lưu DID.","operationId":"entity_did_submit_api__entity_type___entity_id__did_submit_post","parameters":[{"name":"entity_type","in":"path","required":true,"schema":{"type":"string","title":"Entity Type"}},{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_entity_did_submit_api__entity_type___entity_id__did_submit_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/grant":{"post":{"summary":"Grant Create","description":"Chủ CẤP quyền cho owner KHÁC trên VƯỜN (farm) hoặc CÂY (tree) của mình. grantor LẤY TỪ\nAUTH. BẮT BUỘC verify grantor SỞ-HỮU scope (chống leo-thang).\n\nperms = chuỗi phân-cách phẩy. Hiện kho CHỈ nhận `read_private` (grant_store.VALID_PERMS);\n`moderate`/`contribute` thuộc lớp đóng-góp §17 CHƯA build nên KHÔNG cấp được — gửi lên sẽ bị\ntừ-chối 400 kèm ĐÚNG TÊN quyền sai, thay vì lọc im rồi báo lỗi chung chung.\nttl_days rỗng = không hết-hạn.","operationId":"grant_create_api_grant_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_grant_create_api_grant_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/grant/{grant_id}":{"delete":{"summary":"Grant Revoke","description":"Thu-hồi grant. CHỈ người CẤP (grantor) mới thu-hồi được — người khác → 404 (không lộ\ntồn-tại). Hiệu-lực NGAY request kế.","operationId":"grant_revoke_api_grant__grant_id__delete","parameters":[{"name":"grant_id","in":"path","required":true,"schema":{"type":"string","title":"Grant Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/account/resolve":{"get":{"summary":"Account Resolve","description":"Tra owner-ref của 1 tài-khoản theo USERNAME chính-xác → để chủ MỜI công-nhân (nhập tên thay\nvì gõ tay UUID/DID dễ sai). CHỈ khớp tuyệt-đối (không liệt-kê/mờ), cần đăng-nhập, trả gọn\n{owner, username} (không PII). Không thấy → 404. (Tài-khoản PhoenixKey-DID: dùng thẳng DID làm\ngrantee, owner == DID.)","operationId":"account_resolve_api_account_resolve_get","parameters":[{"name":"username","in":"query","required":true,"schema":{"type":"string","title":"Username"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/grants":{"get":{"summary":"Grants List","description":"Grant mà người đăng-nhập là grantor HOẶC grantee (mới nhất trước). Làm giàu nhãn người +\ncây/vườn + cờ `live` (tính sẵn hết-hạn) → app dựng màn 'người được chia-sẻ' không cần join.","operationId":"grants_list_api_grants_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/rename":{"post":{"summary":"Rename","description":"Sửa tên cây (user đặt nhầm / trùng tên). Chỉ đổi tên, giữ nguyên ảnh + chữ ký.","operationId":"rename_api_rename_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_rename_api_rename_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/identify":{"post":{"summary":"Identify","operationId":"identify_api_identify_post","parameters":[{"name":"matcher","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Matcher"}},{"name":"measure","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Measure"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_identify_api_identify_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/identify/video":{"post":{"summary":"Identify Video","description":"ĐỊNH-DANH bằng VIDEO (MVP) — nông dân quay 1 vòng quanh cây → chắt vài khung đại-diện → ĐÚNG\nthân /api/identify (_run_identify: embed + GAL.identify + rọc-phách). ADDITIVE: route ẢNH KHÔNG\nđụng; KHÔNG engine/daemon mới (tái-dùng video_ingest + _run_identify sẵn có).\n\nKHÁC /api/tree/{id}/video (verify_add cây ĐÃ CÓ, cần tree_id): route này ĐỊNH-DANH MỞ (chưa biết\ncây nào) → trả ĐÚNG contract identify {decision,candidates,confidence,allow_enroll_new,query_id,\nwarnings,suggest,moved_distance_m}. Trần khung THẤP (IDENTIFY_VIDEO_MAX_FRAMES) cho độ-trễ (khác\nenroll cần phủ góc rộng). query_raws=khung → bark T2 tự kích khi DINOv2 lưỡng-lự (same-species).\nĐọc STREAMING (chống RAM-DoS chunked). auth account như /api/identify.","operationId":"identify_video_api_identify_video_post","parameters":[{"name":"matcher","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Matcher"}},{"name":"measure","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Measure"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_identify_video_api_identify_video_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/identify_verdict":{"post":{"summary":"Identify Verdict","description":"PoC-Tree §3 L2 — GHI GROUND-TRUTH cho 1 lượt /api/identify (join bằng query_id).\n\nKHÁC HẲN /api/feedback (note tự-do): đây là verdict ĐÚNG/SAI để TÍNH ACCURACY, KHÔNG đụng\nfeedback cũ. verdict ∈ {correct, wrong, other}. correct_tid (tùy) = cây ĐÚNG (client lấy từ\n/api/trees), CHỈ tin khi là cây của CHÍNH CHỦ (chống ghi/dò cây người khác — như last_tree\nở /api/identify; sai-chủ → bỏ qua, KHÔNG 403 vì đây là nhãn-đo, không phải truy-cập tài-nguyên).\nGhi 1 dòng ground_truth.jsonl. KHÔNG lộ nội-tạng so-khớp ra client.","operationId":"identify_verdict_api_identify_verdict_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_identify_verdict_api_identify_verdict_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/update_location":{"post":{"summary":"Update Location","description":"Cây ĐÃ DỜI → cập nhật vị trí mới (giữ chữ ký, ghi lịch sử di chuyển).","operationId":"update_location_api_update_location_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_update_location_api_update_location_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tree_drift/{tree_id}":{"get":{"summary":"Tree Drift","description":"ĐO BIẾN THIÊN chữ-ký cây theo từng kênh + dự đoán vùng dễ-đổi / vùng-bền.\n\nTrả {ok, tree_id, days_since_update, n_views, drift_per_channel, predict}. Cây chỉ 1\ngóc (chưa đủ cặp để đo) → ok=False. Thuần đọc, KHÔNG đụng định-danh.","operationId":"tree_drift_api_tree_drift__tree_id__get","parameters":[{"name":"tree_id","in":"path","required":true,"schema":{"type":"string","title":"Tree Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tree_views":{"get":{"summary":"Tree Views","description":"Danh sách ảnh của 1 cây + ĐIỂM mỗi ảnh (khớp với phần còn lại) — để user xem/xoá.\n\n?describe=1 (LAZY, mặc-định TẮT): thêm `features_vi` — câu tiếng Việt mô-tả đặc-điểm\nnhận-dạng hình-học mỗi ảnh (góc cành, đếm/vị-trí quả). Mặc-định KHÔNG tính để giữ\nroute nhanh + response CŨ y-nguyên (byte-identical). Chỉ trả chuỗi + số-đo (GIẤU\nnội-tạng: không lộ embedding/score).","operationId":"tree_views_api_tree_views_get","parameters":[{"name":"tree_id","in":"query","required":true,"schema":{"type":"string","title":"Tree Id"}},{"name":"describe","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Describe"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/remove_views":{"post":{"summary":"Remove Views","description":"Xoá các ảnh user chọn (điểm thấp / bấm nhầm) → cây tự cải thiện.","operationId":"remove_views_api_remove_views_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_remove_views_api_remove_views_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/provenance/{tree_id}":{"get":{"summary":"Provenance Api","description":"Xuất xứ 1 cây theo tree_id (mã, CID ảnh, CID bản ghi, hash, trạng thái neo).","operationId":"provenance_api_api_provenance__tree_id__get","parameters":[{"name":"tree_id","in":"path","required":true,"schema":{"type":"string","title":"Tree Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tree_by_code/{code}":{"get":{"summary":"Tree By Code","operationId":"tree_by_code_api_tree_by_code__code__get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tree_by_code/{code}/model3d":{"get":{"summary":"Tree Model3D By Code","description":"Dữ-liệu 3D của cây theo MÃ — cho app dựng cảnh native ở lane KHÁCH VÃNG-LAI. CHỈ ĐỌC.\n\nĐây là bản dữ-liệu của đúng thứ `/view/{code}` đã cho khách xem từ lâu: cùng một tệp\n`model.ply`, cùng một luật vào cửa (cây công-khai, HOẶC người đang đăng nhập đọc được cây\nriêng-tư đó). Cửa này KHÔNG phơi thêm gì so với trang HTML kia — nó chỉ thay chỗ nhúng một\ntrang web bằng một gói JSON app tự vẽ.\n\nMỘT hình-dạng đầu ra duy nhất, kể cả khi người gọi là chủ: chủ đã có cửa riêng\n`/api/tree/{tree_id}/model3d` với đầy đủ số đo lượt chụp. Một cửa một luật thì không có nhánh\nnào để cài sai; cửa này luôn trả bản đã lọc (`model3d.public_view`), luôn dùng GPS làm-thô\ntheo `_public_gps_view`.\n\nBốn thứ bản khách KHÔNG có, và vì sao (chi-tiết ở `model3d.PUBLIC_META_KEYS`): `tree_id`\n(khoá nội-bộ mở nhiều cửa khác — khách vào bằng mã), lý-do dựng hỏng kèm câu chỉ việc (người\nlạ không bấm dựng lại được), độ phủ với số ảnh (nhật ký lao động của chủ), và trần hạ mẫu.\n\nKhông tra được mã, cây riêng-tư mà người gọi không đọc được, cây chưa có xuất-xứ → CÙNG một\n404, đúng luật `/t/{code}` và `/view/{code}`: nới cho chủ không được kéo theo việc lộ ra rằng\nmột mã có thật.","operationId":"tree_model3d_by_code_api_tree_by_code__code__model3d_get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"max_points","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Max Points"}},{"name":"max_fruits","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Max Fruits"}},{"name":"max_segments","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Max Segments"}},{"name":"colors","in":"query","required":false,"schema":{"type":"string","default":"1","title":"Colors"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","default":"json","title":"Format"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/anchor/pending":{"get":{"summary":"Anchor Pending","description":"Danh sách cây cần neo — worker (máy có khoá) đọc rồi ký + submit. CẦN BÍ MẬT.\n\nVì sao cổng này phải đóng, dù nó chỉ \"đọc\":\n\nMỗi mục trả về mang `code`, mà `code` do `identity.tree_code()` sinh ra theo khuôn\n`ORI-{geohash7}-{hậu-tố}` (identity.py:76-84). Geohash 7 ký tự là **một ô ~152m × 149m**.\nNghĩa là bất kỳ ai gọi cổng này cũng đọc được **vị trí vườn** của những cây đang chờ neo —\ntrong đó có cây RIÊNG TƯ. Đúng thứ mà lớp rọc-phách dựng ra để giấu, và đúng thứ\n`_is_public_tree` đã chặn ở `/t/{code}` lẫn `/api/provenance/{tid}`. Cổng này là lối vòng\nduy nhất còn hở, và nó hở với cả thế giới vì không cần đăng nhập gì.\n\nĐo trên máy chủ thật 07/08: 4 cây đang chờ, giải mã geohash ra 4 điểm ở Đắk Lắk, Đồng Nai\nvà TP.HCM. Không phải rủi ro lý thuyết.\n\nBí mật đi bằng HEADER chứ không bằng query: query nằm trong dòng nhật ký truy cập của\nCloudflare lẫn uvicorn, nên đưa bí mật vào đó là ghi nó ra đĩa mỗi lượt gọi.\n\n`ANCHOR_WORKER_SECRET` chưa đặt → `anchor_secret.khop` trả False → cổng đóng hẳn. Đóng khi\nthiếu cấu hình là đúng hướng: người vận hành quên đặt biến thì cổng câm, không phải cổng mở.","operationId":"anchor_pending_api_anchor_pending_get","parameters":[{"name":"x-anchor-secret","in":"header","required":false,"schema":{"type":"string","default":"","title":"X-Anchor-Secret"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/anchor/status":{"get":{"summary":"Anchor Status","description":"Sức khoẻ đường NEO: mạng, hàng đợi, mốc neo gần nhất, số cây kẹt, lý do 403 gần nhất,\nvà chuỗi lượt hỏng liên tiếp hiện tại (`failure_alert`, issue #239).\n\nTrả lời trong một lượt gọi đúng những câu mà trước đây phải SSH vào máy chủ mới biết:\nneo đang chạy mạng nào, khoá Blockfrost lấy từ BIẾN NÀO (tên biến — không bao giờ giá trị),\nlần neo thành công gần nhất là bao giờ, còn bao nhiêu cây khai 'pending', và nếu Blockfrost\nđang từ chối thì từ chối theo ca nào trong BA CA (thiếu khoá / khoá sai / khoá lệch mạng —\nba ca này cùng mã 403 nhưng ba cách sửa khác hẳn nhau).\n\nQUYỀN: nhóm quản trị, gác bằng `_monitor_account` (qua `_anchor_monitor_account`) y như\n`/api/admin/server_status`. KHÔNG mở công khai — `trees_pending`/`last_tx_hash` là đường vòng\nđể đếm và soi vườn người khác, đúng thứ `/api/anchor/pending` ngay dưới đây đã phải đóng lại\n(mã cây mang geohash 7 ký tự ≈ ô 152m).\n\n[#421] Khách (`OLT_MONITOR_OPEN=1`) chỉ đọc được `ok`, `open`, `ts`, `network`. Mọi con số\nvận hành đứng sau cổng quyền, vì hợp lại chúng nói ra ĐÚNG thứ cần giấu: đường neo đang hỏng\nvà hỏng từ bao giờ. Ràng buộc viết thành phép kiểm ở `test_anchor_alert.py`: dựng hai trạng\nthái hệ trái ngược nhau thì thân trả cho khách phải GIỐNG HỆT (trừ `ts`).","operationId":"anchor_status_api_anchor_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/storage_status":{"get":{"summary":"Admin Storage Status","description":"Ảnh mới lưu có rời khỏi máy này không, và nếu KHÔNG thì đã bao lâu rồi. CHỈ ĐỌC.\n\nTrả lời trong một lượt gọi đúng câu mà hôm nay phải SSH vào máy chủ rồi chạy công cụ dòng\nlệnh bằng tay mới biết. Ba con số cốt lõi:\n\n  • `records_recent`      — số bản ghi đem ra xét (N bản mới nhất).\n  • `records_single_node` — trong đó bao nhiêu bản chỉ còn nằm trên đúng một máy.\n  • `since`               — bản ghi một-nút CŨ NHẤT của chuỗi liên tục hiện tại. Đây là\n    trường quan trọng nhất: nó biến \"hôm nay lưu chưa tốt\" thành \"đã 2,52 ngày rồi\", và hai\n    câu đó dẫn tới hai hành động khác hẳn nhau.\n\nQUYỀN: `authr.require_admin` — cổng quản trị THẬT, cố ý KHÔNG dùng `_monitor_account` như\n`/api/admin/server_status` và `/api/anchor/status`. Hai cửa kia mở cho khách khi\n`OLT_MONITOR_OPEN=1`, và đo ngày 20/08 thì chúng đang trả 200 kèm `open=true` cho người lạ\ngọi từ Internet. Đặt con số này vào đó là phát ra ngoài đúng thứ mà `_public_prov` cố ý\ngiấu: một con số 0 bản sao ngoài máy là lời chỉ đích cho người muốn phá, nói thẳng rằng phá\nđúng một máy là mất bằng chứng. Cùng lý do, cửa này không có chế độ khách xem bản che —\nkhông có nhóm người nào được xem một nửa con số này.\n\nFAIL-CLOSED: đọc sổ hỏng thì trạng thái là `khong_do_duoc`, không phải `on`. Chuông mà mặc\nđịnh im lúc hỏng thì nó không phải chuông.\n\nVì sao đáng có: neo Cardano chứng minh bản ghi KHÔNG BỊ SỬA, nó không chứng minh bản ghi\nCÒN TỒN TẠI. Ảnh chỉ nằm một máy thì một lần hỏng ổ đĩa là mã băm trên chuỗi trỏ vào chỗ\ntrống — bằng chứng vẫn hợp lệ và vẫn vô dụng. Lớp lưu trữ phân tán dựng ra đúng để chặn ca\nđó, nên hệ phải biết khi lớp đó ngừng làm việc.\n\nÔ `push_alert` (#398) — TÍN HIỆU THỨ HAI, đọc kèm nhưng ĐỪNG lẫn với các con số ở trên.\nPhần trên đọc SỔ XUẤT XỨ: nó chỉ thấy những gì đã ghi thành bản ghi, nên nó luôn chậm hơn\nthực tế một nhịp và nó câm hẳn với những lượt đẩy hỏng tới mức chưa kịp thành bản ghi nào.\nÔ này đếm THẲNG từng lượt `lampnet.put_bytes` ngay lúc lượt đó kết thúc, nên nó trả lời\nđược ba câu mà sổ không trả lời được:\n\n  • `fail_pct`            — trong cửa sổ trượt vừa rồi, bao nhiêu phần trăm lượt đẩy kết\n    thúc mà mảnh KHÔNG rời khỏi máy này.\n  • `last_offsite_ok_at` — lần cuối một mảnh THẬT SỰ rời đi là bao giờ. Trên máy sản xuất\n    hôm 20/08, mốc này và `last_put_ok_at` lệch nhau 2,52 ngày, và khoảng lệch ấy chính là\n    toàn bộ nội dung issue #398: cổng vẫn nhận byte, vẫn trả mã nội dung, vẫn báo thành\n    công, trong khi không mảnh nào đi đâu cả.\n  • `window_khong_do_duoc` — bao nhiêu lượt không đo được. Sổ xuất xứ KHÔNG phân biệt được\n    ca này với ca \"đo được 0\" (`_inspect_do` khai MÙ, còn sổ vẫn ghi danh sách rỗng, rỗng đếm ra\n    0), nên đây là chỗ duy nhất trong hệ nói ra được là phép đo đang mù.\n\nHai tín hiệu có thể nói khác nhau, và khác nhau là có ích: đường đẩy vừa hỏng thì `push_alert`\nkêu ngay còn phần đọc sổ phải đợi bản ghi mới; ngược lại một đợt hỏng đã qua vẫn còn nằm\ntrong sổ sau khi đường đẩy đã lành. CỐ Ý không gộp hai chữ trạng thái làm một: gộp thì mất\nđúng cái thông tin nằm ở chỗ chúng lệch nhau, và `trang_thai` ở mức trên cùng đang là thứ\nbảng theo dõi đọc, đổi nghĩa của nó là đổi hành vi đang chạy.\n\nBộ đếm ấy nằm trong bộ nhớ tiến trình nên khởi động lại là mất, và nó nói về TIẾN TRÌNH NÀY\nchứ không phải về cả lịch sử. Đó là lý do phần đọc sổ ở trên vẫn phải có, chứ không phải lý\ndo để bỏ ô này.\n\nKHÔNG HỎI NÚT LƯU TRỮ QUA MẠNG. Hai bộ đếm `distribute_shard_total` /\n`distribute_shard_fail_total` mà issue nhắc tới là của nút LampNet, kho này không sinh ra\nchúng (xem docstring `lampnet_alert`). Đi hỏi một nút đang chết là cách biến một sự cố lưu\ntrữ thành sự cố thứ hai, ngay tại cửa mà người trực đang cần nó trả lời. Nên cửa này chỉ đọc\nthứ máy này tự đo được.\n\nChi phí: một vòng duyệt sổ trong bộ nhớ (máy sản xuất đang 177 bản ghi) cộng một vòng đếm\ntrên cửa sổ trượt tối đa 5000 phần tử. Không chạm mạng, không chạm đĩa, không ghi gì.","operationId":"admin_storage_status_api_admin_storage_status_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/anchor/submit":{"post":{"summary":"Anchor Submit","description":"Worker ghi kết quả neo về. Cần ANCHOR_WORKER_SECRET khớp (chặn POST giả).\n\n`record_hash` BẮT BUỘC: worker lấy nó từ chính /api/anchor/pending nên luôn có sẵn, và\nthiếu nó thì bản ghi chỉ khai 'confirmed' mà không nói confirmed NỘI DUNG NÀO — đúng lỗ đã\nlàm 8/110 cây neo lệch mà không đường nào phát hiện. Từ chối sớm (400) thay vì nhận rồi ghi\nmột lời khai rỗng: tx đã lên chuỗi vẫn còn đó, cây vẫn nằm trong /api/anchor/pending, lượt\nsau worker gửi đủ trường là xong.","operationId":"anchor_submit_api_anchor_submit_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_anchor_submit_api_anchor_submit_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/qr/{code}":{"get":{"summary":"Qr","description":"Mã QR (SVG) trỏ tới trang công khai của cây.\n\nHỢP-ĐỒNG LỖI: MỌI lỗi của /qr/ đều text/plain — soi gương /t/ (endpoint anh em, cùng ăn\n`code`, cũng chỉ text/plain). Trước đây 400 đi qua HTTPException → _http_exc_unified → JSON,\ncòn 404 lại text/plain: CÙNG một endpoint mà client phải parse 2 kiểu thân lỗi.\nGiữ NGUYÊN mã 400 cho code sai format (không hạ về 404): chuỗi sai format KHÔNG THỂ là mã\nserver sinh ⇒ 400 không hề lộ cây nào tồn-tại — nó nói về CÚ PHÁP, không phải về sự tồn-tại.","operationId":"qr_qr__code__get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/backfill/{tree_id}":{"post":{"summary":"Backfill","description":"Tạo xuất xứ (mã+LampNet+bản ghi) cho 1 cây ĐÃ đăng ký trước đây + dựng 3D.","operationId":"backfill_api_backfill__tree_id__post","parameters":[{"name":"tree_id","in":"path","required":true,"schema":{"type":"string","title":"Tree Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/showcase/{tree_id}":{"post":{"summary":"Admin Showcase","description":"ADMIN (secret): tạo xuất xứ + 3D + quả + neo cho 1 cây CÓ SẴN (legacy) để trưng bày.\nTrang công khai /t /view không cần đăng nhập nên ai cũng xem được.","operationId":"admin_showcase_api_admin_showcase__tree_id__post","parameters":[{"name":"tree_id","in":"path","required":true,"schema":{"type":"string","title":"Tree Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_admin_showcase_api_admin_showcase__tree_id__post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tree/{tree_id}/model3d":{"get":{"summary":"Tree Model3D","description":"Dữ-liệu 3D của 1 cây cho app tự dựng cảnh native (#281, chị Thư) — CHỈ ĐỌC, additive.\n\nGom point cloud + quả + khung xương vào MỘT lần gọi và quan-trọng hơn là vào CÙNG một hệ\ntoạ-độ: cả ba lớp đều được dời tâm cùng một lượng, đúng như trang xem 3D đang làm ở phía\ntrình duyệt. Trước đây app phải tải `.ply` thô qua `/gimg` rồi tự ghép với toạ-độ quả nằm\ntrong bản ghi xuất-xứ, ba lớp ba đường, lệch một lớp là cây vẽ sai mà không có lỗi nào.\n\nTham-số (đều tuỳ chọn):\n  - `max_points`: trần số điểm, mặc-định 20000 (chị Thư đo an-toàn cho cả máy yếu). App tự\n    chọn theo đời máy. Hạ mẫu ĐỀU theo không-gian (lưới ô vuông) nên hạ sâu cây vẫn ra dáng.\n  - `max_fruits`: trần số quả, mặc-định 300 — ĐÚNG con số trang xem 3D của kho đang chạy\n    (`view_page.MAX_FRUITS_DRAW`), và nay dùng chung một hàm cắt với trang đó nên cùng một\n    cây không thể ra hai bộ quả khác nhau ở hai màn hình. Cắt theo độ tin GIẢM DẦN.\n  - `max_segments`: trần số đoạn khung xương, mặc-định 5000. Khung xương không có điểm để\n    xếp hạng nên cắt theo thứ tự bộ dựng ghi ra; `meta.n_skeleton_kept` nói số trước khi\n    cắt để app biết mình đang vẽ một cây cụt cành.\n  - `colors=0`: bỏ màu, cắt khoảng một phần ba dung-lượng, cho app tô theo chiều cao.\n  - `format=bin`: trả toạ-độ dạng nhị-phân base64 thay vì mảng số, nhẹ hơn khoảng ba lần và\n    app khỏi đọc từng con số.\n\nCả ba nhận CHUỖI chứ không nhận số, và đó là chỗ vừa sửa theo anh Đức (#282): khai kiểu số\nthì khung web chặn ngay ở cửa, app gửi `?max_points=` rỗng là ăn lỗi 422 chứ không về\nmặc-định như tài-liệu hứa. Mà biến rỗng rồi nối vào đường dẫn là chuyện xảy ra hằng ngày ở\nphía app. Nhận chuỗi rồi để `clamp_max_points` tự lọc thì đúng như đã hứa, và nhánh bắt chữ\n`false` cho `colors` mới thật-sự chạy chứ không nằm chết ở đó.\n\n`max_points` khai `Optional[str]` chứ không `str`: mặc-định của nó LÀ `None`, nên khai `str`\nlà lời khai sai ngay trong chính chữ ký hàm. Không đổi hành-vi một ly nào (khung web đọc\ngiá-trị mặc-định chứ không đọc chú-thích kiểu), nhưng chú-thích kiểu là thứ người đọc mã và\nmáy soi kiểu tin theo — để nó nói sai thì bộ soi báo sai chỗ, và người đọc sau tưởng tham-số\nnày bắt buộc phải có mặt.\n\nCây chưa dựng-3D trả 200 với danh-sách RỖNG kèm `meta.status` là `none`/`building`/`failed`,\nKHÔNG trả lỗi HTTP: app hiện chữ tương ứng rồi hỏi lại hoặc mời dựng lại, dễ hơn phải bắt mã\nlỗi. `failed` (#282, anh Đức) là lượt dựng GẦN NHẤT đã thử và hỏng — khác `none` (chưa ai\nbấm dựng bao giờ): trước bản vá này hai ca đó trả CÙNG một trạng-thái, nông dân bấm dựng,\nhỏng, app hiện lại đúng màn \"chưa dựng, bấm để dựng\", bấm lại, hỏng lại, không ai nói vì\nsao. `meta.error` mang câu lý-do ngắn khi `status == \"failed\"`. Chỉ chủ vườn đọc được (chống\nIDOR, nhất quán build3d và capture_guidance).\n\n`meta.layers` nói trạng-thái RIÊNG từng lớp, dùng CHUNG một bộ bốn giá-trị cho cả ba khoá\n(`ready`/`empty`/`pending`/`failed` — xem `model3d._points_layer_status`/`_layer_status`).\nHôm nay quả và khung xương chưa có nơi nào sản-xuất trong lúc máy chủ đang chạy, nên chúng\ntrả `pending` chứ không phải rỗng suông: app đọc được chữ đó ở BẤT KỲ khoá nào thì tạm giấu\nmàn-hình đó đi, thay vì dựng một màn-hình không bao giờ có dữ-liệu.\n\n`meta.coverage` (đo lúc dựng — độ phủ góc quanh cây, lời khuyên chụp thêm) và\n`meta.n_points_model` (số điểm mô-hình có lúc dựng) cho app biết bản dựng CÒN SƠ hay đã đủ\ndùng, đúng câu chị Thư cần trước khi build màn hình: mật-độ điểm trên máy chủ thật đang rất\nthưa: đo lại 154 lượt dựng ngày 18/09/2026 (#517) ra trung vị 366 điểm, một phần tư số lượt\ncòn dưới 88 điểm. Ở cỡ đó thì thứ vẽ ra là một nhúm chấm chứ chưa ra dáng cây.","operationId":"tree_model3d_api_tree__tree_id__model3d_get","parameters":[{"name":"tree_id","in":"path","required":true,"schema":{"type":"string","title":"Tree Id"}},{"name":"max_points","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Max Points"}},{"name":"max_fruits","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Max Fruits"}},{"name":"max_segments","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Max Segments"}},{"name":"colors","in":"query","required":false,"schema":{"type":"string","default":"1","title":"Colors"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","default":"json","title":"Format"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/build3d/{tree_id}":{"post":{"summary":"Build3D","description":"Dựng (hoặc dựng lại) 3D cho 1 cây — chạy nền.","operationId":"build3d_api_build3d__tree_id__post","parameters":[{"name":"tree_id","in":"path","required":true,"schema":{"type":"string","title":"Tree Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tree/{tree_id}/growth":{"get":{"summary":"Tree Growth","description":"Chuỗi các lượt dựng-3D của một cây theo thời gian — ĐỌC-ONLY.\n\nTrả DÁNG chứ không trả CỠ, và nói thẳng điều đó trong chính response. Bản dựng SfM chỉ xác\nđịnh tới một phép đồng dạng nên không suy ra được chiều cao hay đường kính; ai đọc endpoint\nnày rồi in ra \"cây cao thêm 12%\" là đang in ra khoảng cách của người cầm máy. Lý do đầy đủ ở\nđầu `growth_metrics`.\n\nCHỈ CHỦ VƯỜN: chuỗi này nói vườn đang khá lên hay kém đi — thông tin kinh doanh, cùng hạng\nvới video thô. Cây chưa dựng-3D lần nào → rỗng sạch, không lỗi.","operationId":"tree_growth_api_tree__tree_id__growth_get","parameters":[{"name":"tree_id","in":"path","required":true,"schema":{"type":"string","title":"Tree Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/capture_guidance/{tree_id}":{"get":{"summary":"Capture Guidance View","description":"Gợi-ý GÓC-HỞ cho app chụp bù (next-best-view #81) — ĐỌC-ONLY, additive.\n\nĐọc camera poses ĐÃ persist (PROV.set_poses, NGOÀI record neo) + point cloud model.ply,\nchạy capture_guidance (thuần hình-học numpy) → trả câu hướng-dẫn-quay tiếng Việt + sector\ncòn hở cho app hướng nông dân lia bù. KHÔNG đụng verdict định-danh (chỉ đọc, không ghi).\n\nGiấu nội-tạng: KHÔNG lộ ma-trận pose thô (K/R/t) — chỉ trả tóm-tắt hình-học (span/gap/\nparallax) + hướng-dẫn. Cây chưa dựng-3D / chưa có poses → trả RỖNG SẠCH (ok=True,\nguidance=[], không crash). Chỉ chủ vườn (chống IDOR) — guidance là đường của người đang\nchụp cây của họ.","operationId":"capture_guidance_view_api_capture_guidance__tree_id__get","parameters":[{"name":"tree_id","in":"path","required":true,"schema":{"type":"string","title":"Tree Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/capture/plan":{"get":{"summary":"Capture Plan","description":"Còn thiếu gì và nên chụp gì TIẾP — một cửa cho cả cây, quả lẫn con vật. ĐỌC-ONLY, additive.\n\nApp gọi ở hai lúc: (a) mở màn hình chụp, để hiện \"quả này còn thiếu mặt đáy\" thay vì\n\"đã có 4 ảnh\"; (b) ngay sau một lượt bị từ chối, để biến câu từ chối thành một việc làm\nđược. Trả cùng một hình dạng cho cả ba loại đối tượng nên app dựng MỘT màn dùng chung.\n\n`have_kind` nói bên đọc phải hiểu `have` theo nghĩa nào: \"faces\" (quả — đếm theo mặt) hay\n\"coverage\" (cây, con vật — đếm theo số ảnh). Con vật KHÔNG có `missing`/`thin` vì kho chưa\nghi mặt cho từng ảnh; mảng rỗng ở đó là câu trả lời đúng, không phải chỗ chưa làm.\n\nChỉ đối tượng thuộc tài khoản đăng nhập (chống IDOR). Quả tra theo cây chủ của nó; con vật\ntra qua kho động-vật (kho riêng, không đi qua tenancy của cây).\n\n`device` (JSON, tùy chọn) = năng lực máy đang cầm, do hệ điều hành tự trả lời, KHÔNG hỏi\nngười dùng. Có khai thì thân trả về kèm `device_route` — phương pháp + module nên nạp cho\nlượt chụp này (`_capability_route`). `task` ép việc cần định tuyến khi khác mặc-định của loại\nđối tượng (`_ROUTE_TASK_BY_TARGET`). Không khai → `device_route: null`, thân còn lại y hệt.","operationId":"capture_plan_api_capture_plan_get","parameters":[{"name":"target_type","in":"query","required":true,"schema":{"type":"string","title":"Target Type"}},{"name":"target_id","in":"query","required":true,"schema":{"type":"string","title":"Target Id"}},{"name":"after_reject","in":"query","required":false,"schema":{"type":"string","title":"After Reject"}},{"name":"device","in":"query","required":false,"schema":{"type":"string","title":"Device"}},{"name":"task","in":"query","required":false,"schema":{"type":"string","title":"Task"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/resolve/{code}":{"get":{"summary":"Resolve Api","description":"Cổng truy xuất — bản JSON. Cùng ba trạng thái, cùng luật, cùng hàm `_resolve_any`.\n\nMột nguồn sự thật cho cả trang HTML lẫn app: hai đường mà tự quyết định riêng thì sớm muộn\nmột đường sẽ rộng hơn đường kia, và chỗ rộng hơn luôn là chỗ rò.","operationId":"resolve_api_api_resolve__code__get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/r/{code}":{"get":{"summary":"Resolve Page","description":"Cổng truy xuất — bản trang cho người. Đường ngắn `/r/` để in kèm mã khi cần gõ tay.","operationId":"resolve_page_r__code__get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/t/{code}":{"get":{"summary":"Public Tree","description":"Trang công khai một cây — GIỮ NGUYÊN đường dẫn vì QR đã in ra giấy và đã dán ngoài vườn.\n\nĐi qua đúng cổng chung: cây công khai vẫn ra `_public_page` không đổi một byte; cây riêng\ntư nay ra THẺ TỐI THIỂU thay vì 404 (#195); chủ mở cây mình thì thấy đầy đủ.","operationId":"public_tree_t__code__get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/view/{code}":{"get":{"summary":"View 3D","description":"Xem mô hình 3D (point cloud + quả + khung xương) xoay được — three.js.\n\nHTML do view_page._view_page_html dựng (đã gia cố XSS: escape mọi text,\nnhãn quả + anchor link dựng bằng DOM API, giới hạn số quả vẽ, guard NaN).\n\nHAI HẠNG NGƯỜI XEM, MỘT CỬA. Trước đây cửa này chỉ hỏi \"cây có công khai không\", nên nông\ndân dựng xong mô hình cây của CHÍNH MÌNH mà để hồ sơ ở chế độ kín thì mở ra gặp 404 — mất\nđúng thứ họ vừa bỏ công đi vòng quanh cây để chụp. `/qr/{code}` và `_resolve_tree` đã xử\nđúng từ lâu (`công khai HOẶC người này đọc được cây riêng tư`); đây chép về cùng một luật,\nkhông nghĩ ra luật thứ hai.\n\nKhông tồn tại và không-phải-của-mình vẫn CÙNG một 404: nới cho chủ không được kéo theo việc\nlộ ra rằng mã đó có thật.\n\nĐÂY LÀ ĐƯỜNG CỦA TRÌNH DUYỆT, KHÔNG PHẢI ĐƯỜNG CỦA APP. App dựng cảnh 3D bằng mã native qua\n`/api/tree/{tree_id}/model3d` (#282) — đường đó tự mang `Authorization` ở từng lượt gọi và\ntrả cả ba lớp trong cùng một hệ toạ độ. Ở đây chỉ cần đúng phiên đăng nhập của trình duyệt:\nlượt tải `.ply` con là cùng gốc nên tự mang cookie phiên, `/gimg` nhận ra chủ và cho qua (đo\nthật: có cookie → 200, không cookie → 404). Không cần đúc thêm chìa nào trong địa chỉ.","operationId":"view_3d_view__code__get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/kind":{"post":{"summary":"Identify Kind","description":"MỘT tấm ảnh → máy tự nói đây là CÂY, QUẢ hay CON VẬT, rồi chỉ sang cửa định-danh đúng.\n\nVì sao có cửa này: ba tuyến định-danh của OriLife mỗi tuyến một cửa, và tới nay việc chọn\ncửa nào vẫn là app hỏi người dùng bấm nút. Đó đúng là hình dạng mà OriLife sinh ra để phá —\nđẩy việc phân loại sang cho con người vì máy chưa làm. Người cầm điện thoại lia vào một thứ\ntrong vườn thì máy phải tự biết mình đang nhìn cái gì.\n\nNguyên mẫu ba loại dựng TỪ CHÍNH kho của máy chủ (xem kind_gate) nên không cần huấn luyện\nthêm và tự khoẻ lên theo dữ liệu thật. Kho còn mỏng hoặc hai loại sát nhau → `need_confirm`\n= true và `kind` vẫn trả ra để app gợi ý, KHÔNG im lặng đoán bừa.","operationId":"identify_kind_api_kind_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_identify_kind_api_kind_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/identify/auto":{"post":{"summary":"Identify Auto","description":"MỘT cửa cho MỌI loại thực thể: gửi ảnh, máy tự biết đang nhìn cây, quả hay con vật rồi\nđịnh danh luôn — ứng dụng không phải hỏi người dùng đang chụp cái gì.\n\nVì sao cần cửa này khi đã có ba cửa riêng. Ba cửa riêng bắt ứng-dụng biết trước loại, mà cách\nduy-nhất ứng-dụng biết trước là hỏi người cầm máy bấm một nút chọn loại. Đó đúng là hình dạng\nOriLife sinh ra để phá: đẩy việc phân-loại sang cho con người vì máy chưa làm. `/api/kind` đã\nlàm nửa đầu (máy tự nói đang nhìn cái gì) nhưng dừng ở đó, nên ứng-dụng vẫn phải tự đi tiếp\nmột lượt gọi nữa và tự dựng ba nhánh mã. Cửa này nối nốt nửa sau.\n\nTrả về:\n  `kind`        — cây / quả / con vật máy tự nhận, kèm `kind_confidence`.\n  `lane`        — cửa đã chạy, để ứng-dụng gọi thẳng lần sau nếu muốn tiết-kiệm một bước.\n  `result`      — NGUYÊN VĂN phản-hồi của cửa đó, không cắt không đổi tên trường.\n  `need`        — danh sách trường còn thiếu, khi cửa đích đòi thêm dữ-kiện (xem dưới).\n\nChỗ chưa liền mạch, khai thẳng chứ không giấu: cửa con vật đang đòi `species` và `farm_id`.\nKhông có hai trường đó thì cửa này trả `kind=\"animal\"` kèm `need` chứ KHÔNG đoán hộ — đoán\nloài hộ người dùng là dựng một dữ-kiện không ai kiểm được rồi ghi nó vào hồ-sơ cá-thể. Hai\ntrường đó tự chúng là món nợ đúng hướng cần trả (máy phải tự nhận loài), và ngày trả xong thì\nchỗ này rụng đi, hợp-đồng ngoài cửa không đổi.\n\nHoa và sản-phẩm chế-biến chưa có tuyến định-danh nào — `kind` sẽ không bao giờ trả hai giá-trị\nđó, và cửa này nói \"chưa nhận ra\" chứ không xếp bừa vào ba loại đang có.","operationId":"identify_auto_api_identify_auto_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_identify_auto_api_identify_auto_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fruit/detect":{"post":{"summary":"Fruit Detect","description":"Phát hiện quả sầu riêng trong ảnh → trả danh sách bbox.\n\nClient dùng để overlay bbox lên camera trước khi enroll.\n\ntree_id (tuỳ chọn — tương thích ngược): nếu có → tra LOÀI của cây. Loài has_fruit=false\n(vd sứ) → KHÔNG chạy detect, trả {ok:false, reason:\"species_no_fruit\", species, products}\nđể web chuyển sang luồng sản-phẩm khác (hoa/thân). Loài CÓ quả nhưng khai bộ dò KHÁC\n(xoài/mít = \"generic_blob\" — chưa có mã) → {ok:false, reason:\"species_detector_missing\"}:\nthà nói \"chưa dò được\" còn hơn trả bbox sầu riêng chụp lên tán lá xoài. Cây chưa rõ loài\n→ detect như cũ (luồng này do người dùng chủ-động bấm). tree_id=None → hành vi cũ y nguyên.","operationId":"fruit_detect_api_fruit_detect_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_fruit_detect_api_fruit_detect_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fruit/enroll":{"post":{"summary":"Fruit Enroll","description":"Đăng ký 1 quả sầu riêng lên cây tree_id.\n\nLuồng:\n  1. Farmer đã chọn cây tree_id (đã định danh bằng /api/identify).\n  2. Upload 1 ảnh quả (cận cảnh, rõ vỏ gai).\n  3. Server auto-detect quả → embed → kiểm duplicate → gán fruit_id.\n  4. Trả fruit_id để client hiện + lưu phiên.\n\nbbox_x/y/w/h (tuỳ chọn): bbox farmer khoanh thủ công nếu auto-detect sai.","operationId":"fruit_enroll_api_fruit_enroll_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_fruit_enroll_api_fruit_enroll_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fruit/identify":{"post":{"summary":"Fruit Identify","description":"Nhận diện lại quả sầu riêng (chỉ trong vườn của tài khoản — chống IDOR).\n\ntree_id (tuỳ chọn):\n  - Có: chỉ tìm trong cây đó (weekly rescan, harvest cây thấp)\n  - Không có: tìm toàn vườn CỦA TÀI KHOẢN (harvest cây cao dùng sào, nhà máy)","operationId":"fruit_identify_api_fruit_identify_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_fruit_identify_api_fruit_identify_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fruit/identify_verdict":{"post":{"summary":"Fruit Identify Verdict","description":"MCR-PoC-Fruit L2 — GHI GROUND-TRUTH cho 1 lượt /api/fruit/identify (join bằng query_id).\n\nMirror /api/identify_verdict của CÂY, nhưng cho QUẢ. verdict ∈ {correct, wrong, other}.\ncorrect_fruit_id (tùy) = quả ĐÚNG (client lấy từ /api/fruit/list), CHỈ tin khi là quả của\nCHÍNH CHỦ (chống ghi/dò quả người khác; sai-chủ → bỏ nhãn, KHÔNG 403 vì đây là nhãn-đo).\nGhi 1 dòng ground_truth_fruit.jsonl. KHÔNG lộ nội-tạng so-khớp ra client.","operationId":"fruit_identify_verdict_api_fruit_identify_verdict_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_fruit_identify_verdict_api_fruit_identify_verdict_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fruit/candidates":{"post":{"summary":"Fruit Candidates","description":"XẾP-HẠNG ứng-viên quả của 1 CÂY theo độ giống ảnh truy vấn (UX redesign quả).\n\nMirror cách xếp ứng-viên-CÂY (/api/identify): trả danh sách quả của cây tree_id,\ncao→thấp, cap 8, KÈM thumbnail để nông dân nhìn-chọn. KHÔNG kèm điểm sim (giấu\nnội-tạng so-khớp, nhất quán identify cây/quả). Cây chưa có quả → candidates [].\n\n(Đặt ở route /candidates — KHÔNG đụng /api/fruit/identify cũ, vốn là luồng nhận-diện-lại\ntoàn-vườn trả decision; ADDITIVE, không phá hợp đồng/endpoint cũ.)\n\nbbox_x/y/w/h HOẶC shape+points: khoanh đúng quả để crop+embed (tái dùng như enroll).","operationId":"fruit_candidates_api_fruit_candidates_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_fruit_candidates_api_fruit_candidates_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fruit/add_view":{"post":{"summary":"Fruit Add View","description":"Bổ sung góc chụp thêm cho quả đã đăng ký (weekly rescan xác nhận).\n\nVERIFY chống chimera-quả (mirror allow_dup của enroll): trước khi thêm, so góc mới với\nchính quả X (best_self) và với MỌI quả KHÁC cùng cây (best_other).\n  - best_other - best_self >= FRUIT_VIEW_MARGIN → góc mới giống quả KHÁC hơn RÕ RỆT\n    → warn=\"better_other\". Phải có BIÊN, không chỉ \"lớn hơn\": đo nhật ký thật 07/08,\n    30/32 lượt bị chặn có chênh ≤ 0.092 trong khi ảnh mới vẫn giống chính quả đó\n    0.787-0.876. Chặn theo \"lớn hơn\" là tung đồng xu, và nó ăn đúng nửa số ảnh\n    nông dân chụp.\n  - best_self  < FRUIT_VIEW_MIN → góc mới quá khác chính quả X → warn=\"low_self\"\nCó warn VÀ allow_mismatch KHÔNG-truthy → KHÔNG thêm, trả {ok:false, warn, best_other} để HỎI.\nallow_mismatch truthy (1/true/yes) HOẶC không warn → thêm như cũ, trả {ok:true, n_views, warn:null}.","operationId":"fruit_add_view_api_fruit_add_view_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_fruit_add_view_api_fruit_add_view_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fruit/list":{"get":{"summary":"Fruit List","description":"List quả. tree_id: chỉ quả của cây đó; bỏ trống: toàn vườn CỦA TÀI KHOẢN.\n\nLuôn lọc theo owner (chống IDOR — không lộ quả tài khoản khác).","operationId":"fruit_list_api_fruit_list_get","parameters":[{"name":"tree_id","in":"query","required":false,"schema":{"type":"string","title":"Tree Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fruit/{fruit_id}/views":{"get":{"summary":"Fruit Views","description":"Danh sách ảnh các góc của 1 quả (để web hiện thumbnail quả-cũ INLINE khi chọn quả).\n\nurl = \"/gimg/\"+img_path (giống /api/tree_views). Chỉ quả thuộc tài khoản (chống IDOR).\n\n?describe=1 (LAZY, mặc-định TẮT): thêm `features_vi` — câu tiếng Việt tả đặc-điểm ĐO\nĐƯỢC của quả trong từng ảnh (dáng, đuôi quẹo, vết chàm, mặt đang nhìn). CÙNG cờ, CÙNG\ntên trường, CÙNG dải với `/api/tree_views?describe=1` — màn hình cây và màn hình quả\nđọc bằng một khối mã. Tắt mặc-định để response cũ y-nguyên từng byte.","operationId":"fruit_views_api_fruit__fruit_id__views_get","parameters":[{"name":"fruit_id","in":"path","required":true,"schema":{"type":"string","title":"Fruit Id"}},{"name":"describe","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Describe"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fruit/{fruit_id}":{"get":{"summary":"Fruit Detail","description":"Chi tiết 1 quả.","operationId":"fruit_detail_api_fruit__fruit_id__get","parameters":[{"name":"fruit_id","in":"path","required":true,"schema":{"type":"string","title":"Fruit Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Fruit Delete","description":"Xóa quả (farmer nhập nhầm).","operationId":"fruit_delete_api_fruit__fruit_id__delete","parameters":[{"name":"fruit_id","in":"path","required":true,"schema":{"type":"string","title":"Fruit Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fruit/{fruit_id}/model3d":{"get":{"summary":"Fruit Model3D","description":"Hình-học 3D của MỘT quả cho app dựng bằng mã gốc (#281) — CHỈ ĐỌC, chỉ chủ vườn.\n\nQuả KHÔNG có đám mây điểm riêng, và cửa này nói thẳng điều đó thay vì đắp cho đủ ba lớp\ngiống cây. Thứ quả thật-sự có là hai món, và cửa trả đúng hai món đó:\n\n  • `shape` — ellipsoid ba bán trục tính bằng CENTIMET THẬT, suy từ số đo `fruit_mass`. Đây\n    là chỗ quả hơn hẳn cây: cảnh cây dựng từ ảnh đơn-mắt nên không có thang tuyệt-đối, còn\n    quả thì có mỏ neo mét đến từ MÁY (độ sâu cảm biến + tiêu cự ống kính), không hỏi ai một\n    câu nào. Nên `coordinate_system.unit` ở cửa này là `\"cm\"` chứ không phải `null`.\n  • `anchor` — chỗ quả nằm trên mô-hình cây mẹ.\n\nHai món đó hôm nay thường VẮNG, và cửa trả về lý-do máy đã ghi sẵn chứ không bịa hình:\n`OLT_FRUIT_VOLUME` mặc-định TẮT nên phần lớn quả chưa từng đo; còn `anchor` thì đường nối\nĐÃ có (`model3d.link_fruit_anchors` gắn mã sổ `ORI-FRUIT-{8 hex}` vào đốm quả, `fr_000`\nxuống hàng nhãn cục bộ của một lượt dựng) nhưng chưa có làn nào GỌI nó.\n\nĐỪNG LẪN HAI CHUYỆN Ở ĐÂY. `set_fruits` nay ĐÃ có người ghi: `_attach_fruit_anchors` chạy ở\ncuối mỗi lượt dựng 3D, đứng sau công-tắc `OLT_FRUIT_ANCHORS` mặc-định TẮT. Bật công-tắc đó\nthì cây CÓ đốm quả và `layers.fruits` ở cửa CÂY sống dậy, nhưng cửa QUẢ này vẫn vắng\n`anchor`: bộ hình-học chỉ thấy đốm màu trên ảnh nên nó phát ra `fruit_id: None` cho mọi\nđốm, và `link_fruit_anchors` vẫn chưa có người gọi. Hai lớp mở ra ở hai lượt khác nhau.\n\nVắng thì `meta.layers` nói `pending` kèm `shape_reason_vi`/`anchor_reason_vi` hiện thẳng\nđược cho người dùng.\n\nMã lỗi: 400 khi `fruit_id` sai khuôn (kể cả khi app đưa nhầm mã CÂY vào cửa quả — `_vfruit`\nbắt ở biên), 404 khi không có quả đó, 403 khi quả thuộc cây của tài-khoản khác.","operationId":"fruit_model3d_api_fruit__fruit_id__model3d_get","parameters":[{"name":"fruit_id","in":"path","required":true,"schema":{"type":"string","title":"Fruit Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fruit/{fruit_id}/status":{"patch":{"summary":"Fruit Update Status","description":"Cập nhật trạng thái quả: on_tree → harvested | lost.\n\nLượt đổi THẬT được ghi vào dòng thời-gian của quả: `harvest` khi quả đã hái, `observe` cho\ncác trạng-thái còn lại (mất quả là một quan-sát, không phải một vụ thu). Đổi sang đúng\ntrạng-thái đang có → không ghi gì (xem FruitGallery.update_status).","operationId":"fruit_update_status_api_fruit__fruit_id__status_patch","parameters":[{"name":"fruit_id","in":"path","required":true,"schema":{"type":"string","title":"Fruit Id"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_fruit_update_status_api_fruit__fruit_id__status_patch"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/booth/create":{"post":{"summary":"Booth Create","description":"Tạo phiên trưng bày rỗng → trả mã khách + URL trang quét (đem in QR).","operationId":"booth_create_api_booth_create_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_booth_create_api_booth_create_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/booth/list":{"get":{"summary":"Booth List","description":"Các phiên của tài khoản (mới nhất trước).","operationId":"booth_list_api_booth_list_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/booth/{booth_id}":{"get":{"summary":"Booth Detail","description":"Chi tiết phiên + danh sách quả (tên, cây mẹ, ảnh) để chủ soát trước giờ mở cửa.","operationId":"booth_detail_api_booth__booth_id__get","parameters":[{"name":"booth_id","in":"path","required":true,"schema":{"type":"string","title":"Booth Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/booth/{booth_id}/fruits/add":{"post":{"summary":"Booth Add Fruits","description":"Thêm quả vào phiên. CHỈ nhận quả của chính chủ — quả lạ bị bỏ và báo lại rõ ràng.","operationId":"booth_add_fruits_api_booth__booth_id__fruits_add_post","parameters":[{"name":"booth_id","in":"path","required":true,"schema":{"type":"string","title":"Booth Id"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_booth_add_fruits_api_booth__booth_id__fruits_add_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/booth/{booth_id}/fruits/remove":{"post":{"summary":"Booth Remove Fruits","description":"Bớt quả khỏi phiên (quả đã bán / đã bổ).","operationId":"booth_remove_fruits_api_booth__booth_id__fruits_remove_post","parameters":[{"name":"booth_id","in":"path","required":true,"schema":{"type":"string","title":"Booth Id"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_booth_remove_fruits_api_booth__booth_id__fruits_remove_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/booth/{booth_id}/lock":{"post":{"summary":"Booth Lock","description":"KHOÁ danh sách (chốt trước giờ mở cửa) — vẫn quét được. locked=0 để mở khoá lại.","operationId":"booth_lock_api_booth__booth_id__lock_post","parameters":[{"name":"booth_id","in":"path","required":true,"schema":{"type":"string","title":"Booth Id"}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_booth_lock_api_booth__booth_id__lock_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/booth/{booth_id}/close":{"post":{"summary":"Booth Close","description":"ĐÓNG phiên — MỘT CHIỀU: mã hết quét được VÀ hết mở được ảnh (đây là chỗ thu hồi).\n\nMuốn bày tiếp thì tạo phiên mới, mã mới. Cố ý không cho mở lại: mã đã phát ra ngoài (QR\ndán bàn, ảnh chụp màn hình) thì coi như đã lộ, hồi sinh nó là hồi sinh cả chìa đã lộ.","operationId":"booth_close_api_booth__booth_id__close_post","parameters":[{"name":"booth_id","in":"path","required":true,"schema":{"type":"string","title":"Booth Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/booth/public/{code}":{"get":{"summary":"Booth Public Info","description":"Thông tin tối thiểu cho trang quét của khách. KHÔNG trả danh sách quả (không đưa\nnguyên bản đồ kho cho người lạ) — chỉ tên gian hàng + số quả đang bày.","operationId":"booth_public_info_api_booth_public__code__get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/booth/{code}/img/{fruit_id}/{idx}":{"get":{"summary":"Booth Fruit Img","description":"Ảnh đã đăng ký của 1 quả — CHỈ khi quả đó thuộc đúng phiên `code` và phiên còn mở.","operationId":"booth_fruit_img_api_booth__code__img__fruit_id___idx__get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"fruit_id","in":"path","required":true,"schema":{"type":"string","title":"Fruit Id"}},{"name":"idx","in":"path","required":true,"schema":{"type":"integer","title":"Idx"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/booth/{code}/tree_img/{tree_id}/{idx}":{"get":{"summary":"Booth Tree Img","description":"Ảnh CÂY MẸ — chỉ cây có quả trong phiên `code`.","operationId":"booth_tree_img_api_booth__code__tree_img__tree_id___idx__get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}},{"name":"tree_id","in":"path","required":true,"schema":{"type":"string","title":"Tree Id"}},{"name":"idx","in":"path","required":true,"schema":{"type":"integer","title":"Idx"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/booth/img/{token}":{"get":{"summary":"Booth Img By Token","description":"Ảnh gallery phục vụ bằng MÃ theo lượt quét (S7): không đoán được, hết hạn, thu hồi được.\n\nBa lớp phải cùng đúng mới trả ảnh: mã còn hạn · phiên còn mở · quả VẪN nằm trong phiên.\nBán quả đi (bớt khỏi phiên) hay đóng phiên là ảnh tắt ngay, dù mã chưa hết hạn.","operationId":"booth_img_by_token_api_booth_img__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fruit/scan":{"post":{"summary":"Fruit Scan","description":"KHÁCH THAM QUAN quét vỏ một quả đang bày → NĂM ỨNG VIÊN KÈM ẢNH để khách tự đối chiếu.\n\nKhác /api/fruit/identify (của nông dân, có tài khoản) ở ba chỗ cốt lõi:\n\n1. PHẠM VI: chỉ so trong danh sách quả của ĐÚNG phiên `code`. Không bao giờ nới ra kho —\n   mở tập với quả sầu riêng là sai gần như chắc chắn (xem booth.py).\n2. BẮT BUỘC KHOANH VÙNG: bàn hội chợ bày nhiều quả sát nhau. Nếu thiếu khung, endpoint này\n   KHÔNG tự lấy bbox lớn nhất như luồng cũ (chọn nhầm quả TRƯỚC CẢ khi so khớp) mà trả về\n   danh sách vùng phát hiện được để người quét CHỌN.\n3. KHÔNG TỰ KHẲNG ĐỊNH (sửa 04/08/2026 theo số đo thật): ở ngưỡng 0,72 có 73% cặp quả KHÁC\n   NHAU trên cùng một cây bị nhận nhầm là cùng quả, còn siết tới mức hết nhận nhầm thì mất\n   92% quả thật — không có ngưỡng nào cứu được một đáp án đơn. Nên mặc định trả tối đa 5\n   ứng viên xếp hạng KÈM ẢNH (rank-5 đo được 0,92–0,95) để khách tự so vết sẹo/mảng gai.\n   Một đáp án duy nhất chỉ khi đủ BA điều kiện — xem booth.decide_scan.\n   Không lộ điểm số thô, không lộ biên, không lộ thứ hạng nội bộ.\n4. HÀM ĐỒNG BỘ CÓ TRẦN ĐỒNG THỜI (sửa 04/08/2026): embedding + khớp điểm là việc CPU đồng\n   bộ; để trong `async def` là chặn cả event loop — đo thật 4 lượt song song 1,5s CPU chạy\n   tuần tự hết 6,05s, kéo theo /q/{code}, bảng điều khiển và /api/health xếp hàng. Để `def`\n   thì FastAPI tự đẩy sang threadpool; SCAN_SEM chặn trần để threadpool không bị nhét đầy.\n\nMỗi lượt ghi một dòng booth_scans.jsonl (mã lượt, thời gian, kết quả, độ trễ, ảnh truy vấn\nđã lưu). Lượt khách CHỌN ghi tiếp booth_choices.jsonl = mẫu CÓ NHÃN (xem /api/fruit/scan/choose).","operationId":"fruit_scan_api_fruit_scan_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_fruit_scan_api_fruit_scan_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fruit/scan/choose":{"post":{"summary":"Fruit Scan Choose","description":"KHÁCH CHỌN quả nào giống (hoặc \"không quả nào\") → hồ sơ quả + MỘT MẪU CÓ NHÃN.\n\nĐây là giá trị lớn nhất của cả buổi hội chợ. Dự án hiện có 0 nhãn cấp quả; một ngày\n100–300 lượt cho nhiều nhãn hơn toàn bộ lịch sử. Mỗi lượt chọn ghi một dòng\nbooth_choices.jsonl đủ để tính lại rank-1/rank-5 THẬT (compute_scan_accuracy.py).\n\n`pick` = số thứ tự thẻ trong ĐÚNG lượt quét đó (1..5), KHÔNG phải fruit_id: khách không\nbao giờ cầm định danh trong kho (xem _scan_candidate_out). Bỏ trống `pick` = \"không quả\nnào giống\" — đó cũng là một nhãn, và là nhãn quý.\n\nKHÔNG thu bất kỳ thông tin cá nhân nào của khách: không địa chỉ gọi, không trình duyệt,\nkhông định danh. Chỉ mã lượt quét + thẻ được chọn.","operationId":"fruit_scan_choose_api_fruit_scan_choose_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_fruit_scan_choose_api_fruit_scan_choose_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fruit/lookup/img/{token}":{"get":{"summary":"Fruit Lookup Img","description":"Ảnh gallery cho lane công khai — mã cấp theo LƯỢT TRA CỨU: không đoán được, hết hạn.\n\nBa lớp phải cùng đúng: mã còn hạn · quả còn trong kho · CÂY MẸ CÒN CÔNG KHAI. Lớp thứ ba là\ncần gạt thu hồi của nông dân: hạ cây về riêng tư là mọi ảnh đã phát tắt ngay, dù mã chưa hết\nhạn — đúng thứ mà URL /gimg (chìa vĩnh viễn) không làm được.","operationId":"fruit_lookup_img_api_fruit_lookup_img__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fruit/lookup":{"post":{"summary":"Fruit Lookup","description":"NGƯỜI LẠ chụp một quả → tối đa 5 ứng viên KÈM ẢNH trong tập quả CÔNG KHAI, để tự đối chiếu.\n\nHỢP ĐỒNG (đây là nguồn sự thật của hợp đồng — không có tệp .md nào khác mô tả route này):\n\nVào  (multipart/form-data, KHÔNG cần đăng nhập):\n  file                      — bắt buộc, ảnh JPEG/PNG. Trần riêng 2MB (OLT_SCAN_MAX_MB).\n  bbox_x/bbox_y/bbox_w/bbox_h — khung chữ nhật quanh quả (pixel ảnh gốc), tuỳ chọn.\n  points, shape             — khoanh tuỳ ý (polygon) thay cho bbox, tuỳ chọn.\n  sess                      — mã phiên trình duyệt do client tự sinh, dùng cho lớp hạn tần\n                              suất chặt nhất. Giả được ⟹ là lớp GIỮ TRẢI NGHIỆM, không phải\n                              lớp an ninh; kẻ xoay mã rơi xuống lớp địa chỉ gọi + trần đồng thời.\n  coarse_class/frames_gated/gate_available — báo cáo cổng-trên-máy, chỉ để ĐO.\n  (KHÔNG nhận lat/lon: vị trí NGƯỜI MUA không phải thứ hệ này cần, nên không thu.)\n\nRa 200 — ba dạng:\n  (a) {\"ok\":true,\"need_region\":true,\"regions\":[{\"index\",\"bbox\"}...],\"message\"} — trong khung\n      có nhiều quả (hoặc không thấy quả nào), mời người dùng chỉ đúng quả rồi gửi lại.\n      `regions` LUÔN là danh sách, **không bao giờ null**: không thấy quả nào thì nó RỖNG\n      (`[]`) chứ không phải thiếu. `index` số nguyên (0,1,2...), `bbox` là `[x,y,w,h]` bốn số\n      theo hệ ảnh đã gửi. Client thấy `null` ở đây thì lỗi nằm ở lớp giải mã JSON bên client,\n      không ở cửa này — khoá bằng `test_fruit_lookup.py`.\n  (b) {\"ok\":true,\"lookup_id\",\"verdict\":\"CHOICES|SOLO|EMPTY_SCOPE\",\"verdict_label\",\"message\",\n       \"candidates\":[thẻ...],\"fruit\":thẻ|null,\"match\":{...},\"warnings\":[mã...],\n       \"warning_messages\":[câu tiếng Việt...]}\n      thẻ = {\"pick\",\"name\",\"status\",\"enrolled_at\",\"n_imgs\",\"img_urls\":[...],\n             # `pick` là SỐ NGUYÊN (thứ hạng, sinh từ enumerate) — KHÔNG phải chuỗi. Client\n             # nào đọc thành chuỗi sẽ bỏ sạch thẻ và cả lượt tra hoá \"không có gì\": đã xảy ra\n             # thật 19/08. Kiểu này khoá bằng test_fruit_lookup.py.\n             \"tree\":{\"name\",\"code\",\"gps\",\"created_at\",\"public_url\",\n                     \"provenance\":{\"anchored\",\"status\",\"network\",\"explorer_url\",\"label\",\"means\"}}}\n  `fruit` chỉ khác null khi verdict=\"SOLO\".\n  (c) {\"ok\":true,\"verdict\":\"NO_FRUIT_IN_PHOTO\",\"verdict_label\",\"message\",\"candidates\":[],\n       \"fruit\":null,\"warnings\":[],\"warning_messages\":[]}\n      Trong khung không có quả nào để đối chiếu, nên cửa này KHÔNG bày thẻ và KHÔNG nêu tên\n      quả nào. Ba điều client phải biết:\n        • `NO_FRUIT_IN_PHOTO` KHÁC `EMPTY_SCOPE`. Cái sau nói về KHO (\"chưa nhà vườn nào mở\n          công khai\") và việc người mua cần làm là hỏi người bán mã truy xuất; cái này nói về\n          ẢNH và việc cần làm là chụp lại. Gộp hai cái vào một màn hình là chỉ sai đường.\n        • KHÔNG có `lookup_id` và KHÔNG có `match`. Cố ý: chưa có lượt đối chiếu nào xảy ra\n          nên chưa có gì để tra ngược. Client đọc hai trường đó phải chịu được việc chúng\n          vắng mặt, đừng đệm một chuỗi rỗng vào.\n        • `message` đã là câu hoàn chỉnh cho người dùng, nói rõ việc phải làm tiếp — hiện\n          thẳng câu đó, đừng thay bằng câu chung của client.\n      Luật quyết định nằm ở `fruit_scene_gate` (bảng năm hàng ở docstring đầu tệp đó).\n\nRa lỗi:\n  400 {\"error_code\":\"image_unusable\"}  — không nhúng được ảnh (mờ/hỏng/không đọc được).\n  413                                  — ảnh vượt trần riêng của lane khách.\n  429 {\"error_code\":\"rate_limited\",\"retry_after\":N} — quá tần suất HOẶC vượt trần đồng thời.\n\nBỐN PHÒNG THỦ kế thừa nguyên từ đường hội chợ, đừng gỡ cái nào:\n  1. Trần đồng thời (LOOKUP_SEM, mặc định 2) — mỗi lượt là một lần DINOv2; không trần thì một\n     vòng curl nhét đầy threadpool, kéo mọi cửa khác xếp hàng. Vượt trần → 429 NGAY, không xếp\n     hàng: chờ mù trước mặt người mua còn tệ hơn. Giấy phép RIÊNG, không dùng chung với hội chợ.\n  2. Hạn tần suất ba lớp (SCAN_RL, ngăn \"lookup\") — xem PUBLIC_LOOKUP_RL_KEY.\n  3. Ẩn nội tạng — không điểm, không biên, không fruit_id, không owner (xem _lookup_candidate_out).\n  4. Ảnh qua mã hết hạn + thu hồi được, GPS làm thô (xem fruit_lookup_img, _lookup_tree_view).\n\nTHỨ TỰ CÓ CHỦ Ý: xin giấy phép TRƯỚC, đếm hạn tần suất SAU. Đếm trước thì lượt bị \"máy chủ\nđang bận\" đẩy về — lượt chưa hề chấm điểm gì — vẫn tiêu một suất trong ngân sách của người\ndùng, và tới lúc máy rảnh thì họ bị khoá 60 giây (bài học đã đo ở /api/fruit/scan).","operationId":"fruit_lookup_api_fruit_lookup_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_fruit_lookup_api_fruit_lookup_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/chup":{"get":{"summary":"Field Capture Page","description":"Trang THU ẢNH ngoài vườn (mở bằng điện thoại). Đường-dẫn ngắn để gõ tay tại chỗ.\n\nTrang tự gọi /api/trees; chưa đăng nhập thì nó tự chuyển sang màn đăng nhập, nên\nroute này KHÔNG cần auth (chỉ trả về HTML tĩnh, không có dữ-liệu vườn nào trong đó).\n\nHAI đường-dẫn cùng trỏ một trang: `/capture` là tên chuẩn (hệ hướng ra toàn cầu, đường\ndẫn thuộc lớp máy đọc nên phải tiếng Anh); `/chup` GIỮ NGUYÊN vì người thu ngoài vườn\nđang gõ tay đường đó và nó đã lên bản triển khai. Bí danh là đường THÊM, không phải\nđường ĐỔI — đổi một chiều lúc này là làm hỏng buổi thu đang chạy.","operationId":"field_capture_page_chup_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/capture":{"get":{"summary":"Field Capture Page","description":"Trang THU ẢNH ngoài vườn (mở bằng điện thoại). Đường-dẫn ngắn để gõ tay tại chỗ.\n\nTrang tự gọi /api/trees; chưa đăng nhập thì nó tự chuyển sang màn đăng nhập, nên\nroute này KHÔNG cần auth (chỉ trả về HTML tĩnh, không có dữ-liệu vườn nào trong đó).\n\nHAI đường-dẫn cùng trỏ một trang: `/capture` là tên chuẩn (hệ hướng ra toàn cầu, đường\ndẫn thuộc lớp máy đọc nên phải tiếng Anh); `/chup` GIỮ NGUYÊN vì người thu ngoài vườn\nđang gõ tay đường đó và nó đã lên bản triển khai. Bí danh là đường THÊM, không phải\nđường ĐỔI — đổi một chiều lúc này là làm hỏng buổi thu đang chạy.","operationId":"field_capture_page_capture_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/q/{code}":{"get":{"summary":"Booth Scan Page","description":"Trang quét cho KHÁCH (mở bằng điện thoại từ QR dán ở bàn). Mã đọc từ chính đường dẫn.","operationId":"booth_scan_page_q__code__get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/logs":{"get":{"summary":"Admin Logs","description":"Log gần nhất (mới nhất CUỐI danh sách) từ ring-buffer + đuôi file nếu cấu-hình.\n\nQuery: `level` = mức TỐI THIỂU (info|warning|error|critical, rỗng = tất cả);\n`q` = lọc chứa chuỗi (không phân hoa-thường, so trên msg + tên logger); `limit` = số dòng.\nQuyền xem theo _monitor_account: mặc-định admin-only; OLT_MONITOR_OPEN=1 cho khách xem\nbản CHE IP (response mang `open:true` để trang hiện chú-thích).\n\nLọc `level` từ WARNING trở lên đọc vành SÂU riêng (WARNING+ giữ 500 dòng cuối) — kẻ dò\nserver không thể bơm request rẻ (access-log INFO) để rửa ERROR khỏi tầm nhìn admin.","operationId":"admin_logs_api_admin_logs_get","parameters":[{"name":"level","in":"query","required":false,"schema":{"type":"string","default":"","title":"Level"}},{"name":"q","in":"query","required":false,"schema":{"type":"string","default":"","title":"Q"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":200,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/deploy_history":{"get":{"summary":"Admin Deploy History","description":"Những lần server bị đụng tới: chạy lại, dừng, chết, deploy mã mới, sửa bản khai.\n\nCùng cổng quyền với `/api/admin/logs` (mặc-định admin, OLT_MONITOR_OPEN=1 thì khách xem\nđược bản đã che).\n\nCột \"Người\" (`by`) phần lớn dòng sẽ TRỐNG, và đó là câu trả lời đúng chứ không phải thiếu\ndữ-liệu: chỉ cửa đưa mã lên mới ghi lại được ai ra lệnh (xem `_ACTOR_KINDS`), còn lần chạy\nlại thì không nguồn nào trên máy biết ai gõ lệnh. Cái tên hiếm khi xuất hiện đó là tài-khoản\nvận-hành, không phải danh-tính nông dân mà lớp che sinh ra để bảo vệ, nên nó đi ra cho khách.\n\nBản cho khách khác ở hai chỗ: đường dẫn máy chủ trong phần chi-tiết bị che như mọi chỗ\nkhác, và khối `running` KHÔNG mang khoá `account` (xem `_server_account`).","operationId":"admin_deploy_history_api_admin_deploy_history_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":10,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/server_status":{"get":{"summary":"Admin Server Status","description":"Sức khoẻ máy chủ: máy, tiến-trình, card đồ-hoạ, đĩa, kho dữ-liệu, lưu-lượng, mạng, hàng đợi.\n\nCùng cổng quyền với `/api/admin/logs`. Bản cho khách khác ở chỗ giấu đường dẫn tuyệt-đối\n(kho dữ-liệu, tên tệp thì vẫn hiện) và giấu tên tài-khoản chạy tiến-trình (`_server_account`)\n— mọi con số ĐO ĐƯỢC đều giữ nguyên, vì giấu số ở đây không bảo vệ được ai mà lại làm mục\nnày vô dụng đúng lúc cần.","operationId":"admin_server_status_api_admin_server_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/server/monitor":{"get":{"summary":"Server Monitor Page","description":"Trang GIÁM-SÁT LOG cho người vận hành (mở bằng trình duyệt, có form đăng nhập tại chỗ).\n\nRoute KHÔNG cần auth vì chỉ trả HTML tĩnh không kèm dữ-liệu nào (như /capture, /static/\nadmin.html) — dữ-liệu thật đi qua /api/admin/logs (require_admin). Đường-dẫn nằm ngoài\n/static để gõ tay được ngay trên điện thoại.","operationId":"server_monitor_page_server_monitor_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/.well-known/orilife.json":{"get":{"summary":"Wellknown Descriptor","description":"BẢN KHAI DỊCH-VỤ cho máy: đây là gì, gọi ở đâu, cửa nào không cần khoá, trần bao nhiêu.\n\nĐặt dưới `/.well-known/` vì đó là chỗ theo quy-ước một thứ tự-động sẽ dò tới trước, không\nphải chỗ đẹp nhất để người đọc. Nội dung là tập con của `/openapi.json` — bản đó tả TỪNG cửa,\nbản này tả CẢ dịch-vụ, và một con bot cần biết \"có phải nơi này không\" trước khi tải về một\ntệp đặc-tả dài.","operationId":"wellknown_descriptor__well_known_orilife_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/llms.txt":{"get":{"summary":"Llms Txt","description":"Bản đồ một trang cho tác tử ngôn-ngữ, theo quy-ước llms.txt.\n\nViết cho thứ đọc một lần rồi quyết-định gọi cửa nào, nên nó nói ĐIỀU KIỆN chứ không nói tính\nnăng: cửa nào không cần khoá, cửa nào chạm dữ-liệu người khác, và chỗ nào hệ sẽ trả lời\n\"không biết\" thay vì đoán.","operationId":"llms_txt_llms_txt_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/robots.txt":{"get":{"summary":"Robots Txt","description":"Cho thu thập trang công-khai, chặn trang vận-hành và cửa ảnh.\n\nTrước đây cửa này 404. 404 nghĩa là \"không có luật\", và không có luật thì trình thu thập tự\nđặt luật cho mình — nó sẽ thử cả `/server/monitor` lẫn `/gimg`. Chặn ở đây KHÔNG phải lớp bảo\nvệ (các cửa đó đã có auth riêng); nó chỉ để con bot ngoan không tiêu băng thông vào chỗ nó\nkhông lấy được gì.","operationId":"robots_txt_robots_txt_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/{code}":{"get":{"summary":"Resolve Root","description":"Người mua gõ `orilife.io/<mã>` hoặc quét QR đời sau — vào thẳng đúng thực thể.\n\nKhông phải hình dạng mã → 404 cũ (đường máy đọc, không phải người gõ nhầm mã).","operationId":"resolve_root__code__get","parameters":[{"name":"code","in":"path","required":true,"schema":{"type":"string","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AlertOut":{"properties":{"farm_id":{"type":"string","title":"Farm Id"},"zone_id":{"type":"string","title":"Zone Id"},"alert_type":{"type":"string","title":"Alert Type"},"delta_pct":{"type":"number","title":"Delta Pct"},"baseline":{"type":"integer","title":"Baseline"},"current":{"type":"integer","title":"Current"},"triggered_at":{"type":"string","title":"Triggered At"},"message_vn":{"type":"string","title":"Message Vn"}},"type":"object","required":["farm_id","zone_id","alert_type","delta_pct","baseline","current","triggered_at","message_vn"],"title":"AlertOut"},"Body_account_delete_api_account_delete_post":{"properties":{"confirm":{"type":"string","title":"Confirm"}},"type":"object","required":["confirm"],"title":"Body_account_delete_api_account_delete_post"},"Body_admin_showcase_api_admin_showcase__tree_id__post":{"properties":{"secret":{"type":"string","title":"Secret","default":""}},"type":"object","title":"Body_admin_showcase_api_admin_showcase__tree_id__post"},"Body_anchor_submit_api_anchor_submit_post":{"properties":{"tree_id":{"type":"string","title":"Tree Id"},"tx_hash":{"type":"string","title":"Tx Hash"},"explorer_url":{"type":"string","title":"Explorer Url"},"network":{"type":"string","title":"Network","default":""},"secret":{"type":"string","title":"Secret","default":""},"record_hash":{"type":"string","title":"Record Hash"}},"type":"object","required":["tree_id","tx_hash","explorer_url"],"title":"Body_anchor_submit_api_anchor_submit_post"},"Body_animal_detect_animal_detect_post":{"properties":{"farm_id":{"type":"string","title":"Farm Id"},"owner_did":{"type":"string","title":"Owner Did"},"image":{"type":"string","contentMediaType":"application/octet-stream","title":"Image"}},"type":"object","required":["farm_id"],"title":"Body_animal_detect_animal_detect_post"},"Body_animal_detect_api_animal_detect_post":{"properties":{"farm_id":{"type":"string","title":"Farm Id"},"owner_did":{"type":"string","title":"Owner Did"},"image":{"type":"string","contentMediaType":"application/octet-stream","title":"Image"}},"type":"object","required":["farm_id"],"title":"Body_animal_detect_api_animal_detect_post"},"Body_animal_enroll_animal_enroll_post":{"properties":{"species":{"type":"string","title":"Species"},"farm_id":{"type":"string","title":"Farm Id"},"owner_did":{"type":"string","title":"Owner Did"},"name":{"type":"string","title":"Name","default":""},"force":{"type":"string","title":"Force"},"client_event_id":{"type":"string","title":"Client Event Id"},"images":{"items":{"type":"string","contentMediaType":"application/octet-stream"},"type":"array","title":"Images"}},"type":"object","required":["farm_id","images"],"title":"Body_animal_enroll_animal_enroll_post"},"Body_animal_enroll_api_animal_enroll_post":{"properties":{"species":{"type":"string","title":"Species"},"farm_id":{"type":"string","title":"Farm Id"},"owner_did":{"type":"string","title":"Owner Did"},"name":{"type":"string","title":"Name","default":""},"force":{"type":"string","title":"Force"},"client_event_id":{"type":"string","title":"Client Event Id"},"images":{"items":{"type":"string","contentMediaType":"application/octet-stream"},"type":"array","title":"Images"}},"type":"object","required":["farm_id","images"],"title":"Body_animal_enroll_api_animal_enroll_post"},"Body_animal_identify_animal_identify_post":{"properties":{"species":{"type":"string","title":"Species"},"farm_id":{"type":"string","title":"Farm Id"},"owner_did":{"type":"string","title":"Owner Did"},"image":{"type":"string","contentMediaType":"application/octet-stream","title":"Image"}},"type":"object","required":["farm_id","image"],"title":"Body_animal_identify_animal_identify_post"},"Body_animal_identify_api_animal_identify_post":{"properties":{"species":{"type":"string","title":"Species"},"farm_id":{"type":"string","title":"Farm Id"},"owner_did":{"type":"string","title":"Owner Did"},"image":{"type":"string","contentMediaType":"application/octet-stream","title":"Image"}},"type":"object","required":["farm_id","image"],"title":"Body_animal_identify_api_animal_identify_post"},"Body_animal_identify_verdict_animal_identify_verdict_post":{"properties":{"query_id":{"type":"string","title":"Query Id"},"verdict":{"type":"string","title":"Verdict"},"correct_did":{"type":"string","title":"Correct Did"}},"type":"object","required":["query_id","verdict"],"title":"Body_animal_identify_verdict_animal_identify_verdict_post"},"Body_animal_identify_verdict_api_animal_identify_verdict_post":{"properties":{"query_id":{"type":"string","title":"Query Id"},"verdict":{"type":"string","title":"Verdict"},"correct_did":{"type":"string","title":"Correct Did"}},"type":"object","required":["query_id","verdict"],"title":"Body_animal_identify_verdict_api_animal_identify_verdict_post"},"Body_animal_rename_animal_rename_post":{"properties":{"animal_did":{"type":"string","title":"Animal Did"},"name":{"type":"string","title":"Name"}},"type":"object","required":["animal_did","name"],"title":"Body_animal_rename_animal_rename_post"},"Body_animal_rename_api_animal_rename_post":{"properties":{"animal_did":{"type":"string","title":"Animal Did"},"name":{"type":"string","title":"Name"}},"type":"object","required":["animal_did","name"],"title":"Body_animal_rename_api_animal_rename_post"},"Body_animal_scan_animal_scan_post":{"properties":{"farm_id":{"type":"string","title":"Farm Id"},"owner_did":{"type":"string","title":"Owner Did"},"species":{"type":"string","title":"Species"},"image":{"type":"string","contentMediaType":"application/octet-stream","title":"Image"}},"type":"object","required":["farm_id","image"],"title":"Body_animal_scan_animal_scan_post"},"Body_animal_scan_api_animal_scan_post":{"properties":{"farm_id":{"type":"string","title":"Farm Id"},"owner_did":{"type":"string","title":"Owner Did"},"species":{"type":"string","title":"Species"},"image":{"type":"string","contentMediaType":"application/octet-stream","title":"Image"}},"type":"object","required":["farm_id","image"],"title":"Body_animal_scan_api_animal_scan_post"},"Body_animal_verify_animal_verify_post":{"properties":{"animal_did":{"type":"string","title":"Animal Did"},"owner_did":{"type":"string","title":"Owner Did"},"image":{"type":"string","contentMediaType":"application/octet-stream","title":"Image"}},"type":"object","required":["animal_did","image"],"title":"Body_animal_verify_animal_verify_post"},"Body_animal_verify_api_animal_verify_post":{"properties":{"animal_did":{"type":"string","title":"Animal Did"},"owner_did":{"type":"string","title":"Owner Did"},"image":{"type":"string","contentMediaType":"application/octet-stream","title":"Image"}},"type":"object","required":["animal_did","image"],"title":"Body_animal_verify_api_animal_verify_post"},"Body_booth_add_fruits_api_booth__booth_id__fruits_add_post":{"properties":{"fruit_ids":{"type":"string","title":"Fruit Ids"}},"type":"object","required":["fruit_ids"],"title":"Body_booth_add_fruits_api_booth__booth_id__fruits_add_post"},"Body_booth_create_api_booth_create_post":{"properties":{"name":{"type":"string","title":"Name"},"client_event_id":{"type":"string","title":"Client Event Id"}},"type":"object","title":"Body_booth_create_api_booth_create_post"},"Body_booth_lock_api_booth__booth_id__lock_post":{"properties":{"locked":{"type":"string","title":"Locked","default":"1"}},"type":"object","title":"Body_booth_lock_api_booth__booth_id__lock_post"},"Body_booth_remove_fruits_api_booth__booth_id__fruits_remove_post":{"properties":{"fruit_ids":{"type":"string","title":"Fruit Ids"}},"type":"object","required":["fruit_ids"],"title":"Body_booth_remove_fruits_api_booth__booth_id__fruits_remove_post"},"Body_care_delete_api_care_delete_post":{"properties":{"care_event_id":{"type":"string","title":"Care Event Id"}},"type":"object","required":["care_event_id"],"title":"Body_care_delete_api_care_delete_post"},"Body_care_log_api_care_log_post":{"properties":{"target_type":{"type":"string","title":"Target Type"},"target_id":{"type":"string","title":"Target Id"},"product_id":{"type":"string","title":"Product Id"},"applied_at":{"type":"string","title":"Applied At"},"dose":{"type":"string","title":"Dose","default":""},"purpose":{"type":"string","title":"Purpose","default":""},"farm_id":{"type":"string","title":"Farm Id","default":""},"recognition_method":{"type":"string","title":"Recognition Method","default":"manual"},"client_event_id":{"type":"string","title":"Client Event Id"},"files":{"items":{"type":"string","contentMediaType":"application/octet-stream"},"type":"array","title":"Files"}},"type":"object","required":["target_type","target_id","product_id"],"title":"Body_care_log_api_care_log_post"},"Body_care_match_api_care_match_post":{"properties":{"text":{"type":"string","title":"Text"},"scope":{"type":"string","title":"Scope"},"files":{"items":{"type":"string","contentMediaType":"application/octet-stream"},"type":"array","title":"Files"}},"type":"object","title":"Body_care_match_api_care_match_post"},"Body_delete_api_delete_post":{"properties":{"tree_id":{"type":"string","title":"Tree Id"}},"type":"object","required":["tree_id"],"title":"Body_delete_api_delete_post"},"Body_did_verify_api_auth_did_verify_post":{"properties":{"did":{"type":"string","title":"Did"},"challenge":{"type":"string","title":"Challenge"},"signature":{"type":"string","title":"Signature"},"pubkey_hex":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pubkey Hex"}},"type":"object","required":["did","challenge","signature"],"title":"Body_did_verify_api_auth_did_verify_post"},"Body_enroll_api_enroll_post":{"properties":{"name":{"type":"string","title":"Name"},"lat":{"type":"string","title":"Lat"},"lon":{"type":"string","title":"Lon"},"acc":{"type":"string","title":"Acc"},"heading":{"type":"string","title":"Heading"},"pitch":{"type":"string","title":"Pitch"},"heading_ref":{"type":"string","title":"Heading Ref"},"roll":{"type":"string","title":"Roll"},"tree_id":{"type":"string","title":"Tree Id"},"force":{"type":"string","title":"Force"},"dup":{"type":"string","title":"Dup"},"source":{"type":"string","title":"Source","default":"phone"},"prev_tree":{"type":"string","title":"Prev Tree"},"species":{"type":"string","title":"Species"},"farm_id":{"type":"string","title":"Farm Id"},"visibility":{"type":"string","title":"Visibility"},"envelope":{"type":"string","title":"Envelope"},"signature":{"type":"string","title":"Signature"},"points":{"type":"string","title":"Points"},"shape":{"type":"string","title":"Shape","default":"rect"},"bbox_x":{"type":"string","title":"Bbox X"},"bbox_y":{"type":"string","title":"Bbox Y"},"bbox_w":{"type":"string","title":"Bbox W"},"bbox_h":{"type":"string","title":"Bbox H"},"regions":{"type":"string","title":"Regions"},"masks":{"type":"string","title":"Masks"},"view_poses":{"type":"string","title":"View Poses"},"img_w":{"type":"string","title":"Img W"},"img_h":{"type":"string","title":"Img H"},"coarse_class":{"type":"string","title":"Coarse Class"},"frames_gated":{"type":"string","title":"Frames Gated"},"gate_available":{"type":"string","title":"Gate Available"},"client_event_id":{"type":"string","title":"Client Event Id"},"files":{"items":{"type":"string","contentMediaType":"application/octet-stream"},"type":"array","title":"Files"}},"type":"object","required":["name","files"],"title":"Body_enroll_api_enroll_post"},"Body_entity_did_submit_api__entity_type___entity_id__did_submit_post":{"properties":{"owner_signature":{"type":"string","title":"Owner Signature"}},"type":"object","required":["owner_signature"],"title":"Body_entity_did_submit_api__entity_type___entity_id__did_submit_post"},"Body_farm_create_api_farm_post":{"properties":{"name":{"type":"string","title":"Name"},"kind":{"type":"string","title":"Kind"},"boundary_json":{"type":"string","title":"Boundary Json"},"center_json":{"type":"string","title":"Center Json"},"boundary_method":{"type":"string","title":"Boundary Method"},"boundary_acc_m":{"type":"string","title":"Boundary Acc M"},"note":{"type":"string","title":"Note"},"client_event_id":{"type":"string","title":"Client Event Id"}},"type":"object","required":["name"],"title":"Body_farm_create_api_farm_post"},"Body_farm_photos_api_farm__farm_id__photos_post":{"properties":{"lat":{"type":"string","title":"Lat"},"lon":{"type":"string","title":"Lon"},"source":{"type":"string","title":"Source","default":"phone"},"note":{"type":"string","title":"Note"},"files":{"items":{"type":"string","contentMediaType":"application/octet-stream"},"type":"array","title":"Files"}},"type":"object","required":["files"],"title":"Body_farm_photos_api_farm__farm_id__photos_post"},"Body_farm_update_api_farm__farm_id__update_post":{"properties":{"name":{"type":"string","title":"Name"},"kind":{"type":"string","title":"Kind"},"boundary_json":{"type":"string","title":"Boundary Json"},"center_json":{"type":"string","title":"Center Json"},"boundary_method":{"type":"string","title":"Boundary Method"},"boundary_acc_m":{"type":"string","title":"Boundary Acc M"},"note":{"type":"string","title":"Note"}},"type":"object","title":"Body_farm_update_api_farm__farm_id__update_post"},"Body_farm_video_api_farm__farm_id__video_post":{"properties":{"lat":{"type":"string","title":"Lat"},"lon":{"type":"string","title":"Lon"},"source":{"type":"string","title":"Source","default":"phone"},"note":{"type":"string","title":"Note"},"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_farm_video_api_farm__farm_id__video_post"},"Body_feedback_api_feedback_post":{"properties":{"note":{"type":"string","title":"Note"},"context":{"type":"string","title":"Context","default":""}},"type":"object","required":["note"],"title":"Body_feedback_api_feedback_post"},"Body_fruit_add_view_api_fruit_add_view_post":{"properties":{"fruit_id":{"type":"string","title":"Fruit Id"},"bbox_x":{"type":"string","title":"Bbox X"},"bbox_y":{"type":"string","title":"Bbox Y"},"bbox_w":{"type":"string","title":"Bbox W"},"bbox_h":{"type":"string","title":"Bbox H"},"shape":{"type":"string","title":"Shape","default":"rect"},"points":{"type":"string","title":"Points"},"allow_mismatch":{"type":"string","title":"Allow Mismatch"},"zone":{"type":"string","title":"Zone"},"pos_x":{"type":"string","title":"Pos X"},"pos_h":{"type":"string","title":"Pos H"},"pos_z":{"type":"string","title":"Pos Z"},"view_type":{"type":"string","title":"View Type"},"coarse_class":{"type":"string","title":"Coarse Class"},"frames_gated":{"type":"string","title":"Frames Gated"},"gate_available":{"type":"string","title":"Gate Available"},"capture":{"type":"string","title":"Capture"},"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["fruit_id","file"],"title":"Body_fruit_add_view_api_fruit_add_view_post"},"Body_fruit_candidates_api_fruit_candidates_post":{"properties":{"tree_id":{"type":"string","title":"Tree Id"},"bbox_x":{"type":"string","title":"Bbox X"},"bbox_y":{"type":"string","title":"Bbox Y"},"bbox_w":{"type":"string","title":"Bbox W"},"bbox_h":{"type":"string","title":"Bbox H"},"shape":{"type":"string","title":"Shape","default":"rect"},"points":{"type":"string","title":"Points"},"coarse_class":{"type":"string","title":"Coarse Class"},"frames_gated":{"type":"string","title":"Frames Gated"},"gate_available":{"type":"string","title":"Gate Available"},"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["tree_id","file"],"title":"Body_fruit_candidates_api_fruit_candidates_post"},"Body_fruit_detect_api_fruit_detect_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"tree_id":{"type":"string","title":"Tree Id"}},"type":"object","required":["file"],"title":"Body_fruit_detect_api_fruit_detect_post"},"Body_fruit_enroll_api_fruit_enroll_post":{"properties":{"tree_id":{"type":"string","title":"Tree Id"},"name":{"type":"string","title":"Name"},"lat":{"type":"string","title":"Lat"},"lon":{"type":"string","title":"Lon"},"bbox_x":{"type":"string","title":"Bbox X"},"bbox_y":{"type":"string","title":"Bbox Y"},"bbox_w":{"type":"string","title":"Bbox W"},"bbox_h":{"type":"string","title":"Bbox H"},"shape":{"type":"string","title":"Shape","default":"rect"},"points":{"type":"string","title":"Points"},"allow_dup":{"type":"string","title":"Allow Dup"},"zone":{"type":"string","title":"Zone"},"pos_x":{"type":"string","title":"Pos X"},"pos_h":{"type":"string","title":"Pos H"},"pos_z":{"type":"string","title":"Pos Z"},"view_type":{"type":"string","title":"View Type"},"coarse_class":{"type":"string","title":"Coarse Class"},"frames_gated":{"type":"string","title":"Frames Gated"},"gate_available":{"type":"string","title":"Gate Available"},"capture":{"type":"string","title":"Capture"},"mask":{"type":"string","title":"Mask"},"client_event_id":{"type":"string","title":"Client Event Id"},"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["tree_id","file"],"title":"Body_fruit_enroll_api_fruit_enroll_post"},"Body_fruit_identify_api_fruit_identify_post":{"properties":{"tree_id":{"type":"string","title":"Tree Id"},"lat":{"type":"string","title":"Lat"},"lon":{"type":"string","title":"Lon"},"bbox_x":{"type":"string","title":"Bbox X"},"bbox_y":{"type":"string","title":"Bbox Y"},"bbox_w":{"type":"string","title":"Bbox W"},"bbox_h":{"type":"string","title":"Bbox H"},"shape":{"type":"string","title":"Shape","default":"rect"},"points":{"type":"string","title":"Points"},"coarse_class":{"type":"string","title":"Coarse Class"},"frames_gated":{"type":"string","title":"Frames Gated"},"gate_available":{"type":"string","title":"Gate Available"},"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_fruit_identify_api_fruit_identify_post"},"Body_fruit_identify_verdict_api_fruit_identify_verdict_post":{"properties":{"query_id":{"type":"string","title":"Query Id"},"verdict":{"type":"string","title":"Verdict"},"correct_fruit_id":{"type":"string","title":"Correct Fruit Id"}},"type":"object","required":["query_id","verdict"],"title":"Body_fruit_identify_verdict_api_fruit_identify_verdict_post"},"Body_fruit_lookup_api_fruit_lookup_post":{"properties":{"bbox_x":{"type":"string","title":"Bbox X"},"bbox_y":{"type":"string","title":"Bbox Y"},"bbox_w":{"type":"string","title":"Bbox W"},"bbox_h":{"type":"string","title":"Bbox H"},"shape":{"type":"string","title":"Shape","default":"rect"},"points":{"type":"string","title":"Points"},"sess":{"type":"string","title":"Sess"},"coarse_class":{"type":"string","title":"Coarse Class"},"frames_gated":{"type":"string","title":"Frames Gated"},"gate_available":{"type":"string","title":"Gate Available"},"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_fruit_lookup_api_fruit_lookup_post"},"Body_fruit_scan_api_fruit_scan_post":{"properties":{"code":{"type":"string","title":"Code"},"bbox_x":{"type":"string","title":"Bbox X"},"bbox_y":{"type":"string","title":"Bbox Y"},"bbox_w":{"type":"string","title":"Bbox W"},"bbox_h":{"type":"string","title":"Bbox H"},"shape":{"type":"string","title":"Shape","default":"rect"},"points":{"type":"string","title":"Points"},"sess":{"type":"string","title":"Sess"},"coarse_class":{"type":"string","title":"Coarse Class"},"frames_gated":{"type":"string","title":"Frames Gated"},"gate_available":{"type":"string","title":"Gate Available"},"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["code","file"],"title":"Body_fruit_scan_api_fruit_scan_post"},"Body_fruit_scan_choose_api_fruit_scan_choose_post":{"properties":{"code":{"type":"string","title":"Code"},"scan_id":{"type":"string","title":"Scan Id"},"pick":{"type":"string","title":"Pick"},"sess":{"type":"string","title":"Sess"}},"type":"object","required":["code","scan_id"],"title":"Body_fruit_scan_choose_api_fruit_scan_choose_post"},"Body_fruit_update_status_api_fruit__fruit_id__status_patch":{"properties":{"status":{"type":"string","title":"Status"}},"type":"object","required":["status"],"title":"Body_fruit_update_status_api_fruit__fruit_id__status_patch"},"Body_grant_create_api_grant_post":{"properties":{"grantee":{"type":"string","title":"Grantee"},"scope_type":{"type":"string","title":"Scope Type"},"scope_id":{"type":"string","title":"Scope Id"},"perms":{"type":"string","title":"Perms"},"ttl_days":{"type":"string","title":"Ttl Days"},"client_event_id":{"type":"string","title":"Client Event Id"}},"type":"object","required":["grantee","scope_type","scope_id","perms"],"title":"Body_grant_create_api_grant_post"},"Body_identify_api_identify_post":{"properties":{"lat":{"type":"string","title":"Lat"},"lon":{"type":"string","title":"Lon"},"acc":{"type":"string","title":"Acc"},"heading":{"type":"string","title":"Heading"},"pitch":{"type":"string","title":"Pitch"},"roll":{"type":"string","title":"Roll"},"heading_ref":{"type":"string","title":"Heading Ref"},"source":{"type":"string","title":"Source","default":"phone"},"last_tree":{"type":"string","title":"Last Tree"},"platform":{"type":"string","title":"Platform"},"points":{"type":"string","title":"Points"},"shape":{"type":"string","title":"Shape","default":"rect"},"bbox_x":{"type":"string","title":"Bbox X"},"bbox_y":{"type":"string","title":"Bbox Y"},"bbox_w":{"type":"string","title":"Bbox W"},"bbox_h":{"type":"string","title":"Bbox H"},"regions":{"type":"string","title":"Regions"},"masks":{"type":"string","title":"Masks"},"img_w":{"type":"string","title":"Img W"},"img_h":{"type":"string","title":"Img H"},"view_poses":{"type":"string","title":"View Poses"},"coarse_class":{"type":"string","title":"Coarse Class"},"frames_gated":{"type":"string","title":"Frames Gated"},"gate_available":{"type":"string","title":"Gate Available"},"files":{"items":{"type":"string","contentMediaType":"application/octet-stream"},"type":"array","title":"Files"}},"type":"object","required":["files"],"title":"Body_identify_api_identify_post"},"Body_identify_auto_api_identify_auto_post":{"properties":{"lat":{"type":"string","title":"Lat"},"lon":{"type":"string","title":"Lon"},"species":{"type":"string","title":"Species"},"farm_id":{"type":"string","title":"Farm Id"},"files":{"items":{"type":"string","contentMediaType":"application/octet-stream"},"type":"array","title":"Files"}},"type":"object","required":["files"],"title":"Body_identify_auto_api_identify_auto_post"},"Body_identify_kind_api_kind_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_identify_kind_api_kind_post"},"Body_identify_verdict_api_identify_verdict_post":{"properties":{"query_id":{"type":"string","title":"Query Id"},"verdict":{"type":"string","title":"Verdict"},"correct_tid":{"type":"string","title":"Correct Tid"}},"type":"object","required":["query_id","verdict"],"title":"Body_identify_verdict_api_identify_verdict_post"},"Body_identify_video_api_identify_video_post":{"properties":{"lat":{"type":"string","title":"Lat"},"lon":{"type":"string","title":"Lon"},"acc":{"type":"string","title":"Acc"},"heading":{"type":"string","title":"Heading"},"pitch":{"type":"string","title":"Pitch"},"roll":{"type":"string","title":"Roll"},"heading_ref":{"type":"string","title":"Heading Ref"},"source":{"type":"string","title":"Source","default":"phone"},"last_tree":{"type":"string","title":"Last Tree"},"platform":{"type":"string","title":"Platform"},"masks":{"type":"string","title":"Masks"},"coarse_class":{"type":"string","title":"Coarse Class"},"frames_gated":{"type":"string","title":"Frames Gated"},"gate_available":{"type":"string","title":"Gate Available"},"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_identify_video_api_identify_video_post"},"Body_login_api_login_post":{"properties":{"username":{"type":"string","title":"Username"},"password":{"type":"string","title":"Password"}},"type":"object","required":["username","password"],"title":"Body_login_api_login_post"},"Body_remove_views_api_remove_views_post":{"properties":{"tree_id":{"type":"string","title":"Tree Id"},"indices":{"type":"string","title":"Indices"}},"type":"object","required":["tree_id","indices"],"title":"Body_remove_views_api_remove_views_post"},"Body_rename_api_rename_post":{"properties":{"tree_id":{"type":"string","title":"Tree Id"},"name":{"type":"string","title":"Name"}},"type":"object","required":["tree_id","name"],"title":"Body_rename_api_rename_post"},"Body_scan_api_scan_post":{"properties":{"files":{"items":{"type":"string","contentMediaType":"application/octet-stream"},"type":"array","title":"Files"}},"type":"object","required":["files"],"title":"Body_scan_api_scan_post"},"Body_scan_confirm_api_scan_confirm_post":{"properties":{"scan_token":{"type":"string","title":"Scan Token"},"species":{"type":"string","title":"Species"}},"type":"object","required":["scan_token","species"],"title":"Body_scan_confirm_api_scan_confirm_post"},"Body_set_farm_api_tree_set_farm_post":{"properties":{"tree_id":{"type":"string","title":"Tree Id"},"farm_id":{"type":"string","title":"Farm Id"}},"type":"object","required":["tree_id"],"title":"Body_set_farm_api_tree_set_farm_post"},"Body_set_species_api_tree_set_species_post":{"properties":{"tree_id":{"type":"string","title":"Tree Id"},"species":{"type":"string","title":"Species"}},"type":"object","required":["tree_id"],"title":"Body_set_species_api_tree_set_species_post"},"Body_set_tree_position_api_tree_set_position_post":{"properties":{"tree_id":{"type":"string","title":"Tree Id"},"x":{"type":"string","title":"X"},"z":{"type":"string","title":"Z"},"clear":{"type":"string","title":"Clear"}},"type":"object","required":["tree_id"],"title":"Body_set_tree_position_api_tree_set_position_post"},"Body_set_visibility_api_tree_set_visibility_post":{"properties":{"tree_id":{"type":"string","title":"Tree Id"},"visibility":{"type":"string","title":"Visibility"},"expose_location":{"type":"string","title":"Expose Location"},"public_card":{"type":"string","title":"Public Card"}},"type":"object","required":["tree_id","visibility"],"title":"Body_set_visibility_api_tree_set_visibility_post"},"Body_signup_api_signup_post":{"properties":{"username":{"type":"string","title":"Username"},"password":{"type":"string","title":"Password"}},"type":"object","required":["username","password"],"title":"Body_signup_api_signup_post"},"Body_tree_fruit_video_api_tree__tree_id__fruit_video_post":{"properties":{"lat":{"type":"string","title":"Lat"},"lon":{"type":"string","title":"Lon"},"source":{"type":"string","title":"Source","default":"phone"},"note":{"type":"string","title":"Note"},"fruit_id":{"type":"string","title":"Fruit Id"},"coarse_class":{"type":"string","title":"Coarse Class"},"frames_gated":{"type":"string","title":"Frames Gated"},"gate_available":{"type":"string","title":"Gate Available"},"capture":{"type":"string","title":"Capture"},"client_event_id":{"type":"string","title":"Client Event Id"},"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_tree_fruit_video_api_tree__tree_id__fruit_video_post"},"Body_tree_marker_api_tree_marker_post":{"properties":{"tree_id":{"type":"string","title":"Tree Id"},"label":{"type":"string","title":"Label"},"side":{"type":"string","title":"Side"}},"type":"object","required":["tree_id","label","side"],"title":"Body_tree_marker_api_tree_marker_post"},"Body_tree_video_api_tree__tree_id__video_post":{"properties":{"lat":{"type":"string","title":"Lat"},"lon":{"type":"string","title":"Lon"},"source":{"type":"string","title":"Source","default":"phone"},"note":{"type":"string","title":"Note"},"coarse_class":{"type":"string","title":"Coarse Class"},"frames_gated":{"type":"string","title":"Frames Gated"},"gate_available":{"type":"string","title":"Gate Available"},"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_tree_video_api_tree__tree_id__video_post"},"Body_update_location_api_update_location_post":{"properties":{"tree_id":{"type":"string","title":"Tree Id"},"lat":{"type":"string","title":"Lat"},"lon":{"type":"string","title":"Lon"}},"type":"object","required":["tree_id","lat","lon"],"title":"Body_update_location_api_update_location_post"},"Body_verify_add_api_verify_add_post":{"properties":{"tree_id":{"type":"string","title":"Tree Id"},"lat":{"type":"string","title":"Lat"},"lon":{"type":"string","title":"Lon"},"acc":{"type":"string","title":"Acc"},"heading":{"type":"string","title":"Heading"},"pitch":{"type":"string","title":"Pitch"},"heading_ref":{"type":"string","title":"Heading Ref"},"roll":{"type":"string","title":"Roll"},"source":{"type":"string","title":"Source","default":"phone"},"prev_tree":{"type":"string","title":"Prev Tree"},"farm_id":{"type":"string","title":"Farm Id"},"envelope":{"type":"string","title":"Envelope"},"signature":{"type":"string","title":"Signature"},"masks":{"type":"string","title":"Masks"},"points":{"type":"string","title":"Points"},"shape":{"type":"string","title":"Shape","default":"rect"},"bbox_x":{"type":"string","title":"Bbox X"},"bbox_y":{"type":"string","title":"Bbox Y"},"bbox_w":{"type":"string","title":"Bbox W"},"bbox_h":{"type":"string","title":"Bbox H"},"regions":{"type":"string","title":"Regions"},"img_w":{"type":"string","title":"Img W"},"img_h":{"type":"string","title":"Img H"},"view_poses":{"type":"string","title":"View Poses"},"coarse_class":{"type":"string","title":"Coarse Class"},"frames_gated":{"type":"string","title":"Frames Gated"},"gate_available":{"type":"string","title":"Gate Available"},"files":{"items":{"type":"string","contentMediaType":"application/octet-stream"},"type":"array","title":"Files"}},"type":"object","required":["tree_id","files"],"title":"Body_verify_add_api_verify_add_post"},"CountRequest":{"properties":{"farm_id":{"type":"string","title":"Farm Id"},"zone_id":{"type":"string","title":"Zone Id"},"zone_name":{"type":"string","title":"Zone Name"},"count":{"type":"integer","minimum":0.0,"title":"Count"},"method":{"type":"string","title":"Method","default":"manual"},"device_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Id"}},"type":"object","required":["farm_id","zone_id","zone_name","count"],"title":"CountRequest"},"CountResponse":{"properties":{"ok":{"type":"boolean","title":"Ok"},"farm_id":{"type":"string","title":"Farm Id"},"zone_id":{"type":"string","title":"Zone Id"},"count":{"type":"integer","title":"Count"},"counted_at":{"type":"string","title":"Counted At"},"alerts":{"items":{"$ref":"#/components/schemas/AlertOut"},"type":"array","title":"Alerts"}},"type":"object","required":["ok","farm_id","zone_id","count","counted_at","alerts"],"title":"CountResponse"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"InterpretByStampsIn":{"properties":{"market":{"type":"string","title":"Market","description":"CN | EU | Codex"},"stamp_ids":{"items":{"type":"string"},"type":"array","title":"Stamp Ids"},"phi_gate_open":{"type":"boolean","title":"Phi Gate Open","default":false},"cd_soil":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cd Soil"},"soil_ph":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Soil Ph"}},"type":"object","required":["market"],"title":"InterpretByStampsIn","description":"Diễn-giải 1 lô theo stamp_id VeData (INV-RES-13).\n\nApp gửi TẬP stamp_id của lô; server RESOLVE qua Query (adapter injectable) rồi decode →\nverdict. Ưu điểm vs nhận measurements trực-tiếp: giá-trị đo KHÔNG do caller tự dựng (giảm\nrủi-ro caller giấu FAIL / chọn tập-con thuận lợi). Resolve thiếu/DENY/timeout → fail-closed\nUNKNOWN (INV-RES-12), KHÔNG âm-thầm bỏ."},"InterpretIn":{"properties":{"market":{"type":"string","title":"Market","description":"CN | EU | Codex"},"measurements":{"items":{"$ref":"#/components/schemas/MeasurementIn"},"type":"array","title":"Measurements"},"phi_gate_open":{"type":"boolean","title":"Phi Gate Open","default":false},"cd_soil":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cd Soil"},"soil_ph":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Soil Ph"}},"type":"object","required":["market"],"title":"InterpretIn","description":"Yêu cầu diễn-giải 1 lô: tập phép đo + thị-trường (+ rủi-ro-vùng tuỳ chọn)."},"MeasurementIn":{"properties":{"analyte":{"type":"string","title":"Analyte"},"grade":{"type":"string","title":"Grade","default":"screening"},"matrix":{"type":"string","title":"Matrix","default":"flesh"},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value"},"unit":{"type":"string","title":"Unit","default":"mg/kg"},"lod":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lod"},"loq":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Loq"},"uncertainty":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Uncertainty"},"measured_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Measured At"},"qualitative_positive":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Qualitative Positive"},"stamp_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stamp Id"}},"type":"object","required":["analyte"],"title":"MeasurementIn","description":"1 phép đo OriLife đọc từ VeData (CONTRACT §3.2). App đã resolve qua Query rồi truyền vào.\n\ngrade KHÔNG tin tự-khai (INV-RES-15): chỉ \"accredited\" khi đúng nguồn đã-verify; mặc định\nscreening. Ở đây nhận grade từ app (app đã decode theo query_adapter); engine vẫn fail-safe."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}}}}