Skip to main content

qualia_client_core/wellfair/api/
backup_clinical.rs

1//! Backup/restore + clinical documents
2
3use super::super::backup::{self, BackupReport};
4use super::super::blob_store::BlobStore;
5use super::super::journal::JournalEntry;
6use super::super::sync_outbox::SyncOutbox;
7use wellfare_core::clinical::{
8    build_clinical_attachment_envelope, build_clinical_report_envelope,
9    clinical_attachment_summary, clinical_report_summary, AttachmentMeta, ClinicalReport,
10    ClinicalReportType,
11};
12
13use super::*;
14
15impl WebizenHostApi {
16    // --- Backup / restore of the WellFair data subtree (T3.3) ---
17
18    /// Build a portable backup of this node's WellFair data (the `wellfair/` subtree) as archive
19    /// bytes. The Sanctuary vault stays encrypted inside it.
20    pub fn export_backup_bytes(&self) -> Result<Vec<u8>, String> {
21        backup::create_backup(&self.storage_root, Self::now_unix() as u32)
22    }
23
24    /// Restore a backup (archive bytes) into this node's storage. Path-traversal-safe.
25    pub fn import_backup_bytes(&self, bytes: &[u8]) -> Result<BackupReport, String> {
26        backup::restore_backup(&self.storage_root, bytes)
27    }
28
29    /// Write a backup archive to `path`; returns the file count + archive size.
30    #[cfg(not(target_arch = "wasm32"))]
31    pub fn export_backup_to_path(&self, path: &str) -> Result<BackupReport, String> {
32        let archive = backup::build_archive(&self.storage_root, Self::now_unix() as u32)?;
33        let files = archive.files.len();
34        let bytes = backup::encode_archive(&archive)?;
35        let size = bytes.len() as u64;
36        std::fs::write(path, &bytes).map_err(|e| e.to_string())?;
37        Ok(BackupReport { files, bytes: size })
38    }
39
40    /// Restore a backup archive from `path` into this node's storage.
41    #[cfg(not(target_arch = "wasm32"))]
42    pub fn import_backup_from_path(&self, path: &str) -> Result<BackupReport, String> {
43        let bytes = std::fs::read(path).map_err(|e| e.to_string())?;
44        self.import_backup_bytes(&bytes)
45    }
46
47    /// A node health/status snapshot (record counts, sync queue depths, data footprint, Sanctuary
48    /// state, build version). Native-only (reads the on-disk Sanctuary vault state).
49    #[cfg(not(target_arch = "wasm32"))]
50    pub fn diagnostics_report(&self) -> Result<DiagnosticsReport, String> {
51        let journal_records = self.list_health_records(4096)?.len();
52        let outbox_queued = SyncOutbox::open(&self.storage_root)
53            .map_err(|e| e.to_string())?
54            .count_queued()
55            .map_err(|e| e.to_string())?;
56        let inbox_validated = self.validated_sync_operations()?.len();
57        let (data_files, data_bytes) = backup::wellfair_data_stats(&self.storage_root)?;
58        Ok(DiagnosticsReport {
59            crate_version: env!("CARGO_PKG_VERSION").to_string(),
60            sanctuary_configured: super::super::sanctuary_vault::is_configured(&self.storage_root),
61            sanctuary_keychain_wrapped: super::super::sanctuary_vault::is_keychain_wrapped(
62                &self.storage_root,
63            ),
64            journal_records,
65            outbox_queued,
66            inbox_validated,
67            data_files,
68            data_bytes,
69        })
70    }
71
72    // --- Clinical documents (Phase 3 / CLI-01..) ---
73
74    pub fn add_clinical_report(
75        &mut self,
76        title: &str,
77        report_type: ClinicalReportType,
78        observed_at_unix: u32,
79        body: &str,
80        author_label: Option<String>,
81    ) -> Result<JournalEntry, String> {
82        let mut report = ClinicalReport::new(title, report_type, observed_at_unix, body);
83        report.author_label = author_label.filter(|s| !s.is_empty());
84        let hash =
85            Self::payload_hash_hex(&serde_json::to_string(&report).map_err(|e| e.to_string())?);
86        let asserted = Self::now_unix() as u32;
87        let envelope = build_clinical_report_envelope(
88            &report,
89            &self.owner_did,
90            &self.author_did,
91            asserted,
92            Some(hash),
93        );
94        let summary = clinical_report_summary(&report);
95        self.submit_record_with_summary(QAPP_CLINICAL, envelope, SOURCE_CLINICAL, Some(summary))?;
96        self.finalize_batch().ok();
97        self.latest_journal_entry()
98    }
99
100    pub fn list_clinical_reports(&self, limit: usize) -> Result<Vec<JournalEntry>, String> {
101        self.list_journal_by_kind("clinical_report", limit)
102    }
103
104    /// Store an attachment's bytes as a content-addressed blob and commit its metadata record.
105    /// The bytes live only in the blob store; the journal row holds filename/size/hash metadata.
106    pub fn add_clinical_attachment(
107        &mut self,
108        filename: &str,
109        media_type: &str,
110        bytes: &[u8],
111    ) -> Result<JournalEntry, String> {
112        let store = BlobStore::open(&self.storage_root).map_err(|e| e.to_string())?;
113        let content_hash = store.put(bytes).map_err(|e| e.to_string())?;
114        let meta = AttachmentMeta::new(filename, media_type, bytes.len() as u64, content_hash);
115        let asserted = Self::now_unix() as u32;
116        let envelope =
117            build_clinical_attachment_envelope(&meta, &self.owner_did, &self.author_did, asserted);
118        let summary = clinical_attachment_summary(&meta);
119        self.submit_record_with_summary(QAPP_CLINICAL, envelope, SOURCE_CLINICAL, Some(summary))?;
120        self.finalize_batch().ok();
121        self.latest_journal_entry()
122    }
123
124    pub fn list_clinical_attachments(&self, limit: usize) -> Result<Vec<JournalEntry>, String> {
125        self.list_journal_by_kind("clinical_attachment", limit)
126    }
127
128    /// Read the blob bytes for any record that carries a `blob_hash` (clinical attachments,
129    /// government-letter documents, …), integrity-verified by the blob store.
130    pub fn attachment_bytes(&self, record_id: &str) -> Result<Option<Vec<u8>>, String> {
131        let Some(entry) = self
132            .list_health_records(256)?
133            .into_iter()
134            .find(|e| e.id == record_id)
135        else {
136            return Ok(None);
137        };
138        let Some(hash) = entry.blob_hash else {
139            return Ok(None);
140        };
141        BlobStore::open(&self.storage_root)
142            .map_err(|e| e.to_string())?
143            .get(&hash)
144            .map_err(|e| e.to_string())
145    }
146}