diff --git a/hive-c0re/assets/app.js b/hive-c0re/assets/app.js index 5fd0030..4d247a7 100644 --- a/hive-c0re/assets/app.js +++ b/hive-c0re/assets/app.js @@ -22,10 +22,14 @@ } return e; }; - const form = (action, btnClass, btnLabel, confirmMsg, extra = {}) => { + const form = (action, btnClass, btnLabel, confirmMsg, extra = {}, opts = {}) => { const f = el('form', { method: 'POST', action, class: 'inline', 'data-async': '', ...(confirmMsg ? { 'data-confirm': confirmMsg } : {}), + // Endpoints whose mutation fires a DashboardEvent (and whose + // derived store applies it live) opt out of the post-submit + // /api/state refetch. See the async-form handler. + ...(opts.noRefresh ? { 'data-no-refresh': '' } : {}), }); for (const [name, value] of Object.entries(extra)) { f.append(el('input', { type: 'hidden', name, value })); @@ -195,7 +199,15 @@ if (btn) { btn.disabled = false; btn.innerHTML = original; } // Clear text inputs whose value was just submitted. f.querySelectorAll('input[type="text"], input:not([type]), textarea').forEach((i) => { i.value = ''; }); - refreshState(); + // Forms whose endpoint already emits a DashboardEvent that + // updates the derived store can opt out of the post-submit + // /api/state refetch (the event delivers the new row faster + // than the snapshot poll anyway). Container-lifecycle forms + // still rely on the refresh since `ContainerView` isn't yet + // event-derivable. + if (!f.hasAttribute('data-no-refresh')) { + refreshState(); + } } catch (err) { alert('action failed: ' + err); if (btn) { btn.disabled = false; btn.innerHTML = original; } @@ -587,7 +599,7 @@ li.append(head, el('div', { class: 'q-body' }, q.question)); const f = el('form', { method: 'POST', action: '/answer-question/' + q.id, - class: 'qform', 'data-async': '', + class: 'qform', 'data-async': '', 'data-no-refresh': '', }); const hasOptions = q.options && q.options.length; const isMulti = !!q.multi && hasOptions; @@ -638,7 +650,7 @@ // merge-on-submit handler attached to the main form. const cancelForm = el('form', { method: 'POST', action: '/cancel-question/' + q.id, - class: 'qform-cancel', 'data-async': '', + class: 'qform-cancel', 'data-async': '', 'data-no-refresh': '', 'data-confirm': 'cancel this question? manager will see ' + '"[cancelled]" as the answer.', }); @@ -768,7 +780,7 @@ // the containers list (the agent doesn't exist yet). const spawn = el('form', { method: 'POST', action: '/request-spawn', - class: 'spawnform', 'data-async': '', + class: 'spawnform', 'data-async': '', 'data-no-refresh': '', }); spawn.append( el('input', { @@ -851,13 +863,13 @@ // HelperEvent::ApprovalResolved { note }. const denyForm = el('form', { method: 'POST', action: '/deny/' + a.id, - class: 'inline', 'data-async': '', + class: 'inline', 'data-async': '', 'data-no-refresh': '', 'data-prompt': 'reason for denying (optional, sent to manager):', }); denyForm.append(el('button', { type: 'submit', class: 'btn btn-deny' }, 'DENY')); row.append( ' ', - form('/approve/' + a.id, 'btn-approve', '◆ APPR0VE'), + form('/approve/' + a.id, 'btn-approve', '◆ APPR0VE', null, {}, { noRefresh: true }), ' ', denyForm, ); diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index f5720e9..8054fff 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -783,7 +783,11 @@ async fn dashboard_stream( async fn post_approve(State(state): State, AxumPath(id): AxumPath) -> Response { match actions::approve(state.coord.clone(), id).await { - Ok(()) => Redirect::to("/").into_response(), + // 200 instead of 303 — `actions::approve` fires + // `ApprovalResolved` (success path) or the eventual failure + // event, both of which the dashboard's derived store applies + // live. The matching form carries `data-no-refresh`. + Ok(()) => (StatusCode::OK, "ok").into_response(), Err(e) => error_response(&format!("approve {id} failed: {e:#}")), } } @@ -805,7 +809,7 @@ async fn post_deny( .map(str::trim) .filter(|s| !s.is_empty()); match actions::deny(&state.coord, id, note).await { - Ok(()) => Redirect::to("/").into_response(), + Ok(()) => (StatusCode::OK, "ok").into_response(), Err(e) => error_response(&format!("deny {id} failed: {e:#}")), } } @@ -853,7 +857,7 @@ async fn post_answer_question( false, ); } - Redirect::to("/").into_response() + (StatusCode::OK, "ok").into_response() } Err(e) => error_response(&format!("answer {id} failed: {e:#}")), } @@ -894,7 +898,7 @@ async fn post_cancel_question( answerer: hive_sh4re::OPERATOR_RECIPIENT.to_owned(), }, ); - Redirect::to("/").into_response() + (StatusCode::OK, "ok").into_response() } Err(e) => error_response(&format!("cancel-question {id} failed: {e:#}")), } @@ -1199,7 +1203,7 @@ async fn post_request_spawn( state .coord .emit_approval_added(id, &name, "spawn", None, None, None); - Redirect::to("/").into_response() + (StatusCode::OK, "ok").into_response() } Err(e) => error_response(&format!("request-spawn {name} failed: {e:#}")), }