feat(host): unify typed failure results

Represent denied, timeout, unavailable, and error host outcomes with one HostFailure result shape, update the browser adapter, and keep Workout host recovery flowing through app code before UI effects.

req: host/002

req: host/005

req: local/003
This commit is contained in:
slhx agent
2026-06-12 07:58:44 +02:00
parent cc97330bd1
commit 21100aafdf
4 changed files with 109 additions and 27 deletions
+28 -12
View File
@@ -3,8 +3,8 @@ use hemx::{Html, IntoEffect};
use hemx_axum::Form;
use hemx_host::{
browser_pwa_host_profile, native_shell_host_profile, Capability, CapabilityManifest,
CapabilityShape, CapabilityUse, HapticPattern, HostCall, HostCallId, HostEvent,
SharePayload as HostShareData,
CapabilityShape, CapabilityUse, HapticPattern, HostCall, HostCallId, HostEvent, HostFailure,
HostFailureKind, SharePayload as HostShareData,
};
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
@@ -942,9 +942,10 @@ pub fn record_share_denied(app: AppState) -> impl IntoEffect {
// req: host/002 req: host/005 req: local/003
apply_host_event(
state,
HostEvent::PermissionDenied {
capability: Capability::Share,
},
HostEvent::Failed(
HostFailure::new(HostFailureKind::PermissionDenied, "share permission denied")
.with_capability(Capability::Share),
),
);
effects(state, "Share permission denial returned through app code")
})
@@ -955,10 +956,11 @@ pub fn record_host_timeout(app: AppState) -> impl IntoEffect {
// req: host/002 req: host/005 req: local/003
apply_host_event(
state,
HostEvent::Failed {
id: Some(HostCallId::new("workout-export")),
message: "share timed out".into(),
},
HostEvent::Failed(
HostFailure::new(HostFailureKind::Timeout, "share timed out")
.with_id(HostCallId::new("workout-export"))
.with_capability(Capability::Share),
),
);
effects(state, "Host timeout returned through app code")
})
@@ -999,6 +1001,15 @@ pub fn record_native_haptic_ack(app: AppState) -> impl IntoEffect {
})
}
fn host_failure_label(kind: HostFailureKind) -> &'static str {
match kind {
HostFailureKind::PermissionDenied => "permission denied",
HostFailureKind::Timeout => "timeout",
HostFailureKind::Unavailable => "unavailable",
HostFailureKind::Error => "error",
}
}
fn apply_host_event(state: &mut WorkoutState, event: HostEvent) {
match event {
HostEvent::ShareCompleted {
@@ -1013,14 +1024,19 @@ fn apply_host_event(state: &mut WorkoutState, event: HostEvent) {
state.host_status =
"Share cancelled; local event log unchanged. Try Share export when ready.".into();
}
HostEvent::PermissionDenied { capability } if capability == Capability::Share => {
HostEvent::Failed(failure)
if failure.kind == HostFailureKind::PermissionDenied
&& failure.capability == Some(Capability::Share) =>
{
state.host_status =
"Share permission denied. Local export text is still ready; try again or copy it."
.into();
}
HostEvent::Failed { message, .. } => {
HostEvent::Failed(failure) => {
state.host_status = format!(
"Host unavailable: {message}. Keep the local export below and try Share export again."
"Host {}: {}. Keep the local export below and try Share export again.",
host_failure_label(failure.kind),
failure.message
);
}
HostEvent::Acknowledged { id } if id.0 == "workout-set-haptic" => {