You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Improve the operator UX around the Shadow / Review / Live phase transitions without changing phase semantics or breaking the explicit-save contract. The current phase stays globally visible. Shadow and Review keep the existing explicit form-save flow unchanged (no auto-save). Clicking Save on a change to Live opens an explicit confirmation modal before the POST. Cancel (button, Escape, or outside-click) closes the modal and reverts the select to the previous value. The Live confirmation modal focuses Cancel initially (settled decision). Existing modal callers are unaffected.
Current state
Phase setting operating_phase lives in SETTING_DEFS. Default and parser already enforce the three valid values.
/api/settings POST: when operating_phase is set to live, the server runs _live_readiness_errors() and rolls back to review if any readiness check fails (see app/main.py:597-606).
Frontend: #phase-badge (topbar) shows the current phase. #settings-core form has the phase select.
The three-phase explainer panel lives on the dashboard (now Monitoramento) but does not gate transitions.
Settings save today is explicit: the operator clicks "Salvar" (or equivalent) on a form. Auto-save is NOT a current behavior. This issue must NOT introduce auto-save.
showModal({ title, message, confirmText, cancelText, onConfirm }) is defined at app/static/app.js:165. Today it does NOT accept onCancel or a focus flag. The existing FR24 cluster-delete caller (app/static/app.js:649) passes only {...onConfirm} and retains its current focus (Confirm is initially focused).
UX target
Global phase indicator stays in the topbar (existing #phase-badge) and is always visible regardless of active tab.
The select still submits through the existing form-save flow on Save. There is no auto-save for Shadow or Review.
When the operator clicks Save and the new phase value is live, the existing form-save handler is intercepted: a confirmation modal opens before the POST. Cancel closes the modal and reverts the select to the previous value. Confirm triggers the existing POST /api/settings with the new phase.
Existing functionality to reuse
operating_phase setting key + parser + setter (already enforces the three values).
_live_readiness_errors() server-side check.
POST /api/settings with {"values": {"operating_phase": "live"}} — unchanged.
#phase-badge global indicator — unchanged.
showModal() helper (app/static/app.js:165) — extended with two additive, optional parameters (see Focus behavior below). Existing callers pass neither and behave exactly as today.
saveForm() helper — reuse for the underlying POST. The Save button keeps its existing behavior for Shadow/Review. For Live, the save call is gated by the modal.
The existing three-phase explainer wording from app/static/index.html lines 55-58.
Important constraint: no new appState
The current implementation does NOT have appState.alertRecipients (it does not exist anywhere in app/static/app.js). Recipients live in the settings state from /api/settings (key alert_recipients) — that value can be read at modal-render time, but a new appState.alertRecipients field MUST NOT be introduced. The modal reads from the existing settings state.
Scope
Add an explicit confirmation modal when the operator clicks Save and the new phase value is live.
Keep the select control; do not introduce a separate "phase control widget".
Add a small plain-language description under the phase select in Operação: "Sombra: eventos são registrados, nenhum alerta externo · Revisão: classifique eventos para calibrar · Ao vivo: eventos de parada provável e desaparecimento podem acionar e-mail configurado".
Extend showModal() with two optional parameters (additive; existing callers unaffected):
onCancel — callback that fires on Cancel button click, Escape key, or outside-click.
focusCancel — boolean (default false); when true, initial focus is on Cancel rather than Confirm.
The Live-enabling modal passes onCancel and focusCancel: true so Cancel receives initial focus for this safety-sensitive transition.
Existing FR24 cluster-delete caller (app/static/app.js:649) does NOT pass either parameter; it retains its existing behavior (Confirm is initially focused, Cancel button still fires its existing onConfirm path via the Cancel click — note: today the FR24 delete caller does not define onCancel, so onCancel is undefined and no extra side effect fires).
Read recipient count from the existing settings state (alert_recipients) at modal-render time.
Focus behavior in the confirmation modal (settled)
For the Live-enabling confirmation modal, the decision is settled:
showModal accepts an optional focusCancel: true flag (or an equivalently clearly named option).
The Live-enabling modal passes focusCancel: true so Cancel receives initial focus.
Existing callers do NOT pass focusCancel: true and retain their current Confirm-focus behavior. The FR24 cluster-delete modal therefore remains unchanged (Confirm initially focused).
onCancel must fire exactly once for cancellation via:
Cancel button.
Escape key.
Outside click.
onCancel MUST NOT fire when the Confirm path successfully closes the modal. Confirm continues to invoke onConfirm exactly as today.
Out of scope
New phases beyond shadow / review / live.
New delivery rules or alert logic.
New preflight checks beyond _live_readiness_errors().
Auto-save for any phase. Shadow and Review use the existing explicit-save form flow exactly as today. Only the Live transition is intercepted by a confirmation modal.
New appState.alertRecipients or any other new appState field. Recipients are read from existing settings state.
Disabling the Live option from the UI — the operator is the source of truth; readiness failures surface as server-side errors.
A general URL hash router.
Changing the focus behavior of the existing FR24 cluster-delete modal.
Detailed behavior
Phase select
Three options as today. Selecting a value updates the select visually but does NOT save.
The Save button submits the current form values via the existing saveForm() helper. No new auto-save behavior.
Confirmation modal (intercept before POST)
The save handler detects: payload contains values.operating_phase === "live" && current phase (from settings) !== "live". When detected, the modal opens instead of POSTing.
Title: "Mudar fase para Ao vivo?"
Body (one paragraph, plain language):
Current phase name (translated).
The selected phase name.
Recipient count: "Destinatários configurados: N" read at modal-render time from the existing settings state (alert_recipients); or "Nenhum destinatário configurado · alertas não serão entregues".
Reference to the existing three-phase explainer wording from app/static/index.html lines 55-58.
Two buttons: Cancel (default action — fires on Escape and outside-click) and Confirm (calls the existing save path).
The modal is invoked with { ..., onCancel, focusCancel: true }.
Escape key and outside-click close the modal and revert the select (via onCancel). Cancel button click also fires onCancel.
Saving
Confirm button calls the existing save handler with the same payload, same endpoint. This is the only path that submits operating_phase: "live" to the server.
After successful save:
#phase-badge updates via the existing loadStatus() re-fetch.
Modal closes.
A success indicator appears in .form-result (existing pattern).
CLIENT-SIDE CANCELLATION (Cancel / Escape / outside-click before POST): restore the previously selected phase in the select; no request occurred. The phase badge continues to reflect the persisted phase.
SERVER-SIDE READINESS FAILURE AFTER CONFIRM (the canonical case): the backend runs _live_readiness_errors() (app/main.py:597-606); on failure it rolls operating_phase back to review. The frontend MUST:
reuse the existing post-save loadSettings() / refresh behavior so the persisted phase is re-read from the server;
render the server's readiness error through the existing .form-result pattern;
display the actual persisted phase, which is normally review because the server rolled back. The select must NOT blindly revert to the client-side previous phase (e.g. shadow) — doing so would lie to the operator about the persisted phase.
the #phase-badge must likewise reflect the persisted phase.
This is consistent with — and depends on — the existing backend rollback behavior in app/main.py:597-606. The backend's rollback semantics are NOT modified by this issue.
Shadow and Review save
Identical to today: explicit click on Save submits via saveForm(). No modal. No auto-save. No new behavior.
Cancellation
Cancel button (and Escape key, and clicking outside the modal) closes the modal via the onCancel callback. The select visually reverts to the previous phase value. No save call is made. onCancel fires exactly once for any cancel path; it does NOT fire on Confirm.
Mirrors the three-phase explainer panel on Monitoramento but condensed.
Loading / error state
Save in flight: existing withLoading() button state.
Error from server: existing .form-result element.
Responsive behavior
Modal adapts to existing responsive modal styles.
Keyboard behavior
Modal: Cancel is initially focused. Tab to Confirm, Tab back to Cancel. Escape closes.
Phase select: native keyboard.
Data / API impact
No backend/API changes expected. The save call is unchanged; only the modal gate is added in the UI. The showModal() extension is purely additive (two new optional parameters).
Selecting shadow or review saves via the existing form-save flow exactly as today — no modal, no auto-save.
Clicking Save with a change to live opens a confirmation modal summarizing current / target phase and recipient count.
The Live-enabling confirmation modal is invoked with focusCancel: true, so Cancel is initially focused.
Cancel via button, Escape, or outside-click reverts the select without saving. onCancel fires exactly once per cancel path; it does NOT fire on Confirm.
Shadow → choose Live → Confirm → readiness failure → the persisted and displayed phase is Review (NOT Shadow). The select reflects the server's actual rollback. The phase badge likewise reflects Review. This proves the select does NOT blindly revert to the client-side previous selection.
Review → Live → readiness failure → the persisted and displayed phase remains Review. Readiness failure is idempotent.
Confirm saves via the existing POST /api/settings endpoint with the existing payload.
On server-side readiness failure, the existing error pattern renders AND the select / phase badge display the actual persisted phase (Review, per the server's rollback) — NOT the client-side previous phase.
#phase-badge reflects the new phase after save.
Recipient count in the modal is read from existing settings state at modal-render time. No new appState.alertRecipients (or any other new appState field) is introduced.
showModal() accepts an optional onCancel callback and an optional focusCancel flag; existing callers (FR24 cluster-delete) pass neither and retain their current Confirm-focus behavior. No existing modal focus / cancel behavior changes.
VM JS (tests/test_phase_transition_vm.mjs): shadow → review → shadow via existing form-save (no modal), review → live opens modal on Save click; assert Cancel is initially focused; cancel (button / Escape / outside-click) reverts to the client-side previous phase; confirm saves; assert onCancel fires exactly once per cancel path and NOT on Confirm; assert the FR24 cluster-delete modal still has its current Confirm focus (no focusCancel flag passed); Shadow → choose Live → Confirm → readiness failure → the persisted/displayed phase is Review (NOT Shadow) — this proves the select reflects the server's rollback, not the client's previous selection; Review → Live → readiness failure → the persisted/displayed phase remains Review — readiness failure is idempotent.
Integration: change phase end-to-end and verify the polling/scheduler respects the new phase (existing tests).
Regression: existing phase-change E2E tests pass unchanged; FR24 cluster delete confirmation still works with its existing focus.
Accessibility: modal is role="dialog" + aria-modal, focus trap, Escape closes.
Responsive: modal at 1280, 768, 480.
Visual QA
1280px Configurações › Operação with phase select and description.
1280px modal open showing recipient count = 0 (warn) and = 3 (normal). Cancel focused initially.
Summary
Improve the operator UX around the Shadow / Review / Live phase transitions without changing phase semantics or breaking the explicit-save contract. The current phase stays globally visible. Shadow and Review keep the existing explicit form-save flow unchanged (no auto-save). Clicking Save on a change to Live opens an explicit confirmation modal before the POST. Cancel (button, Escape, or outside-click) closes the modal and reverts the select to the previous value. The Live confirmation modal focuses Cancel initially (settled decision). Existing modal callers are unaffected.
Current state
operating_phaselives inSETTING_DEFS. Default and parser already enforce the three valid values./api/settingsPOST: whenoperating_phaseis set tolive, the server runs_live_readiness_errors()and rolls back toreviewif any readiness check fails (seeapp/main.py:597-606).#phase-badge(topbar) shows the current phase.#settings-coreform has the phase select.showModal({ title, message, confirmText, cancelText, onConfirm })is defined atapp/static/app.js:165. Today it does NOT acceptonCancelor a focus flag. The existing FR24 cluster-delete caller (app/static/app.js:649) passes only{...onConfirm}and retains its current focus (Confirm is initially focused).UX target
#phase-badge) and is always visible regardless of active tab.live, the existing form-save handler is intercepted: a confirmation modal opens before the POST. Cancel closes the modal and reverts the select to the previous value. Confirm triggers the existingPOST /api/settingswith the new phase.Existing functionality to reuse
operating_phasesetting key + parser + setter (already enforces the three values)._live_readiness_errors()server-side check.POST /api/settingswith{"values": {"operating_phase": "live"}}— unchanged.#phase-badgeglobal indicator — unchanged.showModal()helper (app/static/app.js:165) — extended with two additive, optional parameters (see Focus behavior below). Existing callers pass neither and behave exactly as today.saveForm()helper — reuse for the underlying POST. The Save button keeps its existing behavior for Shadow/Review. For Live, the save call is gated by the modal.phase_shadow,phase_review,phase_live,shadow_desc,review_desc,live_desc.app/static/index.htmllines 55-58.Important constraint: no new appState
The current implementation does NOT have
appState.alertRecipients(it does not exist anywhere inapp/static/app.js). Recipients live in the settings state from/api/settings(keyalert_recipients) — that value can be read at modal-render time, but a newappState.alertRecipientsfield MUST NOT be introduced. The modal reads from the existing settings state.Scope
live.showModal()with two optional parameters (additive; existing callers unaffected):onCancel— callback that fires on Cancel button click, Escape key, or outside-click.focusCancel— boolean (defaultfalse); whentrue, initial focus is on Cancel rather than Confirm.onCancelandfocusCancel: trueso Cancel receives initial focus for this safety-sensitive transition.app/static/app.js:649) does NOT pass either parameter; it retains its existing behavior (Confirm is initially focused, Cancel button still fires its existing onConfirm path via the Cancel click — note: today the FR24 delete caller does not define onCancel, so onCancel is undefined and no extra side effect fires).settings_phase_change_confirm_title,settings_phase_change_confirm_live_body,settings_phase_change_confirm_button,settings_phase_change_cancel.alert_recipients) at modal-render time.Focus behavior in the confirmation modal (settled)
For the Live-enabling confirmation modal, the decision is settled:
showModalaccepts an optionalfocusCancel: trueflag (or an equivalently clearly named option).focusCancel: trueso Cancel receives initial focus.focusCancel: trueand retain their current Confirm-focus behavior. The FR24 cluster-delete modal therefore remains unchanged (Confirm initially focused).onCancelmust fire exactly once for cancellation via:onCancelMUST NOT fire when the Confirm path successfully closes the modal. Confirm continues to invokeonConfirmexactly as today.Out of scope
_live_readiness_errors().appState.alertRecipientsor any other newappStatefield. Recipients are read from existing settings state.Detailed behavior
Phase select
saveForm()helper. No new auto-save behavior.Confirmation modal (intercept before POST)
payload contains values.operating_phase === "live" && current phase (from settings) !== "live". When detected, the modal opens instead of POSTing.alert_recipients); or "Nenhum destinatário configurado · alertas não serão entregues".app/static/index.htmllines 55-58.{ ..., onCancel, focusCancel: true }.onCancel). Cancel button click also firesonCancel.Saving
operating_phase: "live"to the server.#phase-badgeupdates via the existingloadStatus()re-fetch..form-result(existing pattern)._live_readiness_errors()(app/main.py:597-606); on failure it rollsoperating_phaseback toreview. The frontend MUST:loadSettings()/ refresh behavior so the persisted phase is re-read from the server;.form-resultpattern;reviewbecause the server rolled back. The select must NOT blindly revert to the client-side previous phase (e.g. shadow) — doing so would lie to the operator about the persisted phase.#phase-badgemust likewise reflect the persisted phase.This is consistent with — and depends on — the existing backend rollback behavior in
app/main.py:597-606. The backend's rollback semantics are NOT modified by this issue.Shadow and Review save
saveForm(). No modal. No auto-save. No new behavior.Cancellation
onCancelcallback. The select visually reverts to the previous phase value. No save call is made.onCancelfires exactly once for any cancel path; it does NOT fire on Confirm.Global visibility
#phase-badgealready updates vialoadStatus(). Issue [UX Redesign] Reorganize Configurações into clear operational sections #19 ensures it is always in the topbar regardless of tab. This issue verifies it updates correctly after a phase change.Plain-language description under the select
Loading / error state
withLoading()button state..form-resultelement.Responsive behavior
Keyboard behavior
Data / API impact
No backend/API changes expected. The save call is unchanged; only the modal gate is added in the UI. The
showModal()extension is purely additive (two new optional parameters).Routing / navigation impact
#operacaoinside Configurações (Issue [UX Redesign] Reorganize Configurações into clear operational sections #19) lets Monitoramento or another tab deep-focus to the phase control viaopenSettingsSection("operacao")if desired.Security / privacy
Acceptance criteria
Phase select lives in Configurações › Operação.
Selecting shadow or review saves via the existing form-save flow exactly as today — no modal, no auto-save.
Clicking Save with a change to live opens a confirmation modal summarizing current / target phase and recipient count.
The Live-enabling confirmation modal is invoked with
focusCancel: true, so Cancel is initially focused.Cancel via button, Escape, or outside-click reverts the select without saving.
onCancelfires exactly once per cancel path; it does NOT fire on Confirm.Shadow → choose Live → Confirm → readiness failure → the persisted and displayed phase is Review (NOT Shadow). The select reflects the server's actual rollback. The phase badge likewise reflects Review. This proves the select does NOT blindly revert to the client-side previous selection.
Review → Live → readiness failure → the persisted and displayed phase remains Review. Readiness failure is idempotent.
Confirm saves via the existing
POST /api/settingsendpoint with the existing payload.On server-side readiness failure, the existing error pattern renders AND the select / phase badge display the actual persisted phase (Review, per the server's rollback) — NOT the client-side previous phase.
#phase-badgereflects the new phase after save.Recipient count in the modal is read from existing settings state at modal-render time. No new
appState.alertRecipients(or any other newappStatefield) is introduced.showModal()accepts an optionalonCancelcallback and an optionalfocusCancelflag; existing callers (FR24 cluster-delete) pass neither and retain their current Confirm-focus behavior. No existing modal focus / cancel behavior changes.VM JS (
tests/test_phase_transition_vm.mjs): shadow → review → shadow via existing form-save (no modal), review → live opens modal on Save click; assert Cancel is initially focused; cancel (button / Escape / outside-click) reverts to the client-side previous phase; confirm saves; assertonCancelfires exactly once per cancel path and NOT on Confirm; assert the FR24 cluster-delete modal still has its current Confirm focus (nofocusCancelflag passed); Shadow → choose Live → Confirm → readiness failure → the persisted/displayed phase is Review (NOT Shadow) — this proves the select reflects the server's rollback, not the client's previous selection; Review → Live → readiness failure → the persisted/displayed phase remains Review — readiness failure is idempotent.Integration: change phase end-to-end and verify the polling/scheduler respects the new phase (existing tests).
Regression: existing phase-change E2E tests pass unchanged; FR24 cluster delete confirmation still works with its existing focus.
Accessibility: modal is
role="dialog"+aria-modal, focus trap, Escape closes.Responsive: modal at 1280, 768, 480.
Visual QA
Dependencies
Rollout / compatibility
Open questions
None.