Skip to main content

qualia_client_core/api/
system.rs

1//! QPU, hardware, engine, daemon, tax, vault, ingest, solid pod, image
2
3#![allow(non_snake_case)]
4
5use super::*;
6
7use crate::engine::ingestion;
8use crate::engine::q42_compiler;
9use crate::state::*;
10use futures_util::StreamExt;
11use qualia_core_db::ilp_dispatcher::{DispatchResult, HttpIlpTransport, IlpDispatcher};
12use qualia_core_db::rpc::{route_tax_payment, TaxRecipientSuite};
13use serde::Serialize;
14use std::io::Write;
15use std::path::{Path, PathBuf};
16use std::sync::atomic::{AtomicBool, Ordering};
17use std::sync::Arc;
18use sysinfo::System;
19
20pub use crate::setup::{SetupProfile, SetupState};
21
22pub fn get_setup_state() -> Result<SetupState, String> {
23    crate::setup::get_setup_state()
24}
25
26pub fn complete_setup_step(step: String) -> Result<SetupState, String> {
27    crate::setup::complete_setup_step(step)
28}
29
30pub fn update_setup_profile(profile: SetupProfile) -> Result<SetupState, String> {
31    crate::setup::update_setup_profile(profile)
32}
33
34pub fn finish_setup() -> Result<SetupState, String> {
35    crate::setup::finish_setup()
36}
37
38// ── Person / apparatus identity plane (person ≠ machine ≠ OS account) ─────────
39
40pub fn get_identity_plane() -> Result<crate::identity_plane::IdentityPlaneSnapshot, String> {
41    crate::identity_plane::get_identity_plane()
42}
43
44pub fn list_apparatus_devices() -> Result<Vec<crate::identity_plane::DeviceRecordPublic>, String> {
45    crate::identity_plane::list_devices()
46}
47
48pub fn export_person_public() -> Result<crate::identity_plane::PersonPublic, String> {
49    crate::identity_plane::export_person_public()
50}
51
52/// Recovery-grade bundle to install the same person principal on another machine.
53pub fn export_person_transfer_bundle() -> Result<crate::identity_plane::PersonTransferBundle, String>
54{
55    crate::identity_plane::export_person_transfer_bundle()
56}
57
58pub fn import_person_transfer_bundle(
59    bundle: crate::identity_plane::PersonTransferBundle,
60) -> Result<crate::identity_plane::IdentityPlaneSnapshot, String> {
61    crate::identity_plane::import_person_transfer_bundle(bundle)
62}
63
64pub fn register_remote_apparatus_device(
65    device: crate::identity_plane::DeviceRecordPublic,
66) -> Result<crate::identity_plane::IdentityPlaneSnapshot, String> {
67    crate::identity_plane::register_remote_device(device)
68}
69
70pub fn resolve_job_device_placement(
71    target_device_id: Option<String>,
72) -> Result<crate::identity_plane::JobPlacement, String> {
73    crate::identity_plane::resolve_job_placement(target_device_id.as_deref())
74}
75
76pub fn set_local_control_base_url(
77    url: String,
78) -> Result<crate::identity_plane::IdentityPlaneSnapshot, String> {
79    crate::identity_plane::set_local_control_base_url(url)
80}
81
82pub fn list_remote_job_outbox() -> Result<Vec<crate::identity_plane::RemoteOutboxEntry>, String> {
83    crate::identity_plane::list_remote_outbox()
84}
85
86pub fn retry_remote_job_outbox() -> Result<usize, String> {
87    crate::identity_plane::fleet_jobs::retry_remote_outbox()
88}
89
90/// Mint a real WebID-TLS self-signed cert bound to the local person DID (SAN URI).
91pub fn mint_person_webid_tls_cert() -> Result<serde_json::Value, String> {
92    let person = crate::identity_plane::PersonPrincipal::load_or_create(None)?;
93    let (cert_pem, key_pem) = qualia_core_db::key_vault::generate_webid_tls_cert_for_seed(
94        &person.ed25519_secret,
95        &person.person_id,
96    )?;
97    // Persist under app meta for operators (key material is person-secret grade).
98    let dir = crate::state::app_meta_dir().join("certs");
99    std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
100    std::fs::write(dir.join("person-webid.crt.pem"), &cert_pem).map_err(|e| e.to_string())?;
101    std::fs::write(dir.join("person-webid.key.pem"), &key_pem).map_err(|e| e.to_string())?;
102    Ok(serde_json::json!({
103        "person_id": person.person_id,
104        "cert_pem": cert_pem,
105        "key_pem_path": dir.join("person-webid.key.pem").display().to_string(),
106        "cert_path": dir.join("person-webid.crt.pem").display().to_string(),
107        "note": "Self-signed WebID-TLS style cert; DID is in SAN URI. Treat key as recovery material."
108    }))
109}
110
111pub fn accept_fleet_job_envelope(
112    envelope: crate::identity_plane::FleetJobEnvelope,
113) -> Result<serde_json::Value, String> {
114    let job = crate::identity_plane::accept_fleet_job_envelope(envelope)?;
115    serde_json::to_value(job).map_err(|e| e.to_string())
116}
117
118pub use crate::qpu_oracle::{QpuChatCommandResult, QpuOracleSettings, QpuOracleSettingsInput};
119
120pub fn get_qpu_settings() -> QpuOracleSettings {
121    crate::qpu_oracle::get_qpu_settings()
122}
123
124pub fn is_qpu_feature_unlocked() -> bool {
125    crate::qpu_oracle::is_qpu_feature_unlocked()
126}
127
128pub fn save_qpu_settings(input: QpuOracleSettingsInput) -> Result<QpuOracleSettings, String> {
129    crate::qpu_oracle::save_qpu_settings(input)
130}
131
132pub fn handle_qpu_chat_command(text: String) -> QpuChatCommandResult {
133    crate::qpu_oracle::handle_qpu_chat_command(&text)
134}
135
136pub fn handle_engine_chat_command(text: String) -> QpuChatCommandResult {
137    crate::qpu_pipeline::handle_engine_chat_command(&text)
138}
139
140pub fn profile_energy_circumstance() -> String {
141    let mut sys = System::new_all();
142    sys.refresh_all();
143    let total_mem = sys.total_memory() / 1024 / 1024;
144    let used_mem = sys.used_memory() / 1024 / 1024;
145    format!(
146        "Energy: AC_POWER\nTotal RAM: {} MB\nUsed RAM: {} MB\nSwarm Auth: GRANTED",
147        total_mem, used_mem
148    )
149}
150
151pub fn check_ollama_status() -> bool {
152    std::process::Command::new("ollama")
153        .arg("-v")
154        .output()
155        .map(|o| o.status.success())
156        .unwrap_or(false)
157}
158
159#[derive(Serialize)]
160pub struct HardwareStatus {
161    pub ram_total_gb: f64,
162    pub ram_used_gb: f64,
163    pub vram_estimated_gb: f64,
164}
165
166pub fn get_hardware_status() -> HardwareStatus {
167    let mut sys = System::new_all();
168    sys.refresh_all();
169    let vram_available_gb = {
170        #[cfg(target_os = "windows")]
171        {
172            qualia_core_db::directml_bridge::probe_best_adapter_memory()
173                .map(|memory| {
174                    let free = memory.available_local_bytes();
175                    let fallback = memory.dedicated_vram_bytes;
176                    let bytes = if free > 0 { free } else { fallback };
177                    bytes as f64 / 1024.0 / 1024.0 / 1024.0
178                })
179                .unwrap_or(0.0)
180        }
181        #[cfg(not(target_os = "windows"))]
182        {
183            0.0
184        }
185    };
186    HardwareStatus {
187        ram_total_gb: sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0,
188        ram_used_gb: sys.used_memory() as f64 / 1024.0 / 1024.0 / 1024.0,
189        vram_estimated_gb: vram_available_gb,
190    }
191}
192
193/// Orchestrator + LLM arena signals for the Flutter Vault HUD.
194#[derive(Debug, Clone, Serialize)]
195pub struct EngineTelemetryFields {
196    pub thermal_state: String,
197    pub llm_memory_bytes: u64,
198    pub memory_floor_mb: u32,
199    pub model_lifecycle: String,
200    pub kv_cache_used_mb: u32,
201    pub vram_used_mb: u32,
202    pub vram_total_mb: u32,
203    pub npu_used_mb: u32,
204    pub npu_total_mb: u32,
205}
206
207pub fn get_engine_telemetry_fields() -> EngineTelemetryFields {
208    let (vram_used_mb, vram_total_mb) = probe_vram_usage_mb();
209    let (npu_used_mb, npu_total_mb) = probe_npu_usage_mb();
210    EngineTelemetryFields {
211        thermal_state: crate::model_lifecycle::get_thermal_state_label().to_string(),
212        llm_memory_bytes: crate::model_lifecycle::get_llm_memory_bytes(),
213        memory_floor_mb: crate::model_lifecycle::MEMORY_FLOOR_MB,
214        model_lifecycle: crate::model_lifecycle::lifecycle_label(
215            crate::model_lifecycle::get_model_lifecycle_state(),
216        )
217        .to_string(),
218        kv_cache_used_mb: crate::model_lifecycle::get_kv_cache_used_mb(),
219        vram_used_mb,
220        vram_total_mb,
221        npu_used_mb,
222        npu_total_mb,
223    }
224}
225
226fn probe_vram_usage_mb() -> (u32, u32) {
227    #[cfg(target_os = "windows")]
228    {
229        if let Ok(memory) = qualia_core_db::directml_bridge::probe_best_adapter_memory() {
230            let used = memory.local_usage_bytes / (1024 * 1024);
231            let total = memory.local_budget_bytes / (1024 * 1024);
232            return (used as u32, total as u32);
233        }
234    }
235    (0, 0)
236}
237
238fn probe_npu_usage_mb() -> (u32, u32) {
239    #[cfg(target_os = "windows")]
240    {
241        if let Ok(memory) = qualia_core_db::directml_bridge::probe_npu_adapter_memory() {
242            let used = memory.shared_usage_bytes / (1024 * 1024); // NPUs often use shared memory
243            let total = memory.shared_budget_bytes / (1024 * 1024);
244            return (used as u32, total as u32);
245        }
246    }
247    (0, 0)
248}
249
250pub async fn download_and_vectorize(
251    url: String,
252    filename: String,
253    item_id: String,
254) -> Result<String, String> {
255    let state = crate::state::APP_STATE.get().unwrap();
256    let storage_path = state.config.lock().unwrap().storage_path.clone();
257    let handles = state.download_handles.clone();
258    let active_dl = state.active_downloads.clone();
259
260    let index_dir = PathBuf::from(&storage_path).join("Index");
261    std::fs::create_dir_all(&index_dir).map_err(|e| e.to_string())?;
262    let dest_path = index_dir.join(&filename);
263
264    let cancelled = Arc::new(AtomicBool::new(false));
265    handles
266        .lock()
267        .unwrap()
268        .insert(item_id.clone(), cancelled.clone());
269
270    let response = reqwest::get(&url).await.map_err(|e| {
271        handles.lock().unwrap().remove(&item_id);
272        active_dl.lock().unwrap().remove(&item_id);
273        e.to_string()
274    })?;
275    let total_bytes = response.content_length().unwrap_or(0);
276    let mut dest = std::fs::File::create(&dest_path).map_err(|e| e.to_string())?;
277    let mut stream = response.bytes_stream();
278    let mut downloaded: u64 = 0;
279    let mut last_report = std::time::Instant::now();
280    let mut last_downloaded: u64 = 0;
281
282    while let Some(chunk) = stream.next().await {
283        if cancelled.load(Ordering::Relaxed) {
284            let _ = std::fs::remove_file(&dest_path);
285            let payload = ProgressPayload {
286                id: item_id.clone(),
287                progress: 0.0,
288                downloaded_bytes: downloaded,
289                total_bytes,
290                speed_kbps: 0.0,
291                status: "cancelled".to_string(),
292            };
293            let _ = state.download_events.send(payload.clone());
294            handles.lock().unwrap().remove(&item_id);
295            active_dl.lock().unwrap().remove(&item_id);
296            return Err("Cancelled".to_string());
297        }
298        let chunk = chunk.map_err(|e| e.to_string())?;
299        dest.write_all(&chunk).map_err(|e| e.to_string())?;
300        downloaded += chunk.len() as u64;
301
302        let now = std::time::Instant::now();
303        if now.duration_since(last_report).as_millis() >= 200 {
304            let elapsed = now.duration_since(last_report).as_secs_f64().max(0.001);
305            let speed_kbps = ((downloaded - last_downloaded) as f64 / 1024.0) / elapsed;
306            let progress = if total_bytes > 0 {
307                (downloaded as f64 / total_bytes as f64) * 100.0
308            } else {
309                0.0
310            };
311            let payload = ProgressPayload {
312                id: item_id.clone(),
313                progress,
314                downloaded_bytes: downloaded,
315                total_bytes,
316                speed_kbps,
317                status: "downloading".to_string(),
318            };
319            let _ = state.download_events.send(payload.clone());
320            active_dl.lock().unwrap().insert(item_id.clone(), payload);
321            last_report = now;
322            last_downloaded = downloaded;
323        }
324    }
325
326    let processing_payload = ProgressPayload {
327        id: item_id.clone(),
328        progress: 100.0,
329        downloaded_bytes: downloaded,
330        total_bytes,
331        speed_kbps: 0.0,
332        status: "processing".to_string(),
333    };
334    let _ = state.download_events.send(processing_payload.clone());
335    active_dl
336        .lock()
337        .unwrap()
338        .insert(item_id.clone(), processing_payload);
339
340    let _quin_count = crate::resource_import::ingest_local_rdf(
341        &dest_path,
342        &item_id,
343        Path::new(&storage_path),
344        None,
345    )
346    .map_err(|e| e.to_string())?;
347
348    let _ = std::fs::remove_file(&dest_path);
349
350    let done_payload = ProgressPayload {
351        id: item_id.clone(),
352        progress: 100.0,
353        downloaded_bytes: downloaded,
354        total_bytes,
355        speed_kbps: 0.0,
356        status: "complete".to_string(),
357    };
358    let _ = state.download_events.send(done_payload.clone());
359    handles.lock().unwrap().remove(&item_id);
360    active_dl.lock().unwrap().remove(&item_id);
361    Ok("Download and vectorization complete".to_string())
362}
363
364pub async fn download_model(
365    url: String,
366    filename: String,
367    model_id: String,
368) -> Result<String, String> {
369    let state = crate::state::APP_STATE.get().unwrap();
370    let storage_path = state.config.lock().unwrap().storage_path.clone();
371    let handles = state.download_handles.clone();
372    let active_dl = state.active_downloads.clone();
373
374    let models_dir = PathBuf::from(&storage_path).join("Models");
375    std::fs::create_dir_all(&models_dir).map_err(|e| e.to_string())?;
376    let dest_path = models_dir.join(&filename);
377
378    let cancelled = Arc::new(AtomicBool::new(false));
379    handles
380        .lock()
381        .unwrap()
382        .insert(model_id.clone(), cancelled.clone());
383
384    let response = reqwest::get(&url)
385        .await
386        .and_then(reqwest::Response::error_for_status)
387        .map_err(|e| {
388            handles.lock().unwrap().remove(&model_id);
389            active_dl.lock().unwrap().remove(&model_id);
390            e.to_string()
391        })?;
392    let total_bytes = response.content_length().unwrap_or(0);
393    let mut dest = std::fs::File::create(&dest_path).map_err(|e| {
394        handles.lock().unwrap().remove(&model_id);
395        active_dl.lock().unwrap().remove(&model_id);
396        format!("create {}: {e}", dest_path.display())
397    })?;
398    let mut stream = response.bytes_stream();
399    let mut downloaded: u64 = 0;
400    let mut last_report = std::time::Instant::now();
401    let mut last_downloaded: u64 = 0;
402
403    while let Some(chunk) = stream.next().await {
404        if cancelled.load(Ordering::Relaxed) {
405            let _ = std::fs::remove_file(&dest_path);
406            let payload = ProgressPayload {
407                id: model_id.clone(),
408                progress: 0.0,
409                downloaded_bytes: downloaded,
410                total_bytes,
411                speed_kbps: 0.0,
412                status: "cancelled".to_string(),
413            };
414            let _ = state.download_events.send(payload.clone());
415            handles.lock().unwrap().remove(&model_id);
416            active_dl.lock().unwrap().remove(&model_id);
417            return Err("Cancelled".to_string());
418        }
419        let chunk = match chunk {
420            Ok(chunk) => chunk,
421            Err(error) => {
422                drop(dest);
423                let _ = std::fs::remove_file(&dest_path);
424                handles.lock().unwrap().remove(&model_id);
425                active_dl.lock().unwrap().remove(&model_id);
426                return Err(format!("download stream failed: {error}"));
427            }
428        };
429        if let Err(error) = dest.write_all(&chunk) {
430            drop(dest);
431            let _ = std::fs::remove_file(&dest_path);
432            handles.lock().unwrap().remove(&model_id);
433            active_dl.lock().unwrap().remove(&model_id);
434            return Err(format!("write {}: {error}", dest_path.display()));
435        }
436        downloaded += chunk.len() as u64;
437
438        let now = std::time::Instant::now();
439        if now.duration_since(last_report).as_millis() >= 200 {
440            let elapsed = now.duration_since(last_report).as_secs_f64().max(0.001);
441            let speed_kbps = ((downloaded - last_downloaded) as f64 / 1024.0) / elapsed;
442            let progress = if total_bytes > 0 {
443                (downloaded as f64 / total_bytes as f64) * 100.0
444            } else {
445                0.0
446            };
447            let payload = ProgressPayload {
448                id: model_id.clone(),
449                progress,
450                downloaded_bytes: downloaded,
451                total_bytes,
452                speed_kbps,
453                status: "downloading".to_string(),
454            };
455            let _ = state.download_events.send(payload.clone());
456            active_dl.lock().unwrap().insert(model_id.clone(), payload);
457            last_report = now;
458            last_downloaded = downloaded;
459        }
460    }
461
462    let done_payload = ProgressPayload {
463        id: model_id.clone(),
464        progress: 100.0,
465        downloaded_bytes: downloaded,
466        total_bytes,
467        speed_kbps: 0.0,
468        status: "complete".to_string(),
469    };
470    let _ = state.download_events.send(done_payload.clone());
471    handles.lock().unwrap().remove(&model_id);
472    active_dl.lock().unwrap().remove(&model_id);
473    Ok(dest_path.to_string_lossy().to_string())
474}
475
476pub fn cancel_download(id: String) -> Result<(), String> {
477    let state = crate::state::APP_STATE.get().unwrap();
478    if let Some(flag) = state.download_handles.lock().unwrap().get(&id) {
479        flag.store(true, Ordering::Relaxed);
480    }
481    Ok(())
482}
483
484pub fn start_daemon() -> String {
485    "Daemon Started".to_string()
486}
487
488pub fn daemon_status() -> String {
489    let state = crate::state::APP_STATE.get().unwrap();
490    if *state.daemon_running.lock().unwrap() {
491        "running".to_string()
492    } else {
493        "stopped".to_string()
494    }
495}
496
497pub fn get_tax_suite() -> TaxRecipientSuite {
498    let state = crate::state::APP_STATE.get().unwrap();
499    state.tax_suite.lock().unwrap().clone()
500}
501
502pub fn save_tax_suite(suite: TaxRecipientSuite) -> Result<(), String> {
503    let state = crate::state::APP_STATE.get().unwrap();
504    suite.validate()?;
505    let data_dir = state.config.lock().unwrap().storage_path.clone();
506    let path = suite_file_path(&data_dir);
507    if let Some(p) = path.parent() {
508        std::fs::create_dir_all(p).map_err(|e| e.to_string())?;
509    }
510    let json = serde_json::to_string_pretty(&suite).map_err(|e| e.to_string())?;
511    std::fs::write(&path, json).map_err(|e| e.to_string())?;
512    *state.tax_suite.lock().unwrap() = suite;
513    Ok(())
514}
515
516pub fn dispatch_tax_payment(gross_amount_micro_cents: u64) -> Result<DispatchResult, String> {
517    let state = crate::state::APP_STATE.get().unwrap();
518    let suite = state.tax_suite.lock().unwrap().clone();
519    let plan = route_tax_payment(gross_amount_micro_cents, &suite)?;
520    let disp = IlpDispatcher::new(HttpIlpTransport {
521        connector_url: "http://localhost:7770".to_string(),
522    });
523    Ok(disp.dispatch(&plan))
524}
525
526pub fn accept_vault_handshake(did_key: String, _payload: String) -> Result<String, String> {
527    println!("[VC-8] Vault handshake from: {}", did_key);
528    Ok("HANDSHAKE_SUCCESS".to_string())
529}
530
531pub fn receive_vault_job(
532    job_id: String,
533    task_type: String,
534    _data_blob_cbor_ld: Vec<u8>,
535) -> Result<String, String> {
536    println!("[VC-12] Offload job {} type {}", job_id, task_type);
537    if task_type == "LLM_INFERENCE" && check_ollama_status() {
538        Ok("INFERENCE_QUEUED".to_string())
539    } else {
540        Err("UNSUPPORTED_TASK_OR_NO_CAPACITY".to_string())
541    }
542}
543
544pub async fn ingest_pdf(file_name: String) -> Result<ingestion::IngestionResult, String> {
545    let result = ingestion::process_pdf(&file_name)?;
546    q42_compiler::compile_to_q42(&file_name, &result.bookmarks)?;
547    Ok(result)
548}
549
550pub async fn ingest_literature(file_path: String) -> Result<String, String> {
551    let state = crate::state::APP_STATE.get().unwrap();
552    let storage_path = state.config.lock().unwrap().storage_path.clone();
553    let lib_dir = PathBuf::from(&storage_path).join("SemanticLibrary");
554    if !lib_dir.exists() {
555        std::fs::create_dir_all(&lib_dir).map_err(|e| e.to_string())?;
556    }
557
558    let source_path = std::path::Path::new(&file_path);
559    let filename = source_path.file_name().unwrap_or_default();
560    let dest_path = lib_dir.join(filename);
561    std::fs::copy(&source_path, &dest_path).map_err(|e| e.to_string())?;
562
563    let text = pdf_extract::extract_text(&dest_path).map_err(|e| e.to_string())?;
564    let preview = if text.len() > 100 {
565        &text[0..100]
566    } else {
567        &text
568    };
569
570    Ok(format!(
571        "Successfully ingested literature: {}. Generated ontology nodes from preview: '{}...'",
572        filename.to_string_lossy(),
573        preview.replace("\n", " ")
574    ))
575}
576
577pub async fn upsert_cmld_definition(term: String, context_did: String) -> Result<String, String> {
578    Ok(format!(
579        "Successfully mapped '{}' to Context: {}",
580        term, context_did
581    ))
582}
583
584pub async fn ingest_ontology(file_name: String) -> Result<serde_json::Value, String> {
585    let state = crate::state::APP_STATE.get().unwrap();
586    let storage_path = state.config.lock().unwrap().storage_path.clone();
587    let index_dir = PathBuf::from(&storage_path).join("Index");
588    let source_path = index_dir.join(&file_name);
589
590    if !source_path.is_file() {
591        return Err(format!(
592            "Ontology source not found in Index/: {}",
593            source_path.display()
594        ));
595    }
596
597    let ontology_id = source_path
598        .file_stem()
599        .and_then(|s| s.to_str())
600        .unwrap_or(&file_name)
601        .to_string();
602
603    let quin_count = crate::resource_import::ingest_local_rdf(
604        &source_path,
605        &ontology_id,
606        Path::new(&storage_path),
607        None,
608    )
609    .map_err(|e| e.to_string())?;
610
611    let q42_path = index_dir.join(format!("{ontology_id}.q42"));
612
613    Ok(serde_json::json!({
614        "status": "success",
615        "file": file_name,
616        "ontology_id": ontology_id,
617        "q42_path": q42_path.to_string_lossy(),
618        "quin_count": quin_count,
619    }))
620}
621
622pub async fn import_catalog_ontology(id: String) -> Result<serde_json::Value, String> {
623    let state = crate::state::APP_STATE.get().unwrap();
624    let storage_path = state.config.lock().unwrap().storage_path.clone();
625    let catalog = load_workspace_catalog();
626
627    let cancelled = Arc::new(AtomicBool::new(false));
628    state
629        .download_handles
630        .lock()
631        .unwrap()
632        .insert(id.clone(), cancelled.clone());
633
634    let progress = crate::resource_import::ImportProgressCtx {
635        id: id.clone(),
636        handles: state.download_handles.clone(),
637        active_downloads: state.active_downloads.clone(),
638        download_events: state.download_events.clone(),
639    };
640
641    let result = crate::resource_import::import_catalog_ontology_with_options(
642        &catalog,
643        &id,
644        Path::new(&storage_path),
645        Some(&progress),
646        true,
647    )
648    .await
649    .map_err(|e| {
650        state.download_handles.lock().unwrap().remove(&id);
651        state.active_downloads.lock().unwrap().remove(&id);
652        e.to_string()
653    })?;
654
655    qualia_core_db::daemon_graph::init_daemon_graph(&storage_path);
656
657    serde_json::to_value(result).map_err(|e| e.to_string())
658}
659
660pub async fn export_to_solid(
661    input_q42_path: String,
662    output_dir_path: String,
663) -> Result<String, String> {
664    qualia_core_db::solid_ldp::SolidExporter::export_to_solid_pod(&input_q42_path, &output_dir_path)
665        .map(|_| format!("Exported to {}", output_dir_path))
666        .map_err(|e| e.to_string())
667}
668
669/// Consumer: fetch a Solid LDP resource and return status + Turtle body + Quin count.
670pub async fn fetch_from_solid_pod(
671    url: String,
672    bearer_token: Option<String>,
673) -> Result<serde_json::Value, String> {
674    let r = qualia_solid_bridge::fetch_resource(&url, bearer_token.as_deref())
675        .await
676        .map_err(|e| e.to_string())?;
677    Ok(serde_json::json!({
678        "url": r.url,
679        "status": r.status,
680        "content_type": r.content_type,
681        "quin_count": r.quin_count,
682        "body": r.body,
683    }))
684}
685
686/// Consumer: PUT a local Turtle/file to a Solid resource URL (sync-to-pod).
687pub async fn put_to_solid_pod(
688    url: String,
689    body: Vec<u8>,
690    content_type: Option<String>,
691    bearer_token: Option<String>,
692) -> Result<serde_json::Value, String> {
693    let ct = content_type.unwrap_or_else(|| "text/turtle".into());
694    let status = qualia_solid_bridge::put_resource(&url, &body, &ct, bearer_token.as_deref())
695        .await
696        .map_err(|e| e.to_string())?;
697    Ok(serde_json::json!({ "ok": true, "status": status, "url": url }))
698}
699
700/// Sync: if `body_or_path` is a path to an existing file, upload it; else treat as Turtle body.
701pub async fn sync_to_solid_pod(
702    pod_url: String,
703    body_or_path: Option<String>,
704    bearer_token: Option<String>,
705) -> Result<String, String> {
706    let (bytes, ct) = if let Some(ref p) = body_or_path {
707        let path = std::path::Path::new(p);
708        if path.is_file() {
709            let b = std::fs::read(path).map_err(|e| e.to_string())?;
710            let ct = if p.ends_with(".json") || p.ends_with(".jsonld") {
711                "application/ld+json"
712            } else {
713                "text/turtle"
714            };
715            (b, ct.to_string())
716        } else {
717            (p.as_bytes().to_vec(), "text/turtle".into())
718        }
719    } else {
720        // Minimal deposit marker when UI only passes a URL
721        let body = format!(
722            "@prefix dcterms: <http://purl.org/dc/terms/> .\n<> dcterms:description \"Qualia sync {}\" .\n",
723            chrono::Utc::now().to_rfc3339()
724        );
725        (body.into_bytes(), "text/turtle".into())
726    };
727    let status = qualia_solid_bridge::put_resource(&pod_url, &bytes, &ct, bearer_token.as_deref())
728        .await
729        .map_err(|e| e.to_string())?;
730    Ok(format!(
731        "Synced to Solid Pod {pod_url} (HTTP {status}, {} bytes)",
732        bytes.len()
733    ))
734}
735
736pub async fn ingest_image(file_path: String) -> Result<serde_json::Value, String> {
737    ingest_image_typed(file_path, "Generic Asset".to_string()).await
738}
739
740pub async fn ingest_image_typed(
741    file_path: String,
742    typology: String,
743) -> Result<serde_json::Value, String> {
744    let state = crate::state::APP_STATE.get().unwrap();
745    let storage = state.config.lock().unwrap().storage_path.clone();
746    let active = load_active_model_record_from_disk();
747    let result = crate::vision_ingest::ingest_image_with_active_record(
748        Path::new(&storage),
749        active,
750        Path::new(&file_path),
751        &typology,
752    )
753    .map_err(|e| e.to_string())?;
754    serde_json::to_value(result).map_err(|e| e.to_string())
755}
756
757pub async fn ingest_image_async(file_path: String, typology: String) -> Result<(), String> {
758    let state = crate::state::APP_STATE.get().unwrap();
759    let storage = state.config.lock().unwrap().storage_path.clone();
760    let active = load_active_model_record_from_disk();
761    tokio::spawn(async move {
762        let _ = crate::vision_ingest::ingest_image_with_active_record(
763            Path::new(&storage),
764            active,
765            Path::new(&file_path),
766            &typology,
767        );
768    });
769    Ok(())
770}