1use std::path::Path;
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use super::super::accessibility_prefs;
7use super::super::consent_store::ConsentGrantRecord;
8use super::super::export_package::{
9 build_export_package, export_policy_receipt, ExportReceipt, HealthExportPackage,
10};
11use super::super::graph_query::GraphCoverageRow;
12use super::super::host_state::WellfairHostSnapshot;
13use super::super::host_state::{
14 AccessibilityPreferences, ConsentGrantDraft, PolicyDecisionDto, SubmitOutcome,
15};
16use super::super::import_samsung::{
17 import_samsung_folder, ingest_companion_health_bundle, SamsungImportReport,
18};
19use super::super::journal::JournalEntry;
20use super::super::policy::{DecisionResult, PolicyDecisionService};
21use super::super::receipt::{receipt_from_decision, ReceiptRecord};
22use super::super::sanctuary::{apply_sanctuary_projection, load_prefs as load_sanctuary_prefs};
23use super::super::snapshot::build_host_snapshot;
24use super::super::sync_outbox::SyncOutboxEntry;
25use super::super::vault::VaultService;
26use ed25519_dalek::SigningKey;
27use qualia_core_db::key_vault::KeyVault;
28use sha2::{Digest, Sha256};
29use wellfare_core::companion_sync::CompanionHealthBundle;
30use wellfare_core::conditions::{
31 allergy_summary, build_allergy_envelope, build_condition_envelope, condition_summary,
32 AllergyReport, ConditionReport,
33};
34use wellfare_core::medication::{
35 self, AdministrationStatus, DietEntry, MedicationAdministration, MedicationCatalogEntry,
36};
37use wellfare_core::personal_records::{
38 build_disputed_diagnosis_envelope, build_housing_safety_envelope, disputed_diagnosis_summary,
39 housing_safety_summary, DisputedDiagnosisReport, HousingSafetyReport,
40};
41use wellfare_core::record::RecordEnvelope;
42
43use super::*;
44
45impl WebizenHostApi {
46 pub fn new(
47 vault: VaultService,
48 policy: PolicyDecisionService,
49 signing_key: SigningKey,
50 owner_did: String,
51 author_did: String,
52 storage_root: std::path::PathBuf,
53 ) -> Self {
54 Self {
55 vault,
56 policy,
57 signing_key,
58 owner_did,
59 author_did,
60 storage_root,
61 }
62 }
63
64 pub fn save_accessibility(&self, prefs: &AccessibilityPreferences) -> Result<(), String> {
65 accessibility_prefs::save(&self.storage_root, prefs).map_err(|e| e.to_string())
66 }
67
68 pub fn load_accessibility(&self) -> AccessibilityPreferences {
69 accessibility_prefs::load(&self.storage_root)
70 }
71
72 pub fn snapshot_from_vault(
73 key_vault: &KeyVault,
74 owner_label: &str,
75 demo_mode: bool,
76 ) -> WellfairHostSnapshot {
77 build_host_snapshot(key_vault, true, owner_label, demo_mode)
78 }
79
80 pub fn build_snapshot(
81 &mut self,
82 key_vault: &KeyVault,
83 owner_label: &str,
84 ) -> WellfairHostSnapshot {
85 let mut snap = super::super::snapshot::build_host_snapshot_with_storage(
86 key_vault,
87 true,
88 owner_label,
89 false,
90 Some(&self.storage_root),
91 );
92 if let Ok(count) = self.vault.journal_count() {
93 snap.health_record_count = count as u32;
94 }
95 snap.graph_quin_count = self.vault.graph_quin_count() as u32;
96 if let Ok(pending) = self.vault.wal_buffered_quins() {
97 snap.pending_jobs = pending as u32;
98 snap.sync_state = if pending > 0 {
99 super::super::host_state::SyncQueueState::Queued
100 } else {
101 super::super::host_state::SyncQueueState::Idle
102 };
103 }
104 if let Some(hash) = self.vault.last_checkpoint_hash() {
105 snap.last_checkpoint_prefix = Some(hex::encode(&hash[..4]));
106 }
107 if let Ok(queued) = self.vault.outbox_queued_count() {
108 snap.pending_jobs = snap.pending_jobs.saturating_add(queued as u32);
109 if queued > 0 {
110 snap.sync_state = super::super::host_state::SyncQueueState::Queued;
111 }
112 }
113 snap
114 }
115
116 pub fn fetch_10d_asset_on_demand(&self, _asset_uri: &str) -> Result<Vec<u8>, String> {
118 Ok(Vec::new())
120 }
121
122 pub(crate) fn chora_storage_root(&self) -> &std::path::Path {
123 &self.storage_root
124 }
125
126 pub fn storage_root(&self) -> &std::path::Path {
129 &self.storage_root
130 }
131
132 pub fn owner_did_hash(&self) -> u64 {
134 qualia_core_db::q_hash(&self.owner_did)
135 }
136
137 pub(crate) fn chora_signing_key(&self) -> &SigningKey {
138 &self.signing_key
139 }
140
141 pub(crate) fn chora_owner_did(&self) -> &str {
142 &self.owner_did
143 }
144
145 pub(crate) fn now_unix() -> u64 {
146 SystemTime::now()
147 .duration_since(UNIX_EPOCH)
148 .map(|d| d.as_secs())
149 .unwrap_or(0)
150 }
151
152 pub fn evaluate_policy(
153 &self,
154 qapp_id: &str,
155 requested_scope: &str,
156 sensitivity: wellfare_core::record::SensitivityClass,
157 epistemic: wellfare_core::record::EpistemicStatus,
158 ) -> Result<PolicyDecisionDto, String> {
159 let now = Self::now_unix();
160 let grants = self
161 .vault
162 .list_active_consents(now)
163 .map_err(|e| e.to_string())?;
164 let decision = self.policy.evaluate_access(
165 qapp_id,
166 requested_scope,
167 sensitivity,
168 epistemic,
169 &grants,
170 now,
171 false,
172 );
173 Ok(decision.to_dto())
174 }
175
176 pub fn grant_consent(
177 &mut self,
178 draft: &ConsentGrantDraft,
179 scope: &str,
180 ) -> Result<ConsentGrantRecord, String> {
181 let grant = ConsentGrantRecord::from_draft(draft, scope);
182 self.vault
183 .append_consent(&grant)
184 .map_err(|e| e.to_string())?;
185 let ts = grant.granted_at_unix;
186 let decision = DecisionResult::Permit {
187 obligations: vec!["consent_granted".into(), "emit_wal_receipt".into()],
188 };
189 let receipt = receipt_from_decision(
190 "wellfair-shell",
191 &grant.id,
192 ts,
193 &decision,
194 self.vault.last_checkpoint_hash(),
195 );
196 self.vault
197 .append_receipt(&receipt)
198 .map_err(|e| e.to_string())?;
199 Ok(grant)
200 }
201
202 pub fn revoke_consent(&mut self, grant_id: &str) -> Result<bool, String> {
203 let revoked = self
204 .vault
205 .revoke_consent(grant_id)
206 .map_err(|e| e.to_string())?;
207 if revoked {
208 let ts = Self::now_unix() as u32;
209 let decision = DecisionResult::Deny {
210 reasons: vec!["consent_revoked".into()],
211 };
212 let receipt = receipt_from_decision(
213 "wellfair-shell",
214 grant_id,
215 ts,
216 &decision,
217 self.vault.last_checkpoint_hash(),
218 );
219 self.vault
220 .append_receipt(&receipt)
221 .map_err(|e| e.to_string())?;
222 }
223 Ok(revoked)
224 }
225
226 pub fn list_consents(&self) -> Result<Vec<ConsentGrantRecord>, String> {
227 self.vault
228 .list_active_consents(Self::now_unix())
229 .map_err(|e| e.to_string())
230 }
231
232 pub fn submit_record(
233 &mut self,
234 qapp_id: &str,
235 envelope: RecordEnvelope,
236 source: &str,
237 ) -> Result<usize, String> {
238 self.submit_record_with_summary(qapp_id, envelope, source, None)
239 }
240
241 pub fn submit_record_with_summary(
242 &mut self,
243 qapp_id: &str,
244 envelope: RecordEnvelope,
245 source: &str,
246 summary: Option<String>,
247 ) -> Result<usize, String> {
248 match self.submit_record_guarded(qapp_id, envelope, source, summary)? {
249 SubmitOutcome::Committed { quins } => Ok(quins),
250 SubmitOutcome::Suspended { .. } => {
251 Err("Policy requires guardian approval before write".into())
252 }
253 }
254 }
255
256 pub fn submit_record_guarded(
261 &mut self,
262 qapp_id: &str,
263 envelope: RecordEnvelope,
264 source: &str,
265 summary: Option<String>,
266 ) -> Result<SubmitOutcome, String> {
267 let now = Self::now_unix();
268 let grants = self
269 .vault
270 .list_active_consents(now)
271 .map_err(|e| e.to_string())?;
272 let is_proxy = envelope
273 .proxy_did
274 .as_deref()
275 .map(|p| p != envelope.owner_did)
276 .unwrap_or(false);
277 let decision = self.policy.evaluate_access(
278 qapp_id,
279 "write_record",
280 envelope.sensitivity,
281 envelope.epistemic_status,
282 &grants,
283 now,
284 is_proxy,
285 );
286
287 match &decision {
288 DecisionResult::Deny { reasons } => {
289 Err(format!("Policy denied: {}", reasons.join("; ")))
290 }
291 DecisionResult::Prompt { .. } => Err("Policy requires consent before write".into()),
292 DecisionResult::Suspend { required_approvals } => {
293 let proposal = self.escrow_proxy_write(&envelope, summary, *required_approvals)?;
294 let threshold = proposal.threshold;
295 Ok(SubmitOutcome::Suspended {
296 proposal_id: proposal.id,
297 threshold,
298 })
299 }
300 DecisionResult::Permit { .. } => {
301 let quins =
302 self.commit_permitted(qapp_id, &envelope, source, summary, &decision)?;
303 Ok(SubmitOutcome::Committed { quins })
304 }
305 }
306 }
307
308 pub(crate) fn commit_permitted(
310 &mut self,
311 qapp_id: &str,
312 envelope: &RecordEnvelope,
313 source: &str,
314 summary: Option<String>,
315 decision: &DecisionResult,
316 ) -> Result<usize, String> {
317 let principal_did = qualia_core_db::q_hash(&self.owner_did);
318 let committed = self
319 .vault
320 .commit_envelope(envelope, &self.signing_key, principal_did, source, summary)
321 .map_err(|e| e.to_string())?;
322 let ts = envelope.asserted_time_unix;
323 let receipt = receipt_from_decision(
324 qapp_id,
325 &envelope.id,
326 ts,
327 decision,
328 self.vault.last_checkpoint_hash(),
329 );
330 self.vault
331 .append_receipt(&receipt)
332 .map_err(|e| e.to_string())?;
333 Ok(committed)
334 }
335
336 pub fn finalize_batch(&mut self) -> Result<String, String> {
337 let hash = self.vault.checkpoint().map_err(|e| e.to_string())?;
338 Ok(hex::encode(hash))
339 }
340
341 pub fn list_health_records(&self, limit: usize) -> Result<Vec<JournalEntry>, String> {
342 let entries = self
343 .vault
344 .list_health_records(limit)
345 .map_err(|e| e.to_string())?;
346 let prefs = load_sanctuary_prefs(&self.storage_root);
347 Ok(apply_sanctuary_projection(&prefs, entries))
348 }
349
350 pub fn list_receipts(&self, limit: usize) -> Result<Vec<ReceiptRecord>, String> {
351 self.vault.list_receipts(limit).map_err(|e| e.to_string())
352 }
353
354 pub fn list_outbox(&self, limit: usize) -> Result<Vec<SyncOutboxEntry>, String> {
355 self.vault.list_outbox(limit).map_err(|e| e.to_string())
356 }
357
358 pub fn export_health_package(
360 &mut self,
361 limit: usize,
362 ) -> Result<(HealthExportPackage, ExportReceipt), String> {
363 self.finalize_batch().ok();
364 let entries = self.list_health_records(limit)?;
365 let exported_at = Self::now_unix() as u32;
366 let pkg = build_export_package(&entries, exported_at, self.vault.last_checkpoint_hash());
367 let receipt = export_policy_receipt(&pkg, exported_at);
368 self.vault
369 .append_receipt(&receipt)
370 .map_err(|e| e.to_string())?;
371 let export_receipt = ExportReceipt::from_package(&pkg);
372 Ok((pkg, export_receipt))
373 }
374
375 pub fn query_graph_coverage(&self, limit: usize) -> Result<Vec<GraphCoverageRow>, String> {
381 let rows = self
382 .vault
383 .graph_coverage(limit)
384 .map_err(|e| e.to_string())?;
385 let prefs = load_sanctuary_prefs(&self.storage_root);
386 if !prefs.enabled || !prefs.locked {
387 return Ok(rows);
388 }
389 Ok(rows
390 .into_iter()
391 .filter(|row| !super::super::sanctuary::is_sanctuary_protected_kind(&row.kind))
392 .collect())
393 }
394
395 pub(crate) fn payload_hash_hex(payload: &str) -> String {
396 hex::encode(Sha256::digest(payload.as_bytes()).as_slice())
397 }
398
399 pub fn add_condition(&mut self, report: &ConditionReport) -> Result<JournalEntry, String> {
400 let payload = serde_json::to_string(report).map_err(|e| e.to_string())?;
401 let hash = Self::payload_hash_hex(&payload);
402 let asserted = Self::now_unix() as u32;
403 let envelope = build_condition_envelope(
404 report,
405 &self.owner_did,
406 &self.author_did,
407 asserted,
408 Some(hash),
409 );
410 let summary = condition_summary(report);
411 self.submit_record_with_summary(QAPP_SHELL, envelope, SOURCE_PERSONAL, Some(summary))?;
412 let entries = self.list_health_records(1)?;
413 entries
414 .into_iter()
415 .next()
416 .ok_or_else(|| "condition committed but not found in journal".to_string())
417 }
418
419 pub fn add_disputed_diagnosis(
420 &mut self,
421 report: &DisputedDiagnosisReport,
422 ) -> Result<JournalEntry, String> {
423 let payload = serde_json::to_string(report).map_err(|e| e.to_string())?;
424 let hash = Self::payload_hash_hex(&payload);
425 let asserted = Self::now_unix() as u32;
426 let envelope = build_disputed_diagnosis_envelope(
427 report,
428 &self.owner_did,
429 &self.author_did,
430 asserted,
431 Some(hash),
432 );
433 let summary = disputed_diagnosis_summary(report);
434 self.submit_record_with_summary(QAPP_SHELL, envelope, SOURCE_PERSONAL, Some(summary))?;
435 self.list_health_records(1)?
436 .into_iter()
437 .next()
438 .ok_or_else(|| "disputed diagnosis committed but not found in journal".to_string())
439 }
440
441 pub fn add_housing_safety(
442 &mut self,
443 report: &HousingSafetyReport,
444 ) -> Result<JournalEntry, String> {
445 let payload = serde_json::to_string(report).map_err(|e| e.to_string())?;
446 let hash = Self::payload_hash_hex(&payload);
447 let asserted = Self::now_unix() as u32;
448 let envelope = build_housing_safety_envelope(
449 report,
450 &self.owner_did,
451 &self.author_did,
452 asserted,
453 Some(hash),
454 );
455 let summary = housing_safety_summary(report);
456 self.submit_record_with_summary(QAPP_SHELL, envelope, SOURCE_PERSONAL, Some(summary))?;
457 self.list_health_records(1)?
458 .into_iter()
459 .next()
460 .ok_or_else(|| "housing/safety committed but not found in journal".to_string())
461 }
462
463 pub fn add_allergy(&mut self, report: &AllergyReport) -> Result<JournalEntry, String> {
464 let payload = serde_json::to_string(report).map_err(|e| e.to_string())?;
465 let hash = Self::payload_hash_hex(&payload);
466 let asserted = Self::now_unix() as u32;
467 let envelope = build_allergy_envelope(
468 report,
469 &self.owner_did,
470 &self.author_did,
471 asserted,
472 Some(hash),
473 );
474 let summary = allergy_summary(report);
475 self.submit_record_with_summary(QAPP_SHELL, envelope, SOURCE_PERSONAL, Some(summary))?;
476 let entries = self.list_health_records(1)?;
477 entries
478 .into_iter()
479 .next()
480 .ok_or_else(|| "allergy committed but not found in journal".to_string())
481 }
482
483 pub fn graph_quin_count(&self) -> usize {
484 self.vault.graph_quin_count()
485 }
486
487 pub fn import_samsung_health_folder(&mut self, folder: &Path) -> SamsungImportReport {
488 let owner = self.owner_did.clone();
489 let author = self.author_did.clone();
490 let mut report = import_samsung_folder(self, folder, &owner, &author);
491 if report.records_committed > 0 {
492 if let Ok(hash) = self.finalize_batch() {
493 report.checkpoint_hash = Some(hash);
494 }
495 }
496 report
497 }
498
499 pub fn ingest_companion_health_bundle(
501 &mut self,
502 bundle: &CompanionHealthBundle,
503 ) -> SamsungImportReport {
504 let owner = self.owner_did.clone();
505 let author = self.author_did.clone();
506 let mut report = ingest_companion_health_bundle(self, bundle, &owner, &author);
507 if report.records_committed > 0 {
508 if let Ok(hash) = self.finalize_batch() {
509 report.checkpoint_hash = Some(hash);
510 }
511 }
512 report
513 }
514
515 const QAPP_MEDICATION: &'static str = "wellfair-medication";
516
517 pub fn add_medication(
518 &mut self,
519 name: &str,
520 dose: &str,
521 route: &str,
522 schedule_times: Vec<String>,
523 ) -> Result<JournalEntry, String> {
524 let now = Self::now_unix() as u32;
525 let entry = MedicationCatalogEntry {
526 id: medication::new_medication_id(name, now),
527 name: name.to_string(),
528 dose: dose.to_string(),
529 route: route.to_string(),
530 schedule_times,
531 prescriber: None,
532 ceased_at_unix: None,
533 created_at_unix: now,
534 };
535 let packed = medication::medication_envelope(&entry, &self.owner_did, &self.author_did);
536 self.submit_record_with_summary(
537 Self::QAPP_MEDICATION,
538 packed.envelope,
539 "wellfair-medication:ui",
540 Some(packed.summary),
541 )?;
542 self.finalize_batch().ok();
543 self.list_health_records(1)?
544 .into_iter()
545 .next()
546 .ok_or_else(|| "medication committed but journal empty".into())
547 }
548
549 pub fn record_administration(
550 &mut self,
551 medication_id: &str,
552 medication_name: &str,
553 status: AdministrationStatus,
554 notes: Option<String>,
555 ) -> Result<JournalEntry, String> {
556 let now = Self::now_unix() as u32;
557 let admin = MedicationAdministration {
558 id: medication::new_administration_id(medication_id, now),
559 medication_id: medication_id.to_string(),
560 medication_name: medication_name.to_string(),
561 status,
562 administered_at_unix: now,
563 notes,
564 };
565 let packed = medication::administration_envelope(&admin, &self.owner_did, &self.author_did);
566 self.submit_record_with_summary(
567 Self::QAPP_MEDICATION,
568 packed.envelope,
569 "wellfair-medication:ui",
570 Some(packed.summary),
571 )?;
572 self.finalize_batch().ok();
573 self.list_health_records(1)?
574 .into_iter()
575 .next()
576 .ok_or_else(|| "administration committed but journal empty".into())
577 }
578
579 pub fn add_diet_entry(
580 &mut self,
581 description: &str,
582 meal_type: &str,
583 calories_kcal: Option<u32>,
584 ) -> Result<JournalEntry, String> {
585 let now = Self::now_unix() as u32;
586 let diet = DietEntry {
587 id: medication::new_diet_id(description, now),
588 description: description.to_string(),
589 meal_type: meal_type.to_string(),
590 calories_kcal,
591 logged_at_unix: now,
592 };
593 let packed = medication::diet_envelope(&diet, &self.owner_did, &self.author_did);
594 self.submit_record_with_summary(
595 Self::QAPP_MEDICATION,
596 packed.envelope,
597 "wellfair-medication:ui",
598 Some(packed.summary),
599 )?;
600 self.finalize_batch().ok();
601 self.list_health_records(1)?
602 .into_iter()
603 .next()
604 .ok_or_else(|| "diet entry committed but journal empty".into())
605 }
606
607 pub fn list_journal_by_kind(
608 &self,
609 kind: &str,
610 limit: usize,
611 ) -> Result<Vec<JournalEntry>, String> {
612 Ok(self
613 .list_health_records(limit)?
614 .into_iter()
615 .filter(|e| e.kind == kind)
616 .collect())
617 }
618
619 pub fn compute_anatomy_view(
625 &self,
626 lens: &str,
627 convergence_threshold: usize,
628 ) -> Result<super::super::anatomy_view::AnatomyViewReport, String> {
629 let conditions = self.list_journal_by_kind("condition", 256)?;
630 let medications = self.list_journal_by_kind("medication", 256)?;
631 let diet = self.list_journal_by_kind("diet", 256)?;
632 let state = self.get_physiological_state();
633 Ok(super::super::anatomy_view::build_report_from_journal(
634 &conditions,
635 &medications,
636 &diet,
637 super::super::anatomy_view::parse_lens(lens),
638 convergence_threshold,
639 state,
640 ))
641 }
642
643 pub fn compute_body_scene(
649 &self,
650 azimuth_deg: f64,
651 elevation_deg: f64,
652 ) -> Result<webizen_render::scene_contract::RenderScene, String> {
653 let report = self.compute_anatomy_view("person", 2)?;
654 let fit = self.body_fit();
655 Ok(super::super::anatomy_render::body_scene_with_fit(
656 &report,
657 azimuth_deg,
658 elevation_deg,
659 &fit,
660 ))
661 }
662}