feat(host): wire techdemo host capability flow

Route browser/PWA share and native-shell haptic host results through techdemo app code before returning normal hemx effects; serve the tiny browser host adapter as an app asset.

req: host/001

req: host/002

req: host/004

req: host/005

req: examples/001
This commit is contained in:
slhx agent
2026-06-11 19:43:50 +02:00
parent 2cc6c85e37
commit 3807306158
7 changed files with 243 additions and 2 deletions
Generated
+1
View File
@@ -573,6 +573,7 @@ dependencies = [
"hemx",
"hemx-axum",
"hemx-build",
"hemx-host",
"hemx-test",
"scraper",
"thirtyfour",
+1
View File
@@ -13,6 +13,7 @@ futures-util = "0.3"
hemplate = { path = "../../../hemplate/hemplate" }
hemx = { path = "../../hemx" }
hemx-axum = { path = "../../hemx-axum" }
hemx-host = { path = "../../hemx-host" }
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "time"] }
[dev-dependencies]
+179 -2
View File
@@ -10,9 +10,16 @@ use hemx_axum::{
interactions, runtime_js, runtime_js_path, sse, DispatchRegistry, DispatchRejection,
EffectResponse, InteractionRequest, PageRequest,
};
use hemx_host::{
browser_pwa_host_profile, native_shell_host_profile, Capability, CapabilityManifest,
CapabilityShape, CapabilityUse, HapticPattern, HostCall, HostCallId, HostEvent, SharePayload,
BROWSER_HOST_JS,
};
use hemx_techdemo::ui;
use hemx_techdemo::ui::control_center::{self as control, classes};
use hemx_techdemo::ui::{issue_card as card_control, issue_lane as lane_control};
use hemx_techdemo::ui::{
host_panel as host_control, issue_card as card_control, issue_lane as lane_control,
};
use std::collections::{BTreeMap, VecDeque};
use std::convert::Infallible;
use std::net::SocketAddr;
@@ -25,6 +32,7 @@ const LANES: [(&str, &str, &str); 3] = [
("product", "Product", "Native UX, zero app JS"),
];
const ISLAND_ORBIT: EventName = EventName::new("hemx:island-orbit");
const HOST_CALL: EventName = EventName::new("hemx:host-call");
#[derive(Clone)]
struct WorkItem {
@@ -67,6 +75,7 @@ struct DemoState {
activity: VecDeque<String>,
spotlight: String,
selected_id: Option<u64>,
host_status: String,
}
impl Default for DemoState {
@@ -99,6 +108,7 @@ impl Default for DemoState {
activity: VecDeque::new(),
spotlight: "No selectors. Generated resources address every target.".into(),
selected_id: Some(2),
host_status: "Host calls are typed facts until app code accepts a result.".into(),
};
state.log("Demo booted from server-rendered HTML");
state.log("Runtime attached one delegated listener per root");
@@ -151,6 +161,7 @@ struct ControlCenter {
board: Html,
inspector: Html,
activity: Html,
host: Html,
island_snapshot: String,
}
@@ -192,6 +203,13 @@ struct ActivityFeed {
items: Vec<String>,
}
#[derive(Hemplate)]
#[hemplate = "partials"]
struct HostPanel {
status: String,
boundary: &'static str,
}
#[derive(Hemplate)]
#[hemplate = "partials"]
struct ArchitectureActivity;
@@ -230,6 +248,7 @@ async fn main() {
.route(runtime_js_path(), get(runtime))
.route("/app.css", get(app_css))
.route("/control_center.css", get(control_center_css))
.route("/hemx-browser-host.js", get(browser_host_js))
.route("/island.js", get(island_js))
.with_state(state);
@@ -258,6 +277,7 @@ async fn architecture(request: PageRequest) -> impl IntoResponse {
board: architecture_board(),
inspector: architecture_inspector(),
activity: architecture_activity(),
host: render_host_panel(&DemoState::default()),
island_snapshot: "2|3|21|architecture route · same opaque island bridge".to_owned(),
});
request
@@ -288,6 +308,14 @@ async fn control_center_css() -> impl IntoResponse {
)
}
async fn browser_host_js() -> impl IntoResponse {
// req: host/001 req: host/002 req: host/005
(
[("content-type", "application/javascript; charset=utf-8")],
BROWSER_HOST_JS,
)
}
async fn island_js() -> impl IntoResponse {
(
[("content-type", "text/javascript; charset=utf-8")],
@@ -324,6 +352,72 @@ async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResp
sse(batches)
}
enum HostAppCommand {
BrowserShareCompleted,
NativeHapticAcknowledged,
}
fn browser_manifest() -> CapabilityManifest {
CapabilityManifest::new([CapabilityUse::new(
Capability::Share,
CapabilityShape::Request,
)])
}
fn native_manifest() -> CapabilityManifest {
CapabilityManifest::new([CapabilityUse::new(
Capability::Haptics,
CapabilityShape::Fire,
)])
}
fn browser_share_call() -> HostCall {
HostCall::Share {
id: HostCallId::new("browser-share-1"),
payload: SharePayload::text("hemx host capability demo"),
}
}
fn native_haptic_call() -> HostCall {
HostCall::Haptic {
id: HostCallId::new("native-haptic-tap"),
pattern: HapticPattern::Success,
}
}
fn host_event_to_command(event: HostEvent) -> Option<HostAppCommand> {
match event {
HostEvent::ShareCompleted {
completed: true, ..
} => Some(HostAppCommand::BrowserShareCompleted),
HostEvent::Acknowledged { id } if id.0 == "native-haptic-tap" => {
Some(HostAppCommand::NativeHapticAcknowledged)
}
_ => None,
}
}
fn apply_host_event(demo: &mut DemoState, event: HostEvent) {
// req: host/002 req: host/005
match host_event_to_command(event) {
Some(HostAppCommand::BrowserShareCompleted) => {
demo.host_status =
"Browser/PWA HostEvent became an app command before UI effects.".into();
demo.log("Browser share completed through app host pipeline");
}
Some(HostAppCommand::NativeHapticAcknowledged) => {
demo.host_status =
"Native-shell HostEvent became an app command before UI effects.".into();
demo.log("Native haptic acknowledgment accepted by app code");
}
None => {
demo.host_status =
"HostEvent ignored by app policy; no domain change was appended.".into();
demo.log("Ignored host event by app policy");
}
}
}
fn registry(shared: Arc<Shared>) -> impl DispatchRegistry {
interactions(ui::BUILD_FINGERPRINT)
.on(control::launch_work, {
@@ -419,6 +513,74 @@ fn registry(shared: Arc<Shared>) -> impl DispatchRegistry {
demo_effects(&demo, "Inspector focused")
}
})
.on(host_control::request_browser_share, {
let shared = shared.clone();
move |_| {
// req: host/001 req: host/004 req: examples/001
let mut demo = shared.demo.lock().unwrap();
let call = browser_share_call();
match browser_manifest().validate_call(&browser_pwa_host_profile(), &call) {
Ok(()) => {
demo.host_status = "Browser/PWA share request accepted; waiting for HostEvent from the host adapter.".into();
demo.log("Requested browser/PWA share through hemx-host");
(demo_effects(&demo, "Browser host call requested"), HOST_CALL.emit("browser-share-1"))
}
Err(error) => {
demo.host_status = format!("Browser/PWA host check failed: {error}");
(demo_effects(&demo, "Browser host check failed"), HOST_CALL.emit("browser-share-failed"))
}
}
}
})
.on(host_control::record_browser_share, {
let shared = shared.clone();
move |form| {
// req: host/002 req: host/005 req: examples/001
let completed = form.parse::<bool>("completed").unwrap_or(true);
let mut demo = shared.demo.lock().unwrap();
apply_host_event(
&mut demo,
HostEvent::ShareCompleted {
id: HostCallId::new("browser-share-1"),
completed,
},
);
demo_effects(&demo, "Browser host result accepted by app code")
}
})
.on(host_control::request_native_haptic, {
let shared = shared.clone();
move |_| {
// req: host/001 req: host/004 req: examples/001
let mut demo = shared.demo.lock().unwrap();
let call = native_haptic_call();
match native_manifest().validate_call(&native_shell_host_profile("ios-android-webview"), &call) {
Ok(()) => {
demo.host_status = "Native-shell haptic request accepted; waiting for host acknowledgment.".into();
demo.log("Requested native-shell haptic through hemx-host");
(demo_effects(&demo, "Native host call requested"), HOST_CALL.emit("native-haptic-tap"))
}
Err(error) => {
demo.host_status = format!("Native host check failed: {error}");
(demo_effects(&demo, "Native host check failed"), HOST_CALL.emit("native-haptic-failed"))
}
}
}
})
.on(host_control::record_native_haptic_ack, {
let shared = shared.clone();
move |_| {
// req: host/002 req: host/005 req: examples/001
let mut demo = shared.demo.lock().unwrap();
apply_host_event(
&mut demo,
HostEvent::Acknowledged {
id: HostCallId::new("native-haptic-tap"),
},
);
demo_effects(&demo, "Native host result accepted by app code")
}
})
.on(control::simulate_push, {
let shared = shared.clone();
move |_| {
@@ -464,6 +626,7 @@ fn demo_effects(demo: &DemoState, notice: &'static str) -> impl IntoEffect {
control::board.put(&board_view(demo)),
control::activity.put(&activity_view(demo)),
control::inspector.put(&inspector_view(demo)),
control::host_panel.put(&host_panel(demo)),
control::notice.text(notice),
control::launch_work_form.clear(),
ISLAND_ORBIT.emit(island_snapshot(demo)),
@@ -510,6 +673,7 @@ fn page_html(demo: &DemoState) -> Html {
board: ui::render(&board_view(demo)),
inspector: render_inspector(demo),
activity: render_activity(demo),
host: render_host_panel(demo),
island_snapshot: island_snapshot(demo),
})
}
@@ -536,7 +700,7 @@ fn hero_view(demo: &DemoState) -> HeroMetrics {
.count();
let impact: u64 = demo.work.iter().map(|item| item.impact as u64).sum();
HeroMetrics {
resource_count: 13,
resource_count: 18,
active_count: active,
shipped_count: shipped,
impact_score: impact,
@@ -592,6 +756,19 @@ fn render_activity(demo: &DemoState) -> Html {
ui::render(&activity_view(demo))
}
fn host_panel(demo: &DemoState) -> HostPanel {
// req: host/001 req: host/002 req: host/005
HostPanel {
status: demo.host_status.clone(),
boundary: "HostCall → HostEvent → app command → EffectBatch",
}
}
fn render_host_panel(demo: &DemoState) -> Html {
// req: host/001 req: host/002 req: host/005
ui::render(&host_panel(demo))
}
fn inspector_view(demo: &DemoState) -> InspectorPanel {
// req: html_safety/002 req: view/001
let selected = demo
@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>hemx Techdemo</title>
<script +src="self.runtime_src" defer></script>
<script src="/hemx-browser-host.js" defer></script>
<script src="/island.js" defer></script>
<link rel="stylesheet" href="/app.css">
<link rel="stylesheet" href="/control_center.css">
@@ -32,6 +32,7 @@
<button type="button" data-hemx-handle="simulate_push">Simulate server push</button>
<button type="button" data-hemx-handle="reset_demo">Reset demo</button>
</div>
<div data-hemx-slot="host_panel">{+= self.host =+}</div>
<p data-hemx-slot="notice" class="notice">Every control posts through a generated handle and receives typed updates.</p>
</aside>
@@ -0,0 +1,10 @@
<div class="host-panel">
<p><strong>Typed host boundary</strong><br>{+ self.status +}</p>
<div class="quick-actions">
<button type="button" data-hemx-handle="request_browser_share">Request browser/PWA share</button>
<button type="button" data-hemx-handle="record_browser_share" name="completed" value="true">Record browser share result</button>
<button type="button" data-hemx-handle="request_native_haptic">Request native haptic</button>
<button type="button" data-hemx-handle="record_native_haptic_ack">Record native haptic ack</button>
</div>
<code>{+ self.boundary +}</code>
</div>
+50
View File
@@ -1,5 +1,8 @@
use hemx_axum::runtime_js_path;
use hemx_techdemo::ui::control_center::{self as control, launch_work, reset_demo, simulate_push};
use hemx_techdemo::ui::host_panel::{
record_browser_share, record_native_haptic_ack, request_browser_share, request_native_haptic,
};
use hemx_techdemo::ui::issue_card::{advance_work, delete_work, spotlight_work};
use hemx_techdemo::ui::issue_lane::move_to_lane as move_to_lane_handle;
use hemx_techdemo::ui::BUILD_FINGERPRINT;
@@ -256,6 +259,53 @@ fn product_is_e2e_working_over_http() {
);
assert_emit(&push_batch, &island_event_name("orbit"), "activity rows");
let browser_host_request = post("/", &handle_form_body(request_browser_share, &[]));
assert_effect_response(&browser_host_request);
let browser_request_batch = browser_host_request.effects();
assert_payload_contains(&browser_request_batch, "Browser host call requested");
assert_payload_contains(
&browser_request_batch,
"Browser/PWA share request accepted; waiting for HostEvent",
);
assert_emit(&browser_request_batch, "hemx:host-call", "browser-share-1");
let browser_host_result = post(
"/",
&handle_form_body(record_browser_share, &[("completed", "true")]),
);
assert_effect_response(&browser_host_result);
let browser_result_batch = browser_host_result.effects();
assert_payload_contains(
&browser_result_batch,
"Browser/PWA HostEvent became an app command before UI effects.",
);
assert_payload_contains(
&browser_result_batch,
"Browser share completed through app host pipeline",
);
let native_host_request = post("/", &handle_form_body(request_native_haptic, &[]));
assert_effect_response(&native_host_request);
let native_request_batch = native_host_request.effects();
assert_payload_contains(&native_request_batch, "Native host call requested");
assert_payload_contains(
&native_request_batch,
"Native-shell haptic request accepted; waiting for host acknowledgment.",
);
assert_emit(&native_request_batch, "hemx:host-call", "native-haptic-tap");
let native_host_result = post("/", &handle_form_body(record_native_haptic_ack, &[]));
assert_effect_response(&native_host_result);
let native_result_batch = native_host_result.effects();
assert_payload_contains(
&native_result_batch,
"Native-shell HostEvent became an app command before UI effects.",
);
assert_payload_contains(
&native_result_batch,
"Native haptic acknowledgment accepted by app code",
);
let delete_missing = post("/", &handle_form_body(delete_work, &[("work_id", "999")]));
assert_effect_response(&delete_missing);
let delete_missing_batch = delete_missing.effects();