From 3ba2120c8eeb2f32a5e07fb7a5c5a980797d2f89 Mon Sep 17 00:00:00 2001 From: Momentry Studio Date: Sun, 14 Jun 2026 11:05:23 +0800 Subject: [PATCH] All API through Rust proxy, fix unregistered files null uuid, People enhancements --- AGENTS.md | 36 ++++++++++++ src-tauri/Cargo.toml | 1 + src-tauri/src/main.rs | 102 +++++++++++++++++++++++++++++++-- src/App.vue | 6 +- src/assets/wordpress-exact.css | 3 +- src/components/VideoPlayer.vue | 27 +++++---- src/views/LibraryView.vue | 34 ++++++----- src/views/PeopleView.vue | 101 +++++++++++++++++++++++++++----- 8 files changed, 265 insertions(+), 45 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..cafd817 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,36 @@ +# AGENTS.md + +## Stack +- **Frontend**: Vue 3 + TypeScript + Vite (port 5173) +- **Desktop**: Tauri 2 (Rust backend in `src-tauri/`) +- **Runtime**: Node.js + npm + Rust/Cargo all required + +## Commands +``` +npm run dev # Vite dev server (port 5173) +npm run build # vue-tsc --noEmit && vite build (typecheck + bundle) +npm run preview # Preview production build +npm run tauri # Alias for `cargo tauri` +cargo tauri dev # Full Tauri desktop dev (builds frontend + opens native window) +cargo tauri build # Full native app build (outputs to src-tauri/target/release/bundle/) +``` + +## Architecture +- **All Core API calls go through Rust Tauri commands** — frontend NEVER contacts `localhost:3002` directly. +- **Rust proxies to external API**: `CORE_API = http://localhost:3002` (hardcoded in `src-tauri/src/main.rs`). This service must be running for the app to function. +- **API key**: Hardcoded in `src-tauri/src/main.rs` only (`API_KEY` constant). +- **Rust entrypoint**: `src-tauri/src/main.rs` — 10 Tauri commands: + - `search_llm_smart`, `get_files`, `get_people`, `get_faces`, `get_traces` (data APIs) + - `get_thumbnail`, `get_identity_profile` (return base64 image strings) + - `get_video_stream` (returns base64 video string) + - `update_identity_name`, `delete_identity` (identity management) +- **Frontend routes**: `/search`, `/library`, `/people` (redirect `/` → `/search`) +- **Frontend entry**: `src/main.ts` → `src/App.vue` → `src/router/index.ts` + +## Key Details +- **`withGlobalTauri: true`** in `tauri.conf.json` — `window.__TAURI__` is available in the webview. +- **No linter, formatter, or tests configured** — do not invent them unless asked. +- **TypeScript is non-strict** (`strict: false` in `tsconfig.json`). +- **CI/CD**: Shell scripts only (`ci-cd.sh`, `local-ci-cd.sh`, `setup-gitea-ci.sh`). No GitHub Actions. +- **Full native build order**: `npm install` → `npm run build` → `cargo tauri build` +- **Rust deps**: sqlx (postgres), reqwest, tokio, tauri-plugin-shell, base64 diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 25266d9..38926de 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -16,6 +16,7 @@ serde_json = "1" sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "json"] } reqwest = { version = "0.11", features = ["json"] } tokio = { version = "1", features = ["full"] } +base64 = "0.22" [build-dependencies] tauri-build = { version = "2", features = [] } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 7de7b1d..cd8671b 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,6 +1,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use serde::Serialize; +use base64::{Engine as _, engine::general_purpose::STANDARD}; #[derive(Serialize)] struct SearchResult { @@ -18,6 +19,7 @@ struct FileInfo { file_name: String, file_size: i64, modified_time: String, + #[serde(rename = "isRegistered")] is_registered: bool, } @@ -84,8 +86,15 @@ async fn search_llm_smart(query: String, limit: usize) -> Result Result, String> { +async fn get_files(args: GetFilesArgs) -> Result, String> { + let page_size = args.page_size; let client = reqwest::Client::new(); let url = format!("{}/api/v1/files/scan?api_key={}&page_size={}", CORE_API, API_KEY, page_size); @@ -97,12 +106,15 @@ async fn get_files(page_size: usize) -> Result, String> { .unwrap_or(&vec![]) .iter() .filter_map(|f| { + let is_reg = f["is_registered"].as_bool().unwrap_or(false); + let file_uuid = f["file_uuid"].as_str() + .unwrap_or_else(|| f["file_path"].as_str().unwrap_or("")).to_string(); Some(FileInfo { - file_uuid: f["file_uuid"].as_str()?.to_string(), + file_uuid, file_name: f["file_name"].as_str()?.to_string(), file_size: f["file_size"].as_i64().unwrap_or(0), modified_time: f["modified_time"].as_str().unwrap_or("").to_string(), - is_registered: f["is_registered"].as_bool().unwrap_or(false), + is_registered: is_reg, }) }) .collect(); @@ -112,8 +124,14 @@ async fn get_files(page_size: usize) -> Result, String> { #[tauri::command] async fn get_people(page: usize, per_page: usize) -> Result, String> { + eprintln!("[get_people] page={} per_page={}", page, per_page); let client = reqwest::Client::new(); let url = format!("{}/api/v1/identities?api_key={}&page={}&per_page={}", CORE_API, API_KEY, page, per_page); + eprintln!("[get_people] url={}", url); + + let response = client.get(&url).send().await.map_err(|e| format!("Request failed: {}", e))?; + let json: serde_json::Value = response.json().await.map_err(|e| format!("Parse failed: {}", e))?; + eprintln!("[get_people] identities count: {}", json["identities"].as_array().map(|a| a.len()).unwrap_or(0)); let response = client.get(&url).send().await.map_err(|e| format!("Request failed: {}", e))?; let json: serde_json::Value = response.json().await.map_err(|e| format!("Parse failed: {}", e))?; @@ -130,6 +148,7 @@ async fn get_people(page: usize, per_page: usize) -> Result, Str }) .collect(); + eprintln!("[get_people] returning {} people", people.len()); Ok(people) } @@ -185,6 +204,76 @@ async fn get_traces(uuid: String, per_page: usize) -> Result, Str Ok(traces) } +#[tauri::command] +async fn get_thumbnail(uuid: String, frame: u32) -> Result { + let url = format!("{}/api/v1/file/{}/thumbnail?api_key={}&frame={}", CORE_API, uuid, API_KEY, frame); + let bytes = reqwest::get(&url).await + .map_err(|e| format!("Thumbnail request failed: {}", e))? + .bytes().await + .map_err(|e| format!("Thumbnail read failed: {}", e))?; + Ok(format!("data:image/jpeg;base64,{}", STANDARD.encode(&bytes))) +} + +#[tauri::command] +async fn get_video_stream(uuid: String, start_time: f64, end_time: f64) -> Result { + let url = format!("{}/api/v1/file/{}/video?api_key={}&start_time={}&end_time={}", CORE_API, uuid, API_KEY, start_time, end_time); + let bytes = reqwest::get(&url).await + .map_err(|e| format!("Video request failed: {}", e))? + .bytes().await + .map_err(|e| format!("Video read failed: {}", e))?; + Ok(format!("data:video/mp4;base64,{}", STANDARD.encode(&bytes))) +} + +const PROFILE_DIRS: &[&str] = &[ + "/Users/accusys/momentry/output/identities", + "/Users/accusys/momentry/output_dev/identities", +]; + +#[tauri::command] +async fn get_identity_profile(uuid: String) -> Result { + let no_dash = uuid.replace('-', ""); + for dir in PROFILE_DIRS { + let path = format!("{}/{}/profile.jpg", dir, no_dash); + if std::path::Path::new(&path).exists() { + let bytes = std::fs::read(&path).map_err(|e| format!("Read failed: {}", e))?; + return Ok(format!("data:image/jpeg;base64,{}", STANDARD.encode(&bytes))); + } + let path = format!("{}/{}/profile.jpg", dir, uuid); + if std::path::Path::new(&path).exists() { + let bytes = std::fs::read(&path).map_err(|e| format!("Read failed: {}", e))?; + return Ok(format!("data:image/jpeg;base64,{}", STANDARD.encode(&bytes))); + } + } + Err("Profile image not found".to_string()) +} + +#[tauri::command] +async fn update_identity_name(uuid: String, name: String) -> Result<(), String> { + let client = reqwest::Client::new(); + let url = format!("{}/api/v1/identity/{}?api_key={}", CORE_API, uuid, API_KEY); + let resp = client.patch(&url) + .json(&serde_json::json!({"name": name})) + .send().await + .map_err(|e| format!("Request failed: {}", e))?; + if !resp.status().is_success() { + return Err(format!("Update failed: {}", resp.status())); + } + Ok(()) +} + +#[tauri::command] +async fn delete_identity(uuid: String) -> Result<(), String> { + let client = reqwest::Client::new(); + let url = format!("{}/api/v1/identity/{}?api_key={}", CORE_API, uuid, API_KEY); + let resp = client.delete(&url) + .send().await + .map_err(|e| format!("Request failed: {}", e))?; + if !resp.status().is_success() && resp.status() != 204 { + return Err(format!("Delete failed: {}", resp.status())); + } + Ok(()) +} + fn main() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) @@ -193,7 +282,12 @@ fn main() { get_files, get_people, get_faces, - get_traces + get_traces, + get_thumbnail, + get_video_stream, + get_identity_profile, + update_identity_name, + delete_identity ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/App.vue b/src/App.vue index 9314944..f1f0af9 100644 --- a/src/App.vue +++ b/src/App.vue @@ -34,9 +34,11 @@ +