feat(workout): make finish export a product state

Promote the finish/export card from static copy into session-aware product state and extend the HTTP exemplar through full finish, export, and replay.

req: examples/001

req: local/001

req: local/003

req: local/004
This commit is contained in:
slhx agent
2026-06-11 23:29:09 +02:00
parent c146e9b932
commit 900ae9f51f
4 changed files with 72 additions and 8 deletions
+41 -1
View File
@@ -580,6 +580,9 @@ pub struct Workout {
pub status: String,
pub progress: String,
pub recovery_status: String,
pub finish_title: String,
pub finish_copy: String,
pub share_action: String,
pub event_log: String,
pub export_payload: String,
pub host_status: String,
@@ -600,12 +603,43 @@ pub fn page(runtime_src: &'static str, state: &WorkoutState) -> Html {
pub fn view(state: &WorkoutState) -> Workout {
let export = state.export_event_log();
let has_events = !state.events.is_empty();
let (finish_title, finish_copy, share_action) = match state.projection.phase {
WorkoutPhase::Ready | WorkoutPhase::Resting if !has_events => (
"Finish unlocks as you train".into(),
"Complete a set to create a replayable local export.".into(),
"Export after first set".into(),
),
WorkoutPhase::Ready | WorkoutPhase::Resting => (
"Session log is ready".into(),
"Your progress is already replayable; finish all sets for the final export moment.".into(),
"Share current log".into(),
),
WorkoutPhase::ReadyToFinish => (
"All sets complete".into(),
"Finish to save the workout summary, then share or replay the exact local event log.".into(),
"Finish, then share".into(),
),
WorkoutPhase::Finished if state.projection.exported => (
"Export shared".into(),
"The shared text is still just a copy; this local event log remains the source.".into(),
"Share again".into(),
),
WorkoutPhase::Finished => (
"Workout saved".into(),
"Share the final event log, or replay it locally to prove recovery without hidden browser state.".into(),
"Share final export".into(),
),
};
Workout {
next_action: state.projection.next_action.clone(),
primary_action: state.projection.primary_action.clone(),
status: state.projection.progress.clone(),
progress: state.projection.progress.clone(),
recovery_status: state.recovery_text(),
finish_title,
finish_copy,
share_action,
event_log: state.event_log_text(),
export_payload: if export.is_empty() {
"Complete a set to create a replayable export.".into()
@@ -663,6 +697,9 @@ fn effects(state: &WorkoutState, status: impl Into<String>) -> impl IntoEffect {
ui::workout::status.text(status.into()),
ui::workout::progress.text(&view.progress),
ui::workout::recovery_status.text(&view.recovery_status),
ui::workout::finish_title.text(&view.finish_title),
ui::workout::finish_copy.text(&view.finish_copy),
ui::workout::share_action.text(&view.share_action),
ui::workout::event_log.text(&view.event_log),
ui::workout::export_payload.text(&view.export_payload),
ui::workout::host_status.text(&view.host_status),
@@ -985,6 +1022,8 @@ mod tests {
&finished,
"finished workout: 5/5 sets complete"
));
assert!(contains_payload_text(&finished, "Workout saved"));
assert!(contains_payload_text(&finished, "Share final export"));
assert_eq!(
app.with_workout(|state| state.projection.phase.clone()),
WorkoutPhase::Finished
@@ -1144,8 +1183,9 @@ mod tests {
let html = render(&WorkoutState::demo()).to_string();
assert!(html.contains("Now-first Workout Copilot"));
assert!(html.contains("Complete set"));
assert!(html.contains("Finish unlocks as you train"));
assert!(html.contains("Export after first set"));
assert!(html.contains("Undo last action"));
assert!(html.contains("Share export"));
assert!(html.contains("Host proof panel"));
assert!(html.contains("Simulate share denied"));
assert!(html.contains("Simulate replay failure"));