refactor(saas): make navigation and status idiomatic

Use a real enhanceable Settings URL, remove response-simulated navigation, and replace the one-shot public SSE endpoint with an initial canonical event plus ongoing server-owned updates. Keep a bounded one-event probe for production-reference tests.

req: examples/014

req: nav/001

req: nav/002

req: nav/004

req: push/003
This commit is contained in:
slhx agent
2026-07-22 18:18:48 +02:00
parent c4a6edfed1
commit 276f40ee59
7 changed files with 59 additions and 73 deletions
+1
View File
@@ -58,6 +58,7 @@ Keep it stable. Prefer pointers to canonical sources over copied structure, file
- Use the same Workout command surface for tests, production build, and mobile release: `cargo run -p hemx-xtask -- workout test`, `cargo run -p hemx-xtask -- workout build`, `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-release`, and `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-verify`; Android/iOS SDKs, store submission targets, and signing remain external blockers, not repo-owned secrets, and do not imply a broad `hemx-mobile` framework. req: examples/006 req: examples/011 req: examples/013 - Use the same Workout command surface for tests, production build, and mobile release: `cargo run -p hemx-xtask -- workout test`, `cargo run -p hemx-xtask -- workout build`, `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-release`, and `HEMX_WORKOUT_ORIGIN=https://workout.example.com cargo run -p hemx-xtask -- workout mobile-verify`; Android/iOS SDKs, store submission targets, and signing remain external blockers, not repo-owned secrets, and do not imply a broad `hemx-mobile` framework. req: examples/006 req: examples/011 req: examples/013
- hemx core stays small: effects, typed ids, registries, and wire schema only; keep features in core only when they fit typed resources plus the closed EffectBatch op set, and treat DOM details as runtime lowering. Workspace crates stay separated, stable-Rust-compatible, and free of kitchen-sink boundaries; new primitives must delete special cases. Public identifiers should flow through typed wrappers over internal `ResourceId`/`ResourceRef`, not special-case opcodes. Wire output lowers symbolic authoring names to compact metadata, the versioned canonical hemx `EffectBatch` codec, postcard surface facts, and form-encoded public requests—not JSON. ABI/schema versions and build fingerprints must guard runtime/server compatibility. v0 scope is the checked hypermedia core plus page/runtime/wire/diagnostic/test/axum proof, not optional sync/wasm/query/auth/router breadth. req: v0_scope/001 req: v0_scope/002 req: v0_scope/005 req: laws/001 req: invariant/001 req: invariant/005 req: typed_id/001 req: typed_id/003 req: effect_algebra/001 req: effect_algebra/006 req: wire/001 req: wire/002 req: wire/003 req: wire/004 req: wire/005 req: wire/006 req: abi/001 req: abi/002 req: abi/003 req: abi/004 req: abi/005 req: misc/001 req: misc/002 req: misc/003 req: misc/004 req: misc/005 req: misc/006 req: misc/007 req: misc/008 req: misc/009 req: misc/010 - hemx core stays small: effects, typed ids, registries, and wire schema only; keep features in core only when they fit typed resources plus the closed EffectBatch op set, and treat DOM details as runtime lowering. Workspace crates stay separated, stable-Rust-compatible, and free of kitchen-sink boundaries; new primitives must delete special cases. Public identifiers should flow through typed wrappers over internal `ResourceId`/`ResourceRef`, not special-case opcodes. Wire output lowers symbolic authoring names to compact metadata, the versioned canonical hemx `EffectBatch` codec, postcard surface facts, and form-encoded public requests—not JSON. ABI/schema versions and build fingerprints must guard runtime/server compatibility. v0 scope is the checked hypermedia core plus page/runtime/wire/diagnostic/test/axum proof, not optional sync/wasm/query/auth/router breadth. req: v0_scope/001 req: v0_scope/002 req: v0_scope/005 req: laws/001 req: invariant/001 req: invariant/005 req: typed_id/001 req: typed_id/003 req: effect_algebra/001 req: effect_algebra/006 req: wire/001 req: wire/002 req: wire/003 req: wire/004 req: wire/005 req: wire/006 req: abi/001 req: abi/002 req: abi/003 req: abi/004 req: abi/005 req: misc/001 req: misc/002 req: misc/003 req: misc/004 req: misc/005 req: misc/006 req: misc/007 req: misc/008 req: misc/009 req: misc/010
- Routing, auth, sessions, transport, transitions, sync, async data helpers, multipart parsing/uploads, and storage belong in integration/user crates; hemx-axum preserves normal HTTP auth, credentials, CSRF, multipart/browser fallback, and progressive-enhancement semantics rather than defining policy in core. Sync is optional integration state reconciliation over push/transport, not core. req: auth/001 req: auth/002 req: auth/003 req: auth/004 req: auth/005 req: async_data/001 req: async_data/002 req: async_data/003 req: multipart/001 req: multipart/002 req: multipart/003 req: sync/001 req: sync/008 - Routing, auth, sessions, transport, transitions, sync, async data helpers, multipart parsing/uploads, and storage belong in integration/user crates; hemx-axum preserves normal HTTP auth, credentials, CSRF, multipart/browser fallback, and progressive-enhancement semantics rather than defining policy in core. Sync is optional integration state reconciliation over push/transport, not core. req: auth/001 req: auth/002 req: auth/003 req: auth/004 req: auth/005 req: async_data/001 req: async_data/002 req: async_data/003 req: multipart/001 req: multipart/002 req: multipart/003 req: sync/001 req: sync/008
- The SaaS production reference must use real links/URLs for navigation and an ongoing server-owned canonical SSE stream; bounded one-event behavior is a test probe, not the public transport contract. req: nav/001 req: nav/002 req: push/003 req: examples/014
- Public examples and beginner APIs should use templates plus Rust, generated component APIs, resources, view wrappers, render/page helpers, `#[hemx::app]`, plain `#[hemx::handler]` functions, and `IntoEffect`, not atoms, raw ids, selectors, wire formats, runtime opcodes, manual registries, `$OUT_DIR` includes, raw render/lower calls, raw HTML construction, imperative DOM mutation, or raw effect constructors; keep advanced layers out of starters. req: canonical_authoring/001 req: canonical_authoring/004 req: canonical_authoring/006 req: canonical_authoring/010 req: canonical_authoring/015 req: invariant/003 req: dx/001 req: dx/002 req: dx/010 req: component/003 req: component/004 req: component/005 req: view/001 req: view/002 req: view/003 req: html_safety/001 req: html_safety/003 req: html_safety/005 req: public_api/001 req: public_api/002 req: public_api/003 req: public_api/005 req: public_api/006 req: progressive_disclosure/001 req: progressive_disclosure/002 req: progressive_disclosure/003 req: derive_app/001 req: derive_app/002 req: derive_handler/001 req: derive_handler/002 req: derive_handler/003 req: derive_handler/004 req: derive_handler/005 - Public examples and beginner APIs should use templates plus Rust, generated component APIs, resources, view wrappers, render/page helpers, `#[hemx::app]`, plain `#[hemx::handler]` functions, and `IntoEffect`, not atoms, raw ids, selectors, wire formats, runtime opcodes, manual registries, `$OUT_DIR` includes, raw render/lower calls, raw HTML construction, imperative DOM mutation, or raw effect constructors; keep advanced layers out of starters. req: canonical_authoring/001 req: canonical_authoring/004 req: canonical_authoring/006 req: canonical_authoring/010 req: canonical_authoring/015 req: invariant/003 req: dx/001 req: dx/002 req: dx/010 req: component/003 req: component/004 req: component/005 req: view/001 req: view/002 req: view/003 req: html_safety/001 req: html_safety/003 req: html_safety/005 req: public_api/001 req: public_api/002 req: public_api/003 req: public_api/005 req: public_api/006 req: progressive_disclosure/001 req: progressive_disclosure/002 req: progressive_disclosure/003 req: derive_app/001 req: derive_app/002 req: derive_handler/001 req: derive_handler/002 req: derive_handler/003 req: derive_handler/004 req: derive_handler/005
- Typed partial swaps should stay expressed as generated target plus rendered partial plus swap kind, not selector-driven rerendering or response-side selector retargeting; HTTP, page navigation, push, and island behavior adapt around that loop, and docs should layer new primitives progressively. Navigation is an effect/page-swap concern, not a core router framework; enhanced links and GET forms preserve real URL/history semantics so page state stays reloadable/shareable without a client state graph. Push streams carry canonical versioned hemx `EffectBatch` bytes over server-owned SSE/WebSocket transport and keep `data-hemx-sse` root-scoped/same-origin by default. Preserve keyed/optional scope identity for addressable loop nodes, reconcile filtered keyed collections without clearing retained rows, prefer generated keyed-slot helpers over low-level keyed calls, and route self/row-update diagnostics toward local `data-hemx-slot`/`h-key` targets. req: canonical_authoring/002 req: canonical_authoring/014 req: modes/001 req: scope/001 req: list/001 req: list/002 req: list/003 req: list/004 req: list/005 req: list/006 req: nav/001 req: nav/002 req: nav/003 req: nav/004 req: nav/005 req: push/001 req: push/002 req: push/003 req: push/004 req: push/005 req: push/006 req: push/007 req: progressive_disclosure/004 req: page_swap/001 req: page_swap/002 req: page_swap/003 req: locality/001 req: locality/002 req: target_policy/001 req: target_policy/002 - Typed partial swaps should stay expressed as generated target plus rendered partial plus swap kind, not selector-driven rerendering or response-side selector retargeting; HTTP, page navigation, push, and island behavior adapt around that loop, and docs should layer new primitives progressively. Navigation is an effect/page-swap concern, not a core router framework; enhanced links and GET forms preserve real URL/history semantics so page state stays reloadable/shareable without a client state graph. Push streams carry canonical versioned hemx `EffectBatch` bytes over server-owned SSE/WebSocket transport and keep `data-hemx-sse` root-scoped/same-origin by default. Preserve keyed/optional scope identity for addressable loop nodes, reconcile filtered keyed collections without clearing retained rows, prefer generated keyed-slot helpers over low-level keyed calls, and route self/row-update diagnostics toward local `data-hemx-slot`/`h-key` targets. req: canonical_authoring/002 req: canonical_authoring/014 req: modes/001 req: scope/001 req: list/001 req: list/002 req: list/003 req: list/004 req: list/005 req: list/006 req: nav/001 req: nav/002 req: nav/003 req: nav/004 req: nav/005 req: push/001 req: push/002 req: push/003 req: push/004 req: push/005 req: push/006 req: push/007 req: progressive_disclosure/004 req: page_swap/001 req: page_swap/002 req: page_swap/003 req: locality/001 req: locality/002 req: target_policy/001 req: target_policy/002
- `examples/html_examples` is the copy-paste HTML pattern gallery for htmx-style examples; keep exact htmx URL slugs visible while translating behavior to boring `.heml`, generated resources, and server-owned Rust state, not HTMX syntax, selector targeting, or user-authored browser JavaScript. Shared runtime loading and declarative `data-hemx-*` are allowed. Boost containers enhance same-origin descendants only and preserve native external/download/new-tab behavior. req: htmx_equivalents/001 req: htmx_equivalents/003 req: htmx_equivalents/005 req: examples/005 req: examples/007 req: examples/012 req: page_swap/007 req: page_swap/008 - `examples/html_examples` is the copy-paste HTML pattern gallery for htmx-style examples; keep exact htmx URL slugs visible while translating behavior to boring `.heml`, generated resources, and server-owned Rust state, not HTMX syntax, selector targeting, or user-authored browser JavaScript. Shared runtime loading and declarative `data-hemx-*` are allowed. Boost containers enhance same-origin descendants only and preserve native external/download/new-tab behavior. req: htmx_equivalents/001 req: htmx_equivalents/003 req: htmx_equivalents/005 req: examples/005 req: examples/007 req: examples/012 req: page_swap/007 req: page_swap/008
+17 -37
View File
@@ -1,41 +1,21 @@
# Active frontier — v1 production readiness # Active frontier — peak idiomatic examples
**Parent outcome** **Parent ID:** `examples-idiom/001`
- **User value:** Rust teams can adopt hemx v1 knowing its public contracts, failure recovery, compatibility, dependency policy, and release proof are explicit and reproducible. - **User value:** A newcomer can move from first app to production reference and advanced integration while seeing one coherent hemx model: plain `.heml`, generated resources, typed Rust handlers/effects, real links/forms/URLs, server-owned truth, native fallback, and explicit leaf adapters.
- **State:** In progress — the product and representative runtime/browser/performance/security paths are implemented, but v1 production readiness is not yet closed. - **State:** Ready — the canonical starter and Workout exemplar are already strong; bounded teaching leaks remain in the HTML gallery, client-local example, SaaS reference, and advanced Kanban boundary.
- **Blocked by:** the repository owner must approve a distribution license policy before the legal release gate can close; this does not block mutation hardening or local release verification. - **Non-goals:** no new framework primitives, client router, VDOM, selector authoring API, reactive expression language, global client store, CSS framework/design system, generalized asset pipeline, visual redesign, or feature expansion. Do not churn `examples/workout` or `examples/techdemo` without a concrete failing contract.
- **Non-goals:** no new framework surface, second wire format, WASM runtime decoder, deployment, publication, signing, store submission, or feature expansion. - **Build:**
- **Proof:** every child below is done; `docs/v1-readiness.md` reports GO without contradicting `REQUIREMENTS.md`; the tree is committed and clean. - **`examples-idiom/002` — Done: the SaaS reference tells the production truth.** Settings is a real `/settings` link enhanced by the existing page-swap runtime, direct `/settings` renders the fallback page, and `/events` now sends an initial canonical batch followed by ongoing server-owned status updates; `?once` remains only as a bounded production-reference probe. The removed handler no longer simulates navigation with response effects. Raw selector/form construction remains confined to test adapters because generated handles are the asserted boundary, not an app authoring API. req: canonical_authoring/001 req: nav/001 req: nav/002 req: nav/004 req: push/003 req: examples/003 req: examples/004 req: examples/014
- **`examples-idiom/003` — Make the HTML pattern gallery mechanically copyable.** In `examples/html_examples/src/main.rs`, generated `gallery` resources, and nearby templates/tests, express the dependent-select flow through typed generated values/partials instead of duplicated string-to-option mapping, and remove request fields discarded only to satisfy example plumbing where the generated handler contract permits it. Preserve each visible htmx slug, native form semantics, server-owned state, inserted-content behavior, and no-reload smoke. req: htmx_equivalents/001 req: htmx_equivalents/003 req: htmx_equivalents/005 req: canonical_authoring/001 req: form/004
- **`examples-idiom/004` — Make client-local code visibly a leaf adapter.** In `examples/client_local/src/lib.rs` and its `.heml` surface, project the generated client event/state into a tiny typed counter-domain input/output instead of rendering raw event kind and encoded state as the example's product value. Keep the ordinary `#[hemx::handler(client)]` shape, generated event/state boundary, native event semantics, and no durable client state graph. req: client_local/001 req: client_local/003 req: client_local/004 req: canonical_authoring/017
- **`examples-idiom/005` — Separate the advanced Kanban adapter from ordinary hemx app code.** Move the cohesive sync/presence/session/storage transport responsibility from `examples/kanban/src/main.rs` behind one clearly named local integration module with a small route/state contract; keep board templates and ordinary handlers nearby and unchanged where possible. Update `examples/kanban/README.md` to label the fixture as the advanced local/offline/sync north-star, route beginners to the starter/Workout/gallery first, and name legacy `/sync-demo`/`sync.js` as a compatibility probe rather than recommended authoring. Preserve replay, export, deletion, reconnection, auth, and multiplayer browser proof. req: state/001 req: state/002 req: local/001 req: local/002 req: milestone/001 req: sync/001 req: sync/008
- **`examples-idiom/006` — Publish and enforce the example ladder.** In `README.md`, example READMEs, and the nearest existing xtask/example checks, identify `app new`/`examples/v0` as the first canonical app, `html_examples` as the pattern gallery, Workout as the product exemplar, SaaS as the production integration reference, `client_local` as the narrow leaf-adapter proof, Techdemo as exhaustive verification, and Kanban as advanced north-star integration. Add the smallest repository-owned guard that fails when beginner/reference authoring regresses to raw IDs, selectors, raw wire/effect constructors, `$OUT_DIR` includes, or app-authored DOM mutation; keep legitimate advanced/test adapters scoped rather than banning tokens globally. req: examples/001 req: examples/003 req: examples/004 req: examples/005 req: examples/007 req: examples/008 req: examples/009 req: canonical_authoring/001 req: progressive_disclosure/001 req: progressive_disclosure/002 req: progressive_disclosure/003
- **Blocked by:** none. The separate v1 legal release gate remains blocked on the owner license decision but does not block example work.
- **Proof:** each slice must make its user path observable, preserve the named native/recovery path, and pass its focused package/browser proof. Parent closure requires `cargo run -p hemx-xtask -- html-examples-smoke`, `cargo run -p hemx-xtask -- workout test`, focused SaaS/client-local/Kanban package tests, `cargo run -p hemx-xtask -- test`, `cargo check --workspace`, `cargo fmt --check`, `redgate list`, `redgate refs`, and a clean diff. Frustration signals to reject: a beginner must author selectors/raw IDs/wire ops; navigation loses URL/history; JavaScript becomes durable truth; an example claims live/recovery behavior with a one-shot stub; or advanced sync machinery appears to be the default app model.
## 1. Checkpoint the current robustness slice ## Blocked release decision retained
- [x] **State:** Done — committed as the current robustness slice after full workspace, focused compile-fail, requirements-reference, formatting, and check proof. - [ ] **State:** Needs decision — choose the repository distribution license and approved third-party SPDX set, then add root license file(s), workspace package metadata, `deny.toml`, and rerun the release matrix.
- **User value:** users receive one explicit typed `EffectBatch` codec and stronger fail-closed form, sync, host, WASM, macro, test-harness, and generated-contract boundaries without parallel magic. - **Blocked by:** owner/legal authority. Current third-party set includes Apache-2.0, Apache-2.0 WITH LLVM-exception, BSD-3-Clause, BSL-1.0, MIT, Unicode-3.0, and Unlicense; all 20 workspace packages currently lack license metadata.
- **Build:** review the current diff as one coherent behavior/requirements slice; retain the canonical non-WASM `EffectBatch` codec and remove the duplicate postcard batch API; keep only mutation-driven tests and simplifications that prove public behavior; run the full local test authority; commit the slice. - **Proof:** `cargo deny check advisories sources licenses` and the full `docs/v1-readiness.md` matrix pass, then readiness changes from NO-GO to GO without publication or deployment.
- **Blocked by:** none.
- **Proof:** `cargo run -p hemx-xtask -- test`, `cargo fmt --check`, `cargo check --workspace`, `redgate list`, and `redgate refs` pass; `git diff --check` is clean; the focused mutest receipts for the effect codec, `hemx`, `hemx-host`, `hemx-js`, `hemx-wasm`, `hemx-sync`, `hemx-sync-macros`, `hemx-test` public helpers, and `hemx-axum` form/SSE codecs contain no missed mutants; one Conventional Commit records the requirement IDs.
## 2. Make mutation testing a reproducible release gate
- [x] **State:** Done — the package-native capped xtask entry point rejects unknown packages and invalid shards, propagates mutest failure, and every mutation-applicable library/proc-macro package passes its complete gate or deterministic shard set with no unexplained survivor.
- **User value:** maintainers can run one bounded repository command and trust that meaningful Rust logic across every mutation-applicable library is either killed or explicitly justified.
- **Build:** add a capped `hemx-xtask` mutation command that invokes `/opt/repositories/mutest`/`mutest` through package-native test targets rather than the broken workspace-wide example path; enumerate only current mutation-applicable library/proc-macro packages; finish adversarial tests or simplify code until every survivor is classified; keep equivalent, invariant-only, and infrastructure-inapplicable classifications inspectable and minimal; document the exact local release command in the existing readiness surface.
- **Blocked by:** none. `hemx-lsp` is binary-only and deliberately outside the mutation-applicable library/proc-macro set elected by test/020; its package tests remain in workspace verification. The runner creates first-use shard output parents, accepts validated one-based shards, maps them to native zero-based shards, uses shard-specific output, grants compiler probes a 120-second floor, and preserves the unsharded gate. Browser-backed `hemx-wasm` runs one mutest worker without a parent jobserver, preventing nested Cargo/Firefox deadlock while preserving the detected Cargo budget; its four shards pass with 59 mutants (52 caught, 7 unviable) after replay projections were batched without changing ordered effect application, Firefox background work was disabled, and the explicit 64-command debug-browser replay budget was set to 250 ms. The complete `hemx` (30 mutants), `hemx-host` (76), `hemx-sync` (164), `hemx-sync-macros` (33), `hemx-axum` (470), `hemx-build` (1,304), and `hemx-derive` (480) gates or deterministic shard sets pass, alongside the previously clean `hemx-core`, `hemx-js`, and `hemx-test` gates.
- **Proof:** the new xtask mutation command exits zero within its documented bound, covers each applicable package, emits no unexplained missed mutant, and a deliberate adjacent mutation makes it fail. `cargo run -p hemx-xtask -- test` remains green. req: test/020 req: test/021
## 3. Elect and enforce the release license policy
- [ ] **State:** Needs decision
- **User value:** adopters can legally evaluate and redistribute hemx with a machine-checked dependency license boundary.
- **Build:** owner chooses the repository distribution license and accepted dependency licenses; add the corresponding root license file(s) and minimal `deny.toml`; classify workspace crates and the current dependency set; run strict license, advisory, and source checks; reject unknown/unlicensed dependencies rather than silently broadening policy.
- **Blocked by:** owner legal decision: choose the repository license and whether weak-copyleft dependencies are acceptable. Current dependency licenses observed by `cargo deny list` include Apache-2.0, Apache-2.0 WITH LLVM-exception, BSD-3-Clause, BSL-1.0, MIT, Unicode-3.0, Unlicense, plus unlicensed workspace packages because the repository has no elected license.
- **Proof:** `cargo deny check licenses advisories sources` exits zero from repository configuration; every allowed license is explicit; the repository license is visible at the root; adding a disallowed/unlicensed fixture fails the gate. req: security/007 req: v1_release/006
## 4. Issue the final v1 GO/NO-GO decision
- [ ] **State:** Blocked by slices 13
- **User value:** users get an honest release candidate whose documented support, recovery, accessibility, performance, examples, and operational behavior match what was actually proven.
- **Build:** rerun the complete local release matrix from `docs/v1-readiness.md`, including mutation and license gates; verify the pinned Rust/browser/WASM/Axum compatibility matrix and migration fixture; run browser, offline/reconnect, mixed-deploy recovery, accessibility, security, performance, docs, and canonical-example proofs; independently audit requirement-to-proof coverage and README/readiness contradictions; fix only release-blocking defects; mark GO only when no P0/P1 or unexplained gate failure remains.
- **Blocked by:** slices 13; publishing/deployment authority remains separate and is not required for a local GO decision.
- **Proof:** all commands in `docs/v1-readiness.md` pass from a clean checkout within documented bounds; `redgate list` and `redgate refs` pass; known installed `redgate health`/`lint` corpus-format warnings are either resolved or accurately documented rather than hidden; `docs/v1-readiness.md` says GO and names zero open release blockers; independent contradiction review agrees. req: v1_release/001 req: v1_release/002 req: v1_release/003 req: v1_release/004 req: v1_release/005 req: v1_release/006 req: v1_release/007 req: v1_release/008 req: v1_release/009 req: v1_release/010
+2
View File
@@ -852,6 +852,8 @@ what a valid business email is. [north_star]
013 Workout mobile commands must not add a broad `hemx-mobile` framework. [north_star] 013 Workout mobile commands must not add a broad `hemx-mobile` framework. [north_star]
014 The SaaS production reference uses real URL navigation and an ongoing server-owned SSE status stream; its bounded test probe emits the same canonical hemx event shape. [north_star]
--- ---
## ms ## ms
+4 -26
View File
@@ -2,7 +2,7 @@
pub mod ui {} pub mod ui {}
use hemplate::Hemplate; use hemplate::Hemplate;
use hemx::{push, Html, IntoEffect}; use hemx::{Html, IntoEffect};
use hemx_axum::{ use hemx_axum::{
interactions, runtime_js_path, Form, HandlerErrorContext, HandlerFailure, IntoHandlerFailure, interactions, runtime_js_path, Form, HandlerErrorContext, HandlerFailure, IntoHandlerFailure,
Registry, State, Registry, State,
@@ -565,15 +565,6 @@ mod dashboard_handlers {
dashboard::live_status.set(format!("{total} projects persisted locally")), dashboard::live_status.set(format!("{total} projects persisted locally")),
)) ))
} }
#[hemx::handler]
pub async fn open_settings(State(_ctx): State<AppContext>) -> impl IntoEffect {
(
dashboard::page_panel.put(&SettingsPage::production_boundaries()),
dashboard::nav.set("Settings"),
push("/settings"),
)
}
} }
pub fn live_status(projects: usize) -> impl IntoEffect { pub fn live_status(projects: usize) -> impl IntoEffect {
@@ -740,23 +731,10 @@ mod tests {
assert!(created.updates_text(dashboard::live_status)); assert!(created.updates_text(dashboard::live_status));
} }
#[tokio::test] #[test]
async fn page_swap_and_push_shape_use_generated_targets() { fn live_status_uses_the_generated_dashboard_target() {
// req: page_swap/002 req: push/003 req: examples/001 // req: push/003 req: examples/014
let ctx = AppContext::demo(); let ctx = AppContext::demo();
let settings = inspect_batch(
InteractionRequest::from(form(dashboard::open_settings, &[]))
.dispatch_async(registry(ctx.clone()))
.await
.unwrap()
.batch,
);
assert!(
settings.updates_html_containing(dashboard::page_panel, "explicit app integrations")
);
assert!(settings.updates_text(dashboard::nav));
assert!(settings.pushes_to("/settings"));
let heartbeat = inspect(live_status(ctx.projects().len())); let heartbeat = inspect(live_status(ctx.projects().len()));
assert!(heartbeat.updates_text(dashboard::live_status)); assert!(heartbeat.updates_text(dashboard::live_status));
assert!(heartbeat.payload_contains("heartbeat")); assert!(heartbeat.payload_contains("heartbeat"));
+15 -9
View File
@@ -5,14 +5,14 @@ use axum::middleware::{self, Next};
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use axum::routing::{get, post}; use axum::routing::{get, post};
use axum::Router; use axum::Router;
use futures_util::stream; use futures_util::{stream, StreamExt};
use hemx::IntoEffect; use hemx::IntoEffect;
use hemx_axum::{runtime_js, runtime_js_path, sse, EffectResponse, InteractionRequest}; use hemx_axum::{runtime_js, runtime_js_path, sse, EffectResponse, InteractionRequest};
use hemx_saas_example::{home_page, live_status, registry, settings_page, ui, AppContext}; use hemx_saas_example::{home_page, live_status, registry, settings_page, ui, AppContext};
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::convert::Infallible; use std::convert::Infallible;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Instant; use std::time::{Duration, Instant};
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
@@ -81,16 +81,22 @@ async fn events(
Query(params): Query<BTreeMap<String, String>>, Query(params): Query<BTreeMap<String, String>>,
State(ctx): State<AppContext>, State(ctx): State<AppContext>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let count = ctx.projects().len(); // The production reference exposes an ongoing server-owned stream; `once`
// keeps a bounded probe for package tests without changing the public path.
// req: examples/014
let event = |ctx: &AppContext| {
Ok::<_, Infallible>(live_status(ctx.projects().len()).into_batch(ui::BUILD_FINGERPRINT))
};
let initial = stream::once(std::future::ready(event(&ctx)));
if params.contains_key("once") { if params.contains_key("once") {
return sse(stream::iter([Ok::<_, Infallible>( return sse(initial.left_stream());
live_status(count).into_batch(ui::BUILD_FINGERPRINT),
)]));
} }
sse(stream::iter([Ok::<_, Infallible>( let updates = stream::unfold(ctx, move |ctx| async move {
live_status(count).into_batch(ui::BUILD_FINGERPRINT), tokio::time::sleep(Duration::from_secs(15)).await;
)])) Some((event(&ctx), ctx))
});
sse(initial.chain(updates).right_stream())
} }
// req: auth/001 req: auth/002 req: auth/004 // req: auth/001 req: auth/002 req: auth/004
+1 -1
View File
@@ -7,7 +7,7 @@
<nav class="tabs" data-hemx-slot="nav"> <nav class="tabs" data-hemx-slot="nav">
<a href="/" data-hemx-nav="">Projects</a> <a href="/" data-hemx-nav="">Projects</a>
<button type="button" data-hemx-handle="open_settings">Settings</button> <a href="/settings" data-hemx-nav>Settings</a>
</nav> </nav>
<section class="panel" data-hemx-slot="page_panel"> <section class="panel" data-hemx-slot="page_panel">
@@ -125,6 +125,25 @@ fn authenticated_project_mutation_is_atomic_and_survives_restart() {
); );
assert!(!home.contains("<script>")); assert!(!home.contains("<script>"));
assert!(!home.contains("javascript:")); assert!(!home.contains("javascript:"));
assert!(
home.contains("href=\"/settings\" data-hemx-nav"),
"settings must remain a real, enhanceable link"
);
let settings = request(&address, "GET", "/settings", &[], "");
assert!(settings.starts_with("HTTP/1.1 200"), "{settings}");
assert!(settings.contains("<h2>Settings</h2>"), "{settings}");
assert!(settings.contains("explicit app integrations"), "{settings}");
let events = request(&address, "GET", "/events?once=1", &[], "");
assert!(events.starts_with("HTTP/1.1 200"), "{events}");
assert_eq!(
response_header(&events, "content-type"),
"text/event-stream"
);
assert!(events.contains("event: hemx"), "{events}");
assert!(events.contains("data:"), "{events}");
// test req: nav/001 req: nav/002 req: push/003 req: examples/014
let live = request(&address, "GET", "/health/live", &[], ""); let live = request(&address, "GET", "/health/live", &[], "");
assert!(live.starts_with("HTTP/1.1 200"), "{live}"); assert!(live.starts_with("HTTP/1.1 200"), "{live}");
assert!(live.contains("{\"status\":\"live\"}"), "{live}"); assert!(live.contains("{\"status\":\"live\"}"), "{live}");