From 1ea34dc836c6075e08b8353049c8a5a97fc9d364 Mon Sep 17 00:00:00 2001 From: Dave Liu <7david12liu@gmail.com> Date: Thu, 13 Aug 2026 22:19:07 -0700 Subject: [PATCH] Implement AUTH-006I profile Edit focus --- IMPLEMENTATION_PLAN.md | 6 +- SECURITY.md | 5 +- SYSTEM_DESIGN.md | 6 +- docs/officers/EVENTS_SHOP_MEMBERS.md | 149 +++++++++++- src/pages/account/Account.test.tsx | 324 +++++++++++++++++++++++++++ src/pages/account/Account.tsx | 43 +++- 6 files changed, 520 insertions(+), 13 deletions(-) diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index 3a68d46..9a8b376 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -169,9 +169,11 @@ Exit gate: - Payment SLOs, structured redacted logs, and actionable alerts are live. - Backup restoration into isolated infrastructure succeeds and is documented. -**AUTH-006G current source boundary:** [#651](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/651) changes only confirmed full-name Save feedback and keyboard-focus settlement in My Account. An exact current update followed by its current successful non-null authoritative profile reread renders exactly **Profile name saved.** between the Profile heading and **Edit**, with status, polite live, atomic, programmatic-focus, 320-pixel containment, and scoped visible-outline semantics. The two-column header grid keeps heading and Edit on the first row while the result spans the second; DOM order remains heading, result, Edit, so Tab from the result reaches Edit. A pending focus intent is created only after validation and synchronous one-attempt admission, only while the exact connected Save button owns focus, and stores only the opaque current profile generation and attempt ID. Exact current confirmed success transfers the matching intent. One layout effect consumes it before target checks, leaves retained result focus alone, restores absent, body, document-root, or disconnected focus to the connected result, and preserves every other connected focus deliberately chosen while the save is pending. An unfocused or programmatic valid Save still shows the truthful result without moving focus. Initial load, validation failure, update rejection, missing or rejected confirmation read, reload, application/Firestore/identity/UID or generation change, newer attempt, stale completion, unmount, and later rerender cannot show or focus stale success; Edit, a new admitted Save, profile load, and context change clear the result and obsolete intents. Existing validation, write, reread, one-attempt/context fences, unconfirmed-change recovery, and exact service-call counts remain unchanged. The result and focus add no read, write, request, retry, provider call, log, or stored value. AUTH-006H [#653](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/653) separately owns unconfirmed-change alert focus; validation error association, Edit-to-input focus, and Cancel-to-Edit focus remain separate. #651 adds one visible page-structure node but changes no data movement, permission, ownership, service contract, Function, Rule, schema, index, package, workflow, provider, account, sign-in, production data, deployment, publication, membership, dues, role, payment, entitlement, roster, biometric processing, or live behavior. The #118 backend-first profile-repair evidence remains separate. Directory availability stays `false`, live #623 remains inert, and #507 keeps every optional-directory connection and live-proof gate. +**AUTH-006G current source boundary:** [#651](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/651) changes only confirmed full-name Save feedback and keyboard-focus settlement in My Account. An exact current update followed by its current successful non-null authoritative profile reread renders exactly **Profile name saved.** between the Profile heading and **Edit**, with status, polite live, atomic, programmatic-focus, 320-pixel containment, and scoped visible-outline semantics. The two-column header grid keeps heading and Edit on the first row while the result spans the second; DOM order remains heading, result, Edit, so Tab from the result reaches Edit. A pending focus intent is created only after validation and synchronous one-attempt admission, only while the exact connected Save button owns focus, and stores only the opaque current profile generation and attempt ID. Exact current confirmed success transfers the matching intent. One layout effect consumes it before target checks, leaves retained result focus alone, restores absent, body, document-root, or disconnected focus to the connected result, and preserves every other connected focus deliberately chosen while the save is pending. An unfocused or programmatic valid Save still shows the truthful result without moving focus. Initial load, validation failure, update rejection, missing or rejected confirmation read, reload, application/Firestore/identity/UID or generation change, newer attempt, stale completion, unmount, and later rerender cannot show or focus stale success; Edit, a new admitted Save, profile load, and context change clear the result and obsolete intents. Existing validation, write, reread, one-attempt/context fences, unconfirmed-change recovery, and exact service-call counts remain unchanged. The result and focus add no read, write, request, retry, provider call, log, or stored value. AUTH-006H [#653](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/653) separately owns unconfirmed-change alert focus; AUTH-006I [#655](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/655) separately owns Edit-to-input focus; validation error association and Cancel-to-Edit focus remain separate. #651 adds one visible page-structure node but changes no data movement, permission, ownership, service contract, Function, Rule, schema, index, package, workflow, provider, account, sign-in, production data, deployment, publication, membership, dues, role, payment, entitlement, roster, biometric processing, or live behavior. The #118 backend-first profile-repair evidence remains separate. Directory availability stays `false`, live #623 remains inert, and #507 keeps every optional-directory connection and live-proof gate. -**AUTH-006H current source boundary:** [#653](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/653) changes only keyboard-focus settlement for the existing unconfirmed full-name Save recovery. After validation and synchronous one-attempt admission, the existing pending Save-focus token is armed only while the exact connected Save button owns focus and retains only the opaque profile generation and attempt ID. An exact current update rejection, `null` confirmation reread, or rejected confirmation reread creates a minimal unconfirmed-result token for the current generation and attempt and transfers a matching pending focus token only to the separate unconfirmed focus ref; it does not guess whether the update persisted. The existing alert keeps the byte-exact message **We could not confirm your profile change. Try the profile again before making another change.**, contains no member or service detail, and now has assertive alert, atomic live-region, programmatic-focus, bounded wrapping, and scoped visible-outline semantics only for this exact current save-unconfirmed state. Its message remains immediately before the enabled **Try profile again** button, making that action the next Tab stop. One layout effect consumes the intent before target checks; it requires the exact current generation and attempt, unavailable state, no profile, editor, or success confirmation, the exact message, and a connected alert. It leaves retained alert focus alone, restores absent, body, document-root, or disconnected focus, and preserves any other connected deliberate focus. Unfocused or programmatic Save still shows the recovery without moving focus, including after its outside origin disappears. Initial setup/read failure, validation failure, reload, application/Firestore/identity/UID or generation change, newer attempt, obsolete update/reread work, unmount, and later rerender cannot focus a stale result; Try clears all old Save intents before the existing load. AUTH-006G success, validation, the name-only payload, update/reread behavior, one-attempt/context fences, fixed recovery copy/action, and exact service calls remain unchanged. The handoff adds no read, write, request, retry, provider call, log, stored value, page node, data movement, permission, ownership, service contract, Function, Rule, schema, index, package, workflow, provider, account, sign-in, production-data action, deployment, publication, membership, dues, role, payment, entitlement, roster, biometric processing, or live behavior. Later Try-profile-again result focus, validation error association, Edit-to-input focus, and Cancel-to-Edit focus remain separate. The #118 backend-first profile-repair evidence remains separate. Directory availability stays `false`, live #623 remains inert, and #507 keeps every optional-directory connection and live-proof gate. +**AUTH-006H current source boundary:** [#653](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/653) changes only keyboard-focus settlement for the existing unconfirmed full-name Save recovery. After validation and synchronous one-attempt admission, the existing pending Save-focus token is armed only while the exact connected Save button owns focus and retains only the opaque profile generation and attempt ID. An exact current update rejection, `null` confirmation reread, or rejected confirmation reread creates a minimal unconfirmed-result token for the current generation and attempt and transfers a matching pending focus token only to the separate unconfirmed focus ref; it does not guess whether the update persisted. The existing alert keeps the byte-exact message **We could not confirm your profile change. Try the profile again before making another change.**, contains no member or service detail, and now has assertive alert, atomic live-region, programmatic-focus, bounded wrapping, and scoped visible-outline semantics only for this exact current save-unconfirmed state. Its message remains immediately before the enabled **Try profile again** button, making that action the next Tab stop. One layout effect consumes the intent before target checks; it requires the exact current generation and attempt, unavailable state, no profile, editor, or success confirmation, the exact message, and a connected alert. It leaves retained alert focus alone, restores absent, body, document-root, or disconnected focus, and preserves any other connected deliberate focus. Unfocused or programmatic Save still shows the recovery without moving focus, including after its outside origin disappears. Initial setup/read failure, validation failure, reload, application/Firestore/identity/UID or generation change, newer attempt, obsolete update/reread work, unmount, and later rerender cannot focus a stale result; Try clears all old Save intents before the existing load. AUTH-006G success, validation, the name-only payload, update/reread behavior, one-attempt/context fences, fixed recovery copy/action, and exact service calls remain unchanged. The handoff adds no read, write, request, retry, provider call, log, stored value, page node, data movement, permission, ownership, service contract, Function, Rule, schema, index, package, workflow, provider, account, sign-in, production-data action, deployment, publication, membership, dues, role, payment, entitlement, roster, biometric processing, or live behavior. AUTH-006I [#655](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/655) separately owns Edit-to-input focus; later Try-profile-again result focus, validation error association, and Cancel-to-Edit focus remain separate. The #118 backend-first profile-repair evidence remains separate. Directory availability stays `false`, live #623 remains inert, and #507 keeps every optional-directory connection and live-proof gate. + +**AUTH-006I current source boundary:** [#655](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/655) changes only keyboard-focus settlement when the existing full-name editor opens. Each activation replaces any older Edit intent, but only the exact connected native **Edit** event target that owns focus records the opaque current profile generation before opening the editor and clearing prior AUTH-006G/H save feedback. An unfocused or programmatic activation opens the same editor without moving focus. One layout effect consumes the intent before checking the target; it requires the mounted matching generation, exact current application/Firestore/identity/UID profile context, ready profile, open editor, no save in progress, and exact connected enabled native **Full name** input. It leaves an already-focused input alone, restores absent, body, document-root, or disconnected focus, and preserves every other connected deliberate focus. Missing or disabled target, obsolete context, closed editor, pending save, or deliberate connected focus consumes the intent without delayed focus on a later render. The existing current name, label, linked description, `autocomplete="name"`, 200-character maximum, enabled state, input-before-Save-and-Cancel order, and global native-control visible-focus rule remain unchanged; Save is the next ordinary Tab stop after the focused input. Context changes, profile load/reload, Try, Save, Cancel, and unmount clear or consume the intent. Editor entry adds no validation, read, write, request, retry, provider call, log, storage, React state, CSS, or page node. AUTH-006F/G/H, payloads, and exact calls remain unchanged. Cancel-to-Edit focus, validation-error association or focus, Try-profile-again settlement focus, and save-pending announcement stay separate. #655 changes no data movement, permission, ownership, service contract, Function, Rule, schema, index, package, workflow, provider, account, sign-in, production data, deployment, publication, membership, dues, role, payment, entitlement, roster, biometric processing, or live behavior. The #118 backend-first profile-repair evidence remains separate. Directory availability stays `false`, live #623 remains inert, and #507 keeps every optional-directory connection and live-proof gate. **Current optional-directory boundary:** Parent [#504](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/504) defines a private, opt-in officer people finder as name search with voluntary thumbnails—not facial recognition. MEMBERS-DIRECTORY-001A [#505](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/505) owns the signed-in person's server-only processed thumbnail and independent default-off preference. MEMBERS-DIRECTORY-001B [#506](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/506) adds source for the minimum server-only projection, current-state reconciler, bounded verified-admin name-prefix callable, query-free audit, and separate `/admin/member-directory` gallery. It returns at most 24 current opted-in display-name/optional-thumbnail cards and has no image query, facial recognition, cursor, total, export, or membership authority. MEMBERS-DIRECTORY-001D [#621](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/621) makes the shipped frontend boundary default unavailable: My Account and the guarded officer route show only visibly described, disabled layouts and perform zero directory reads, uploads, searches, saves, request-ID creation, or service calls. #623 published exactly that inert interface as deploy `6a7e072f8f346b0008510d29`; the temporary authority is re-paused. Protected layout proof remains synthetic. Signed-out public proof is limited to revision and guard readback plus the absence of a directory request. [#507](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/507) still owns #110 policy completion, scoped authorization, #133 protected authority, isolated staging, required backend/index deployment and readback, and a later separately reviewed source flip before connected website publication and live proof. Do not reuse the current browser-side full-account filter or describe the published disabled preview or #505/#506 source as an available directory. diff --git a/SECURITY.md b/SECURITY.md index 13521e6..81eb8e0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -168,8 +168,9 @@ These entries are implementation evidence, not a production risk-acceptance deci | DATA-001A9 / registration-sort failure containment | DATA-001A9 [#598](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/598) catches without binding every exceptional created-time comparison outcome in the final registration sort after event work completes and returns only fixed `unavailable / Registration data could not be loaded.` with no partial callable response. Synthetic `_seconds` access and numeric-coercion failures prove no caught failure-value inspection, formatting, serialization, logging, or raw escape while App Check, Auth, the exact-UID query, runner minimization, A5–A8 boundaries, event lookup concurrency, successful and empty behavior, and descending registration-created ordering stay unchanged. | This source is not deployed or live. It does not validate malformed-but-nonthrowing timestamps or event IDs; alter fallback-to-zero or tie behavior; contain event-ID derivation or response/platform serialization; cancel or reorder already completed event reads; change stored data, response shape, permissions, or retries; prove platform logging outside application code; or prove live behavior. Complete a protected Function deployment/readback and made-up account check before describing the fixed boundary as live. | | RISK-026, RISK-036 | #135 merged through PR #138 as `9eafab1217aff7058c42240aaba72d7b93f8ed24`, replacing automatic frontend-first/fail-open GitHub deployment with a tested manual exact-current-commit gate. Post-merge staging and synthetic production probes failed closed before authentication or mutation, and published neither Firebase nor Pages. | #133 must configure protected environments and least-privilege OIDC/WIF; #136 must prove staged/target deployment and clearing/readback of the existing Pages `runmprc.com` claim; a protected WEB-001 child must establish Netlify publication and rollback. No Firebase, Pages, live-host, or provider-setting change is proven by source/static tests alone. | | RISK-039 | The #118 source slice uses an empty authenticated callable request, bounded Firebase Auth identity fields, one transactional create-only helper shared with signup, constant responses, generic failures, and a UI that hides Edit until setup and the Rules-protected read succeed. Signup and recovery never change custom claims; browser profile creation remains denied. | Source review/merge is not deployment. Under #105, deploy the exact #100 Rules plus both `createMemberOnSignUp` and `ensureMemberProfile` before the website, prove App Check policy, use a synthetic staged account, verify rollback, then record website, Function, Rules, and live behavior separately. Never repair a real profile manually. | -| AUTH-006G / source-only confirmed-save interface containment for RISK-039 | [#651](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/651) gives an exact current full-name update plus successful non-null authoritative profile reread one fixed **Profile name saved.** result with status, polite live, atomic, programmatic-focus, bounded-layout, and scoped visible-outline semantics. Profile-header DOM order is heading, result, Edit. Separate pending and result tokens retain only the opaque profile generation and attempt ID. A token is armed only after validation and synchronous save admission while the exact connected Save button owns focus, and exact current confirmed success alone transfers it. One layout effect consumes the result token before target checks, requires the matching current generation and attempt plus the connected result, leaves retained result focus alone, restores absent, body, document-root, or disconnected focus, and preserves every other connected focus. Unfocused or programmatic success shows the result without moving focus. Initial load, validation failure, update rejection, missing or rejected confirmation read, reload, application/Firestore/identity/UID or generation change, newer attempt, stale work, unmount, and later rerender cannot show or focus stale success. Edit, a new admitted Save, profile load, and context change clear the result and obsolete tokens. Synthetic tests use made-up profiles and cover exact call counts, current reread projection, DOM/live/CSS semantics, lost and preserved focus, one-shot clearing, failure paths, stale authoritative reads, and lifecycle fences. | The fixed result and local focus are current-browser accessibility feedback, not independent proof of Firebase deployment, provider acknowledgement, identity or profile ownership beyond existing authentication, production persistence, membership, dues, role, payment, entitlement, directory eligibility, or live behavior. The tokens contain no name, email, UID, profile, revision, response, error, provider value, or photo data and create no read, write, request, retry, provider call, log, or stored value. Existing validation, name-only payload, authoritative reread, one-attempt/context fences, generic unconfirmed-change recovery, and exact service calls remain unchanged. AUTH-006H [#653](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/653) separately owns unconfirmed-change alert focus; validation error association, Edit-to-input focus, and Cancel-to-Edit focus remain open separate outcomes. Complete #118/#105/#133/#136 backend-first and publication proof before calling the profile repair or this result live. Directory availability remains `false`; #623 remains the inert deployed preview; #507 retains all optional-directory gates. No Function, Rule, schema, index, service, package, workflow, provider, account, sign-in, production-data, deployment, publication, photo-query, facial-recognition, matching, embedding, similarity, biometric, roster, or live action occurs. Active #616 OAUTH-001A2L and its RISK-024 source hunk remain unchanged. | -| AUTH-006H / source-only unconfirmed-save interface containment for RISK-039 | [#653](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/653) reuses the existing full-name Save unconfirmed alert and **Try profile again** action. After validation and synchronous one-attempt admission, the pending focus token is armed only while the exact connected Save button owns focus and retains only the opaque profile generation and attempt ID. An exact current update rejection, `null` confirmation reread, or rejected confirmation reread creates separate unconfirmed result ownership with the same minimal generation-and-attempt pair and transfers the pending focus token only when it matches; no browser state classifies whether the update persisted. Only that exact current save-unconfirmed state gives the byte-exact fixed alert assertive, atomic, programmatic-focus, bounded-wrapping, and scoped visible-outline semantics. The message contains no member, profile, provider, response, or caught detail, immediately precedes the enabled retry button, and keeps that action as the next Tab stop. One layout effect consumes the intent before checks and requires the matching current generation and attempt, unavailable state, no profile/editor/success confirmation, exact fixed message, and connected alert. It leaves retained alert focus alone, restores absent, body, document-root, or disconnected focus, and preserves other connected focus. An unfocused/programmatic Save still shows the alert without moving focus. Initial setup/read failure, validation failure, reload, application/Firestore/identity/UID or generation change, newer attempt, stale update/reread settlement, unmount, and later rerender cannot focus a stale result; Try clears the old intents before the existing reload. Synthetic tests use only made-up profiles and cover all three unconfirmed paths, exact calls, fixed copy/DOM/live/CSS semantics, lost and preserved focus, programmatic invocation, initial and validation failures, retry clearing, lifecycle races, newer work, one-shot consumption, and unmount. | The fixed alert and local focus communicate browser uncertainty; they do not prove the update failed or persisted, provider acknowledgement, profile or identity ownership beyond existing authentication, Firebase deployment, production data, membership, dues, role, payment, entitlement, directory eligibility, or live behavior. The state and refs retain no name, email, UID, profile, revision, request, response, error, provider value, or photo data and add no read, write, request, retry, provider call, log, stored value, page node, or data movement. AUTH-006G success, validation, name-only payload, update/reread behavior, context fences, fixed recovery copy/action, and exact service calls remain unchanged. Later Try-profile-again result focus, validation error association, Edit-to-input focus, and Cancel-to-Edit focus remain separate. Complete #118/#105/#133/#136 backend-first and publication proof before calling the profile repair or this result live. Directory availability remains `false`; #623 remains the inert deployed preview; #507 retains all optional-directory gates. No Function, Rule, schema, index, service, package, workflow, provider configuration, account, sign-in, production-data, deployment, publication, photo query, facial recognition, matching, embedding, similarity, biometric processing, roster, or live action occurs. Active #616 OAUTH-001A2L and its RISK-024 source hunk remain unchanged. | +| AUTH-006G / source-only confirmed-save interface containment for RISK-039 | [#651](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/651) gives an exact current full-name update plus successful non-null authoritative profile reread one fixed **Profile name saved.** result with status, polite live, atomic, programmatic-focus, bounded-layout, and scoped visible-outline semantics. Profile-header DOM order is heading, result, Edit. Separate pending and result tokens retain only the opaque profile generation and attempt ID. A token is armed only after validation and synchronous save admission while the exact connected Save button owns focus, and exact current confirmed success alone transfers it. One layout effect consumes the result token before target checks, requires the matching current generation and attempt plus the connected result, leaves retained result focus alone, restores absent, body, document-root, or disconnected focus, and preserves every other connected focus. Unfocused or programmatic success shows the result without moving focus. Initial load, validation failure, update rejection, missing or rejected confirmation read, reload, application/Firestore/identity/UID or generation change, newer attempt, stale work, unmount, and later rerender cannot show or focus stale success. Edit, a new admitted Save, profile load, and context change clear the result and obsolete tokens. Synthetic tests use made-up profiles and cover exact call counts, current reread projection, DOM/live/CSS semantics, lost and preserved focus, one-shot clearing, failure paths, stale authoritative reads, and lifecycle fences. | The fixed result and local focus are current-browser accessibility feedback, not independent proof of Firebase deployment, provider acknowledgement, identity or profile ownership beyond existing authentication, production persistence, membership, dues, role, payment, entitlement, directory eligibility, or live behavior. The tokens contain no name, email, UID, profile, revision, response, error, provider value, or photo data and create no read, write, request, retry, provider call, log, or stored value. Existing validation, name-only payload, authoritative reread, one-attempt/context fences, generic unconfirmed-change recovery, and exact service calls remain unchanged. AUTH-006H [#653](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/653) separately owns unconfirmed-change alert focus; AUTH-006I [#655](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/655) separately owns Edit-to-input focus; validation error association and Cancel-to-Edit focus remain open separate outcomes. Complete #118/#105/#133/#136 backend-first and publication proof before calling the profile repair or this result live. Directory availability remains `false`; #623 remains the inert deployed preview; #507 retains all optional-directory gates. No Function, Rule, schema, index, service, package, workflow, provider, account, sign-in, production-data, deployment, publication, photo-query, facial-recognition, matching, embedding, similarity, biometric, roster, or live action occurs. Active #616 OAUTH-001A2L and its RISK-024 source hunk remain unchanged. | +| AUTH-006H / source-only unconfirmed-save interface containment for RISK-039 | [#653](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/653) reuses the existing full-name Save unconfirmed alert and **Try profile again** action. After validation and synchronous one-attempt admission, the pending focus token is armed only while the exact connected Save button owns focus and retains only the opaque profile generation and attempt ID. An exact current update rejection, `null` confirmation reread, or rejected confirmation reread creates separate unconfirmed result ownership with the same minimal generation-and-attempt pair and transfers the pending focus token only when it matches; no browser state classifies whether the update persisted. Only that exact current save-unconfirmed state gives the byte-exact fixed alert assertive, atomic, programmatic-focus, bounded-wrapping, and scoped visible-outline semantics. The message contains no member, profile, provider, response, or caught detail, immediately precedes the enabled retry button, and keeps that action as the next Tab stop. One layout effect consumes the intent before checks and requires the matching current generation and attempt, unavailable state, no profile/editor/success confirmation, exact fixed message, and connected alert. It leaves retained alert focus alone, restores absent, body, document-root, or disconnected focus, and preserves other connected focus. An unfocused/programmatic Save still shows the alert without moving focus. Initial setup/read failure, validation failure, reload, application/Firestore/identity/UID or generation change, newer attempt, stale update/reread settlement, unmount, and later rerender cannot focus a stale result; Try clears the old intents before the existing reload. Synthetic tests use only made-up profiles and cover all three unconfirmed paths, exact calls, fixed copy/DOM/live/CSS semantics, lost and preserved focus, programmatic invocation, initial and validation failures, retry clearing, lifecycle races, newer work, one-shot consumption, and unmount. | The fixed alert and local focus communicate browser uncertainty; they do not prove the update failed or persisted, provider acknowledgement, profile or identity ownership beyond existing authentication, Firebase deployment, production data, membership, dues, role, payment, entitlement, directory eligibility, or live behavior. The state and refs retain no name, email, UID, profile, revision, request, response, error, provider value, or photo data and add no read, write, request, retry, provider call, log, stored value, page node, or data movement. AUTH-006G success, validation, name-only payload, update/reread behavior, context fences, fixed recovery copy/action, and exact service calls remain unchanged. AUTH-006I [#655](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/655) separately owns Edit-to-input focus; later Try-profile-again result focus, validation error association, and Cancel-to-Edit focus remain separate. Complete #118/#105/#133/#136 backend-first and publication proof before calling the profile repair or this result live. Directory availability remains `false`; #623 remains the inert deployed preview; #507 retains all optional-directory gates. No Function, Rule, schema, index, service, package, workflow, provider configuration, account, sign-in, production-data, deployment, publication, photo query, facial recognition, matching, embedding, similarity, biometric processing, roster, or live action occurs. Active #616 OAUTH-001A2L and its RISK-024 source hunk remain unchanged. | +| AUTH-006I / source-only Edit-to-input interface containment for RISK-039 | [#655](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/655) keeps **Edit** and **Full name** as their existing native current-profile controls and adds only two local refs: one retains the opaque current profile generation when the exact connected Edit event target owns focus, and one targets the existing input. Each Edit activation replaces the intent before opening the editor and clearing prior Save feedback. One layout effect consumes the intent before every check and requires the mounted matching generation, exact current application/Firestore/identity/UID profile context, ready profile, open editor, no save in progress, and connected enabled input. It leaves an already-focused input alone; restores only absent, body, document-root, or disconnected focus; and preserves other connected focus. A missing or disabled target, stale context, closed editor, pending save, or deliberate connected focus consumes the intent without delayed reuse. Unfocused/programmatic Edit opens the editor without focus movement. Profile load/reload, context change, Try, Save, Cancel, and unmount clear or consume the intent. Synthetic tests use only made-up profiles and cover current name and call counts, native input semantics and keyboard order, global focus styling, lost and preserved focus, programmatic activation, one-shot consumption, context boundaries, prior confirmed success, and unmount. | Local focus is accessibility behavior, not proof of Firebase deployment, provider acknowledgement, profile or identity ownership beyond existing authentication, production persistence, membership, dues, role, payment, entitlement, directory eligibility, or live behavior. The intent retains no name, email, UID, profile, revision, attempt, request, response, error, provider value, or photo data and adds no React state, validation, read, write, request, retry, provider call, log, storage, CSS, page node, or data movement. The existing authoritative current name, label, linked description, autocomplete, maximum length, enabled state, global visible-focus rule, input/Save/Cancel order, AUTH-006F/G/H behavior, payloads, and exact calls remain unchanged. Cancel-to-Edit focus, validation-error association or focus, Try-profile-again settlement focus, and save-pending announcement remain separate. Complete #118/#105/#133/#136 backend-first and publication proof before calling profile repair or this handoff live. Directory availability remains `false`; #623 remains the inert deployed preview; #507 retains all optional-directory gates. No Function, Rule, schema, index, service, package, workflow, provider configuration, account, sign-in, production-data, deployment, publication, photo query, facial recognition, matching, embedding, similarity, biometric processing, roster, or live action occurs. Active #616 OAUTH-001A2L and its RISK-024 source hunk remain unchanged. | | RISK-040 | AUTH-MAIL-002A [#145](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/145) merged as `46557c7`: account creation returns `accepted` or `unavailable` without exposing provider details. AUTH-MAIL-002B [#153](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/153) merged as `23bca8c8`: My Account makes no false “sent” claim, blocks rapid repeats, and applies the same 60-second browser cooldown after either outcome. Its protected release run `29252492614` stopped before build because the required public App Check key was absent, so neither frontend revision is published. AUTH-MAIL-002C1 [#155](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/155) tracks one byte-equivalent password-reset request result after provider success or failure. AUTH-MAIL-002C2 [#194](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/194) tracks the source-only `/auth/action` verification path: the initial capability suppresses Sentry/App Check, the page removes native and router query/fragment state, a scanner-style page load makes no action-code check/apply and no account mutation, and one deliberate action requires provider `VERIFY_EMAIL` before apply. Fixed results expose no email, code, raw provider error, account identity, or provider-directed navigation. Synthetic tests use mocks and canary values only. | Publish and verify each exact frontend revision separately. The existing #99 Pages bridge briefly uses tab-local session storage for the return route and deletes it before React; a failed root load can leave it until tab close, while direct-rewrite hosting avoids that residual. #194 adds no storage write. #118 profile source is merged but live behavior is unproven. Keep delivery, Spam, DNS, templates, provider handler choice, and private Firebase email-enumeration-protection readback under #119. Firebase uses one custom handler for verification, password reset, and email recovery; never point its global action URL at the verification-only #194 route until every enabled mode is safely handled, or keep the default multi-mode handler. Accepted requests do not prove delivery, browser cooldowns are not abuse controls, Auth verification does not grant membership, and the Firestore verification mirror remains unfinished AUTH-001 work. | | RISK-041 | WEB-PRIVACY-001W [#496](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/496) adds source-only containment to the Admin Product editor. One mounted page synchronously admits at most one valid save for its current route, Firestore reference, and exact authenticated admin UID. Pending and unknown results hide the complete form and actions; a rejection is discarded without binding or inspection and becomes one fixed accessible stop result. Missing identity or database state starts no save, obsolete or unmounted completions are inert, and the existing exact create/update projections plus current successful navigation stay unchanged. | This browser guard resets on navigation or reload and does not make a repeated write safe. Direct client writes, missing durable command identity, version fencing, audit, private readback, reconciliation, authorization hardening, backup, rollback, Firebase/Rules deployment, website publication, exact live revision, and production behavior remain unproven. Keep the Admin screen unavailable and replace it with a server-authoritative idempotent command before officer use. | diff --git a/SYSTEM_DESIGN.md b/SYSTEM_DESIGN.md index 061a0cd..efac33a 100644 --- a/SYSTEM_DESIGN.md +++ b/SYSTEM_DESIGN.md @@ -624,9 +624,11 @@ DATA-001C1 [#178](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/178) pau The server chooses initial timestamps. The current self-edit path sends a Firestore server timestamp, but the Rules source type-checks rather than independently proves that edit timestamp. Do not describe arbitrary profile edit timestamps as server-authoritative until a coordinated Rules/API issue closes that residual. -**AUTH-006G confirmed profile-name save result focus — SOURCE ONLY:** [#651](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/651) changes only confirmed full-name Save feedback and keyboard-focus settlement in My Account. Existing validation, synchronous one-attempt admission, name-only write payload, captured application/Firestore/identity/UID context, authoritative profile reread, and current-attempt checks remain unchanged. Only an exact current update followed by a current successful non-null reread renders the fixed result **Profile name saved.** The result contains no member-entered or returned name, email, UID, revision, provider detail, or caught value; it has status, polite live, atomic, programmatic-focus, bounded 320-pixel layout, and scoped 3-pixel `#005bd8` focus-outline semantics. The Profile-header DOM order is heading, result, then **Edit**; a two-column grid keeps the heading and Edit presentation on the first row while the result spans the second, so Tab after the focused result reaches Edit. After validation and exact save admission, separate pending and result focus refs may retain only the opaque profile generation and attempt ID, and only when the exact connected **Save** button owns focus. Exact current confirmed success transfers the matching token. The layout effect consumes the result intent before target checks, requires the matching current generation and attempt plus the connected result node, leaves an already-focused result alone, returns absent, body, document-root, or disconnected focus to the result, and preserves every other connected outside or in-Profile focus deliberately chosen while the save is pending. A valid unfocused or programmatic Save still shows the truthful result but never moves focus, including after its outside focus origin disappears. Initial load, validation failure, update rejection, missing or rejected confirmation read, profile reload, application/Firestore/identity/UID change, an unavailable-to-same-context generation change, a newer attempt, stale completion, unmount, and later rerender cannot show or reuse a stale result intent; **Edit**, a new admitted Save, profile load, and context change clear the prior result and obsolete intents. The result and focus handoff add no read, write, request, retry, provider call, log, or stored value. Existing unconfirmed-change copy and recovery remain unchanged; AUTH-006H [#653](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/653) separately owns unconfirmed-change alert focus, while validation error association, Edit-to-input focus, and Cancel-to-Edit focus remain separate outcomes. This adds one visible page-structure node and no data movement, permission, ownership, service contract, Function, Rule, schema, index, package, workflow, provider configuration, account, sign-in state, production-data action, deployment, publication, membership, dues, role, payment, entitlement, roster, photo query, facial recognition, matching, embedding, similarity, biometric processing, or live-behavior change. The #118 backend-first profile-repair proof remains separate. Directory availability remains byte-for-byte `false`, live #623 remains inert, and #507 retains every optional-directory privacy, authorization, staging, deployment/readback, availability-flip, publication, and live-proof gate. +**AUTH-006G confirmed profile-name save result focus — SOURCE ONLY:** [#651](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/651) changes only confirmed full-name Save feedback and keyboard-focus settlement in My Account. Existing validation, synchronous one-attempt admission, name-only write payload, captured application/Firestore/identity/UID context, authoritative profile reread, and current-attempt checks remain unchanged. Only an exact current update followed by a current successful non-null reread renders the fixed result **Profile name saved.** The result contains no member-entered or returned name, email, UID, revision, provider detail, or caught value; it has status, polite live, atomic, programmatic-focus, bounded 320-pixel layout, and scoped 3-pixel `#005bd8` focus-outline semantics. The Profile-header DOM order is heading, result, then **Edit**; a two-column grid keeps the heading and Edit presentation on the first row while the result spans the second, so Tab after the focused result reaches Edit. After validation and exact save admission, separate pending and result focus refs may retain only the opaque profile generation and attempt ID, and only when the exact connected **Save** button owns focus. Exact current confirmed success transfers the matching token. The layout effect consumes the result intent before target checks, requires the matching current generation and attempt plus the connected result node, leaves an already-focused result alone, returns absent, body, document-root, or disconnected focus to the result, and preserves every other connected outside or in-Profile focus deliberately chosen while the save is pending. A valid unfocused or programmatic Save still shows the truthful result but never moves focus, including after its outside focus origin disappears. Initial load, validation failure, update rejection, missing or rejected confirmation read, profile reload, application/Firestore/identity/UID change, an unavailable-to-same-context generation change, a newer attempt, stale completion, unmount, and later rerender cannot show or reuse a stale result intent; **Edit**, a new admitted Save, profile load, and context change clear the prior result and obsolete intents. The result and focus handoff add no read, write, request, retry, provider call, log, or stored value. Existing unconfirmed-change copy and recovery remain unchanged; AUTH-006H [#653](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/653) separately owns unconfirmed-change alert focus, AUTH-006I [#655](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/655) separately owns Edit-to-input focus, and validation error association and Cancel-to-Edit focus remain separate outcomes. This adds one visible page-structure node and no data movement, permission, ownership, service contract, Function, Rule, schema, index, package, workflow, provider configuration, account, sign-in state, production-data action, deployment, publication, membership, dues, role, payment, entitlement, roster, photo query, facial recognition, matching, embedding, similarity, biometric processing, or live-behavior change. The #118 backend-first profile-repair proof remains separate. Directory availability remains byte-for-byte `false`, live #623 remains inert, and #507 retains every optional-directory privacy, authorization, staging, deployment/readback, availability-flip, publication, and live-proof gate. -**AUTH-006H unconfirmed profile-name save result focus — SOURCE ONLY:** [#653](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/653) changes only keyboard-focus settlement for the existing unconfirmed full-name Save recovery in My Account. Validation, synchronous one-attempt admission, the name-only payload, captured application/Firestore/identity/UID context, the update, the required authoritative profile reread, current-attempt checks, the fixed message, the existing **Try profile again** action, exact service-call counts, and AUTH-006G confirmed-success behavior remain unchanged. After validation and exact save admission, the existing pending Save-focus token may contain only the opaque current profile generation and attempt ID, and only while the exact connected **Save** button owns focus. If that exact current update rejects, or if its required reread returns `null` or rejects, the current catch creates a minimal unconfirmed-result token for the current generation and attempt and transfers a matching pending focus token only to the separate unconfirmed focus ref before it clears the profile, editor, and success confirmation and enters the existing unavailable recovery. This never infers whether the update persisted. The reused alert says exactly **We could not confirm your profile change. Try the profile again before making another change.** and contains no member, profile, provider, or caught detail. Only the exact current save-unconfirmed state gives that alert `role="alert"`, assertive live, atomic, programmatic-focus, 100-percent width/minimum-zero/maximum-100-percent wrapping, and scoped 3-pixel `#005bd8` focus-outline semantics. Its message immediately precedes the enabled existing **Try profile again** button, so the alert announces uncertainty before the retry action and the button remains the next Tab stop. One layout effect consumes the matching intent before every target check. It requires the exact current generation and attempt, unavailable state, no profile, editor, or success confirmation, the byte-exact unconfirmed message, and the connected alert. It leaves an already-focused alert alone, restores focus only from no active element, the document body, the document root, or a disconnected element, and preserves every other connected focus deliberately chosen while the update or reread is pending. A valid unfocused or programmatic Save still shows the same unconfirmed recovery without moving focus, including after its outside focus origin disappears. Initial setup or profile-read failure, validation failure, application/Firestore/identity/UID or generation change, a newer attempt, obsolete update or reread settlement, reload, unmount, and later rerender cannot focus a stale unconfirmed result; **Try profile again** clears every old Save intent before starting its existing load. The handoff adds no read, write, request, retry, provider call, log, or stored value. It reuses the existing alert and retry control, adds no page node or data movement, and changes no service contract, permission, ownership, Function, Rule, schema, index, package, workflow, provider configuration, account, sign-in state, production data, deployment, publication, membership, dues, role, payment, entitlement, roster, photo query, facial recognition, matching, embedding, similarity, biometric processing, or live behavior. Settlement after the later **Try profile again** action, validation error association, Edit-to-input focus, and Cancel-to-Edit focus remain separate. The #118 backend-first profile-repair proof remains separate. Directory availability remains byte-for-byte `false`, live #623 remains inert, and #507 retains every optional-directory privacy, authorization, staging, deployment/readback, availability-flip, publication, and live-proof gate. +**AUTH-006H unconfirmed profile-name save result focus — SOURCE ONLY:** [#653](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/653) changes only keyboard-focus settlement for the existing unconfirmed full-name Save recovery in My Account. Validation, synchronous one-attempt admission, the name-only payload, captured application/Firestore/identity/UID context, the update, the required authoritative profile reread, current-attempt checks, the fixed message, the existing **Try profile again** action, exact service-call counts, and AUTH-006G confirmed-success behavior remain unchanged. After validation and exact save admission, the existing pending Save-focus token may contain only the opaque current profile generation and attempt ID, and only while the exact connected **Save** button owns focus. If that exact current update rejects, or if its required reread returns `null` or rejects, the current catch creates a minimal unconfirmed-result token for the current generation and attempt and transfers a matching pending focus token only to the separate unconfirmed focus ref before it clears the profile, editor, and success confirmation and enters the existing unavailable recovery. This never infers whether the update persisted. The reused alert says exactly **We could not confirm your profile change. Try the profile again before making another change.** and contains no member, profile, provider, or caught detail. Only the exact current save-unconfirmed state gives that alert `role="alert"`, assertive live, atomic, programmatic-focus, 100-percent width/minimum-zero/maximum-100-percent wrapping, and scoped 3-pixel `#005bd8` focus-outline semantics. Its message immediately precedes the enabled existing **Try profile again** button, so the alert announces uncertainty before the retry action and the button remains the next Tab stop. One layout effect consumes the matching intent before every target check. It requires the exact current generation and attempt, unavailable state, no profile, editor, or success confirmation, the byte-exact unconfirmed message, and the connected alert. It leaves an already-focused alert alone, restores focus only from no active element, the document body, the document root, or a disconnected element, and preserves every other connected focus deliberately chosen while the update or reread is pending. A valid unfocused or programmatic Save still shows the same unconfirmed recovery without moving focus, including after its outside focus origin disappears. Initial setup or profile-read failure, validation failure, application/Firestore/identity/UID or generation change, a newer attempt, obsolete update or reread settlement, reload, unmount, and later rerender cannot focus a stale unconfirmed result; **Try profile again** clears every old Save intent before starting its existing load. The handoff adds no read, write, request, retry, provider call, log, or stored value. It reuses the existing alert and retry control, adds no page node or data movement, and changes no service contract, permission, ownership, Function, Rule, schema, index, package, workflow, provider configuration, account, sign-in state, production data, deployment, publication, membership, dues, role, payment, entitlement, roster, photo query, facial recognition, matching, embedding, similarity, biometric processing, or live behavior. AUTH-006I [#655](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/655) separately owns Edit-to-input focus; settlement after the later **Try profile again** action, validation error association, and Cancel-to-Edit focus remain separate. The #118 backend-first profile-repair proof remains separate. Directory availability remains byte-for-byte `false`, live #623 remains inert, and #507 retains every optional-directory privacy, authorization, staging, deployment/readback, availability-flip, publication, and live-proof gate. + +**AUTH-006I profile-name Edit-to-input focus — SOURCE ONLY:** [#655](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/655) changes only keyboard-focus settlement when the existing full-name editor opens in My Account. **Edit** remains a native button shown only for a mounted current ready profile. **Full name** remains the existing native labeled input with the authoritative current name, its linked length description, `autocomplete="name"`, enabled state, 200-character maximum, and position before **Save** and **Cancel**. Every Edit activation replaces any older Edit-focus intent. Only the exact connected event `currentTarget` that owns `document.activeElement` records the opaque current profile generation before the existing handler clears AUTH-006G/H save feedback and opens the editor. An unfocused or programmatic Edit still opens the editor but records no intent and moves no focus. The implementation adds no React state, `autoFocus`, timeout, animation frame, text selection, or CSS. One layout effect consumes the generation intent before every check and requires the mounted matching generation, exact current application/Firestore/identity/UID profile context, ready state, current profile, open editor, no save in progress, and exact connected enabled input. It leaves an already-focused input alone, restores only absent, body, document-root, or disconnected focus to the input, and preserves every other connected focus, including **Sign out** or the in-Profile verification control. A missing or disabled input, obsolete context, closed editor, pending save, or deliberate connected focus consumes the intent without moving it, so later removal or rerender cannot trigger delayed focus. A focused Edit keeps the current name and makes Save the next ordinary Tab stop. Application, Firestore, identity, UID, profile-load or reload, **Try profile again**, Save, Cancel, and unmount paths clear or consume the intent. Entering Edit after AUTH-006G success clears the prior result and uses its authoritative reread name; AUTH-006G confirmed and AUTH-006H unconfirmed Save behavior, AUTH-006F one-attempt/context fences, validation, payload, and exact service-call counts remain unchanged. Editor entry runs no validation, read, write, request, retry, provider call, log, or storage action. The input uses the existing global native-control visible-focus rule, so `Account.css` and `index.css` remain byte-for-byte unchanged. The handoff adds no page node, data movement, permission, ownership, service contract, Function, Rule, schema, index, package, workflow, provider configuration, account, sign-in state, production data, deployment, publication, membership, dues, role, payment, entitlement, roster, photo query, facial recognition, matching, embedding, similarity, biometric processing, or live behavior. Cancel-to-Edit focus, validation-error association or focus, **Try profile again** settlement focus, and save-pending announcement remain separate. The #118 backend-first profile-repair proof remains separate. Directory availability remains byte-for-byte `false`, live #623 remains inert, and #507 retains every optional-directory privacy, authorization, staging, deployment/readback, availability-flip, publication, and live-proof gate. ### 8.0p Private profile thumbnail and officer-finder preference — SOURCE ONLY, NOT LIVE diff --git a/docs/officers/EVENTS_SHOP_MEMBERS.md b/docs/officers/EVENTS_SHOP_MEMBERS.md index cf19ce4..b15e55d 100644 --- a/docs/officers/EVENTS_SHOP_MEMBERS.md +++ b/docs/officers/EVENTS_SHOP_MEMBERS.md @@ -1070,9 +1070,10 @@ flowchart LR Setup -- "Yes" --> Read["Read this member's profile"] Setup -- "No" --> New["Create one pending profile\nwith an empty phone field"] New --> Read - Read --> Name["Display and edit name"] + Read --> Name["Display current name and Edit"] Read --> Pause["Do not display or accept phone"] - Name --> Save["Save one name-only update"] + Name --> Editor["Edit opens the Full name editor\nFocused Edit restores otherwise-lost focus to Full name"] + Editor --> Save["Save one name-only update"] Save --> Rules["Firebase Rules check the name-only update"] Rules --> Updated{"Current update completes?"} Updated -- "No" --> Unconfirmed["Show the fixed unconfirmed alert\nRestore otherwise-lost focus only if Save owned it"] @@ -1084,7 +1085,7 @@ flowchart LR Pause --> Existing["Existing stored value stays unchanged"] ``` -In words: signup or profile recovery creates a missing pending profile without copying a phone from Firebase Auth; My Account shows and edits the member's name, does not display or accept a phone number, and leaves every existing stored phone value unchanged; after one current name-only update completes and its current profile reread returns a profile, My Account shows **Profile name saved.** and restores otherwise-lost focus to that result only when Save owned focus, while an update rejection or missing or rejected reread shows the fixed unconfirmed alert, restores otherwise-lost focus to that alert only when Save owned focus, and keeps **Try profile again** as the next Tab stop; the reviewed Rules deny a browser phone change. +In words: signup or profile recovery creates a missing pending profile without copying a phone from Firebase Auth; My Account shows the current name and Edit, and a focused Edit that opens the existing Full name editor restores otherwise-lost focus to that input while an unfocused Edit opens it without moving focus; My Account does not display or accept a phone number and leaves every existing stored phone value unchanged; after one current name-only update completes and its current profile reread returns a profile, My Account shows **Profile name saved.** and restores otherwise-lost focus to that result only when Save owned focus, while an update rejection or missing or rejected reread shows the fixed unconfirmed alert, restores otherwise-lost focus to that alert only when Save owned focus, and keeps **Try profile again** as the next Tab stop; the reviewed Rules deny a browser phone change. Officer steps after every prerequisite has proof: @@ -1213,7 +1214,7 @@ Officer source-review procedure for AUTH-006G [#651] confirmed profile-name Save 96. Confirm result focus creates no log or stored value. 97. Confirm AUTH-006H [#653](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/653) separately owns unconfirmed-save alert focus. 98. Confirm validation error association remains separate. -99. Confirm Edit-to-input focus remains separate. +99. Confirm AUTH-006I [#655](https://github.com/Run-MPRC/Run-MPRC.github.io/issues/655) separately owns Edit-to-input focus. 100. Confirm Cancel-to-Edit focus remains separate. 101. Confirm the source diff changes no Account service or profile payload. 102. Confirm the source diff changes no Function or Firestore Rule. @@ -1245,7 +1246,7 @@ Officer source-review procedure for AUTH-006G [#651] confirmed profile-name Save **Expected result:** one exact current full-name update plus its successful non-null authoritative profile reread renders exactly **Profile name saved.** between the Profile heading and Edit, with status, polite live, atomic, programmatic-focus, bounded-layout, and visible-outline semantics. Only an admitted Save that owned focus records the current generation and attempt ID. Exact current confirmed success alone transfers that intent. The layout effect consumes it before target checks. Absent, body, document-root, or disconnected focus returns to the connected result; retained result focus is left alone; and every other connected focus is preserved. An unfocused Save still shows the truthful result without moving focus. Initial load, validation, update rejection, missing or rejected reread, profile reload, context or generation change, newer attempt, stale work, unmount, and later rerender cannot show or focus stale success. Edit, a new Save, profile load, and context change clear the old result. The handoff creates no extra read, write, request, retry, provider call, log, or stored value. Existing unconfirmed-change recovery remains unchanged. The behavior is source only and **NOT LIVE** until separately published and verified. Directory availability remains `false`, and live #623 remains inert. -**Stop conditions:** a real account, profile, name, email, UID, screenshot, or production sign-in; direct production Firebase access; a provider configuration or production-data action; success copy other than **Profile name saved.**; a result shown before a current non-null authoritative reread; a result containing a member, provider, or error detail; missing status, live, atomic, programmatic-focus, bounded-layout, or visible-outline semantics; DOM order other than heading, result, Edit; a focused exact current Save whose confirmed result receives no otherwise-lost focus; an unfocused Save that moves focus; a connected control that loses deliberately selected focus; an intent containing anything beyond generation and attempt ID; transfer before current confirmed success; delayed focus on a later render; stale application, Firestore, identity, UID, generation, attempt, reread, or unmounted work that shows or focuses success; an extra read, write, request, retry, provider call, log, or stored value; changed validation, payload, confirmation-reread, AUTH-006F, unconfirmed-change, failure/retry, Edit-to-input focus, Cancel-to-Edit focus, directory, or Strava behavior; a Function, Rule, schema, index, package, workflow, provider, account, sign-in, production-data, deployment, publication, membership, dues, role, payment, entitlement, roster, photo-query, facial-recognition, matching, embedding, similarity, or biometric change; an availability flip; use of a terminal or test harness by the backup officer; or a claim that source, tests, merge, preview, or green CI proves the behavior live. +**Stop conditions:** a real account, profile, name, email, UID, screenshot, or production sign-in; direct production Firebase access; a provider configuration or production-data action; success copy other than **Profile name saved.**; a result shown before a current non-null authoritative reread; a result containing a member, provider, or error detail; missing status, live, atomic, programmatic-focus, bounded-layout, or visible-outline semantics; DOM order other than heading, result, Edit; a focused exact current Save whose confirmed result receives no otherwise-lost focus; an unfocused Save that moves focus; a connected control that loses deliberately selected focus; an intent containing anything beyond generation and attempt ID; transfer before current confirmed success; delayed focus on a later render; stale application, Firestore, identity, UID, generation, attempt, reread, or unmounted work that shows or focuses success; an extra read, write, request, retry, provider call, log, or stored value; changed validation, payload, confirmation-reread, AUTH-006F, unconfirmed-change, failure/retry, AUTH-006I Edit-to-input focus, Cancel-to-Edit focus, directory, or Strava behavior; a Function, Rule, schema, index, package, workflow, provider, account, sign-in, production-data, deployment, publication, membership, dues, role, payment, entitlement, roster, photo-query, facial-recognition, matching, embedding, similarity, or biometric change; an availability flip; use of a terminal or test harness by the backup officer; or a claim that source, tests, merge, preview, or green CI proves the behavior live. **Success proof:** record the exact #651 issue, reviewed pull request and commit; trustworthy unchanged-runtime result with one expected failure and 243 skipped tests plus test-only digest `df02329a39ee24b127f801d4a8726f5748bdff0b992b8706a16456bbeb2fd66f`; green 22-of-22 AUTH-006G block; green 265-of-265 Account suite; passing type-check, scoped ESLint, and diff-check; bounded grid, containment, and focus-style evidence; runtime diff digest `c00a6c7e0486dcdfbb0e9d0a4b4c48215f03e3e8c72634fc1f2b17142c08c1d5`; relevant full frontend, repository Node, diagnostic build, unchanged lint-baseline, workflow, and security checks when complete; independent frontend/accessibility, security/privacy/race, and backup-officer reviews; and exact-main CI if merged. Record source, tests, merge, website publication, exact `runmprc.com` revision, Firebase deployment, provider configuration, account/sign-in change, production-data action, and live behavior separately. Record the unchanged `false` directory availability and unchanged #623 deploy separately. Source and tests do not prove merge. Merge does not prove publication. Publication does not prove `runmprc.com`, Firebase, provider, account, data, or live behavior. @@ -1388,7 +1389,7 @@ Officer source-review procedure for AUTH-006H [#653] unconfirmed profile-name Sa **Expected result:** an exact current update rejection, `null` confirmation reread, or rejected confirmation reread keeps the existing fixed unconfirmed alert and enabled **Try profile again** action. Every exact current unconfirmed result records only its generation and attempt ID to own that result. Only an admitted Save that owned focus records a pending focus token. The exact current catch transfers that pending token only when it matches the result. One layout effect consumes it before target checks and requires the matching generation and attempt, unavailable state, no profile, editor, or success confirmation, the exact message, and a connected alert. The alert has assertive, atomic, programmatic-focus, bounded-wrapping, and scoped visible-outline semantics. Its message immediately precedes the retry action. Absent, body, document-root, or disconnected focus returns to the alert; retained alert focus is left alone; and any other connected focus is preserved. An unfocused Save still shows the recovery without moving focus. Initial setup/read failure, validation, reload, context or generation change, newer attempt, stale update/reread work, unmount, and later rerender cannot focus stale recovery. Try clears the old intent before loading. The handoff creates no extra read, write, request, retry, provider call, log, or stored value. It does not claim whether the update persisted. AUTH-006G success remains unchanged. The behavior is source only and **NOT LIVE** until separately published and verified. Directory availability remains `false`, and live #623 remains inert. -**Stop conditions:** a real account, profile, name, email, UID, screenshot, or production sign-in; direct production Firebase access; a provider configuration or production-data action; unconfirmed copy other than **We could not confirm your profile change. Try the profile again before making another change.**; an alert containing a member, provider, response, or caught detail; missing alert, assertive, atomic, programmatic-focus, bounded-wrapping, or visible-outline semantics; the retry action appearing before the message or not remaining the next Tab stop; focus sent directly to the retry action; a focused exact current Save whose unconfirmed alert receives no otherwise-lost focus; an unfocused Save that moves focus; a connected control that loses deliberately selected focus; a token containing anything beyond generation and attempt ID; transfer outside an exact current catch; a browser claim that the write failed or persisted; delayed focus on a later render; stale application, Firestore, identity, UID, generation, attempt, update, reread, reload, or unmounted work that shows or focuses recovery; an extra read, write, request, retry, provider call, log, or stored value; changed validation, payload, AUTH-006F, AUTH-006G success, Try-profile-again result focus, validation error association, Edit-to-input focus, Cancel-to-Edit focus, directory, or Strava behavior; a Function, Rule, schema, index, package, workflow, provider, account, sign-in, production-data, deployment, publication, membership, dues, role, payment, entitlement, roster, photo-query, facial-recognition, matching, embedding, similarity, or biometric change; an availability flip; use of a terminal or test harness by the backup officer; or a claim that source, tests, merge, preview, or green CI proves the behavior live. +**Stop conditions:** a real account, profile, name, email, UID, screenshot, or production sign-in; direct production Firebase access; a provider configuration or production-data action; unconfirmed copy other than **We could not confirm your profile change. Try the profile again before making another change.**; an alert containing a member, provider, response, or caught detail; missing alert, assertive, atomic, programmatic-focus, bounded-wrapping, or visible-outline semantics; the retry action appearing before the message or not remaining the next Tab stop; focus sent directly to the retry action; a focused exact current Save whose unconfirmed alert receives no otherwise-lost focus; an unfocused Save that moves focus; a connected control that loses deliberately selected focus; a token containing anything beyond generation and attempt ID; transfer outside an exact current catch; a browser claim that the write failed or persisted; delayed focus on a later render; stale application, Firestore, identity, UID, generation, attempt, update, reread, reload, or unmounted work that shows or focuses recovery; an extra read, write, request, retry, provider call, log, or stored value; changed validation, payload, AUTH-006F, AUTH-006G success, Try-profile-again result focus, validation error association, AUTH-006I Edit-to-input focus, Cancel-to-Edit focus, directory, or Strava behavior; a Function, Rule, schema, index, package, workflow, provider, account, sign-in, production-data, deployment, publication, membership, dues, role, payment, entitlement, roster, photo-query, facial-recognition, matching, embedding, similarity, or biometric change; an availability flip; use of a terminal or test harness by the backup officer; or a claim that source, tests, merge, preview, or green CI proves the behavior live. **Success proof:** record the exact #653 issue, reviewed pull request and commit; trustworthy unchanged-runtime result with one expected failure and 287 skipped tests plus test-only RED digest `25ca6ccd34dcd0acbef28727d05f05f72079ed6bd55ee0ae3b11b43c04a4f056`; green 23-of-23 AUTH-006H block; green 288-of-288 Account suite; passing type-check, scoped ESLint, and diff-check; bounded wrapping and focus-style evidence; runtime-only digest `6e9d9a8da90d1a0c22144d7e64b27fdb05735893177393ae74bd5e3ff18e6170`; final test-diff digest `895cb6ef708ed0714d53a0cf2dc7b7336104cf111ab2d5f591078051772d81ea`; combined three-path digest `b84d60f1166e924062d65e7a9834d9a94a567098561e9e4ba955d6f3f3b1350b`; relevant full frontend, repository Node, diagnostic build, unchanged lint-baseline, workflow, and security checks when complete; independent frontend/accessibility, security/privacy/race, and backup-officer reviews; and exact-main CI if merged. Record source, tests, merge, website publication, exact `runmprc.com` revision, Firebase deployment, provider configuration, account/sign-in change, production-data action, and live behavior separately. Record the unchanged `false` directory availability and unchanged #623 deploy separately. Source and tests do not prove merge. Merge does not prove publication. Publication does not prove `runmprc.com`, Firebase, provider, account, data, or live behavior. @@ -1396,6 +1397,142 @@ Officer source-review procedure for AUTH-006H [#653] unconfirmed profile-name Sa **Escalation:** membership lead plus identity/privacy and platform/security owners. Use the private incident path if a real profile detail appeared, the alert exposed a caught or provider value, focus crossed application or account contexts, a stale result appeared, deliberately selected connected focus was stolen, the browser claimed whether an unconfirmed write persisted, an intent retained private or service data, a focus handoff created a read or write, or any source-only behavior became connected or live without separate approval. +Officer source-review procedure for AUTH-006I [#655] profile Edit-to-full-name-input focus — source only, **NOT LIVE**: + +**Purpose:** let a backup officer verify from specialist-prepared evidence that a focused current **Edit** which opens the existing name editor returns otherwise-lost focus to **Full name**, while an unfocused Edit opens the same editor without moving focus, without using a real account, changing Firebase, or publishing the website. + +**Approvers:** membership lead, identity/privacy owner, and platform/security owner. + +**Prerequisites:** released #653/PR #654 supplies the current AUTH-006G/H Save-result focus behavior, and released #531/PR #534 supplies the one-attempt and account-context fences. Ask the platform owner or testing specialist for the exact #655 source candidate, the trustworthy unchanged-runtime named failure, the complete passing AUTH-006I named result, the full Account result, type-check and scoped-lint results, the unchanged CSS and global native-control focus evidence, the exact diff check and digests, and a redacted written synthetic-behavior report. The specialist runs every command and records the evidence. The backup officer reviews only those written records and prepared artifacts; the backup officer does not use a terminal or test harness. In this procedure, a profile generation is one internal number that identifies the current page and service context; it contains no member detail. Keep the optional-directory availability value `false`. Do not sign in to production, use a real name, inspect a real profile, call production Firebase, configure a provider, or change production data. + +1. Keep the profile Edit-to-input focus behavior marked **SOURCE ONLY, NOT LIVE**. +2. Ask the platform owner for the exact #655 issue. +3. Ask the platform owner for the reviewed pull request when one exists. +4. Ask the platform owner for the exact candidate or merge commit. +5. Ask the platform owner for the two-path executable diff. +6. Ask the platform owner for runtime-only digest `a5bddd143b62af06b2357b30f3045117009b3bc827b1753bbb7a34f6c42cd5b4`. +7. Ask the platform owner for final test-diff digest `b5966220783abca0c5cfed832752471f167f0dcf48d636ab2eafd2c07896c3f3`. +8. Ask the platform owner for combined two-path digest `24dcd9003bb12fdc3abccfa19820682473644fa38d8a1b606a8e824277a51dd1`. +9. Ask the testing specialist for the trustworthy unchanged-runtime result with one expected failure, 288 skipped tests, and 289 total tests. +10. Ask the testing specialist for test-only RED digest `3de742e72b8ef0d847c0a5811da0000631ecd926dcd1e9e5c589932e9b78efb4`. +11. Confirm that unchanged runtime opened the editor but left focus on the document body. +12. Ask the testing specialist for the green 17-of-17 AUTH-006I result. +13. Ask the testing specialist for the green 305-of-305 full Account result. +14. Ask the testing specialist for the passing type-check result. +15. Ask the testing specialist for the scoped ESLint result with no output. +16. Ask the testing specialist for the passing diff-check result. +17. Ask the specialist to confirm unchanged `Account.css` blob `7aa2f241fc7e38b3ccdafa3b408a1a85f5a6026c`. +18. Ask the specialist to confirm unchanged `index.css` blob `9241cb4ed3400d5ba0b4d89cdfb86089ff01c15c`. +19. Ask the specialist for written evidence of the existing global native-control visible-focus rule. +20. Ask the testing specialist for the redacted written synthetic-behavior report. +21. Confirm the report names the specialist who ran the checks. +22. Confirm the backup officer used no terminal or test harness. +23. Confirm every profile and account in the evidence is made up. +24. Confirm no real name, email, UID, profile, or screenshot appears in the evidence. +25. Confirm no production Firebase or provider call appears in the evidence. +26. Confirm Edit remains a native button. +27. Confirm Edit appears only for the mounted current ready profile. +28. Confirm Full name remains a native input. +29. Confirm the input keeps the visible **Full name** label. +30. Confirm the input keeps its linked **Up to 200 characters.** description. +31. Confirm the input keeps browser name autocomplete. +32. Confirm the input keeps the 200-character maximum. +33. Confirm the input is enabled when the editor opens. +34. Confirm the input keeps the authoritative current profile name. +35. Confirm DOM order remains Full name, Save, then Cancel. +36. Confirm the next ordinary Tab after Full name reaches Save. +37. Confirm the existing global visible-focus rule applies without an Account or global CSS edit. +38. Confirm the handoff adds no visible page node. +39. Confirm every Edit activation replaces any older Edit-focus intent. +40. Confirm only the exact Edit event target may create the intent. +41. Confirm that Edit target must still be connected. +42. Confirm that Edit target must own browser focus. +43. Confirm the intent records only the current profile generation. +44. Confirm the intent records no name, email, UID, profile, revision, attempt, request, response, error, provider value, or photo data. +45. Confirm an unfocused Edit still opens the editor. +46. Confirm an unfocused or programmatic Edit moves no focus now or on a later render. +47. Confirm entering Edit clears any prior confirmed or unconfirmed Save feedback. +48. Confirm entering Edit after AUTH-006G success uses the authoritative reread name. +49. Confirm an unfocused Edit after AUTH-006G success still clears that result without moving focus now or later. +50. Confirm the implementation uses no `autoFocus`. +51. Confirm the implementation uses no timeout or animation frame. +52. Confirm the implementation changes no text selection. +53. Confirm the implementation adds no React state. +54. Confirm one layout effect consumes the intent before every target check. +55. Confirm the current profile generation must match before focus. +56. Confirm the profile context must still be mounted before focus. +57. Confirm the application, Firestore, identity service, and UID must still match before focus. +58. Confirm the profile state must be ready before focus. +59. Confirm a current profile must be present before focus. +60. Confirm the name editor must be open before focus. +61. Confirm no Save may be in progress before focus. +62. Confirm the exact Full name input must be connected and enabled before focus. +63. Confirm an input that already retained focus is not focused again. +64. Confirm absent focus returns to Full name. +65. Confirm document-body focus returns to Full name. +66. Confirm document-root focus returns to Full name. +67. Confirm disconnected focus returns to Full name. +68. Confirm connected focus outside Profile remains focused. +69. Confirm connected focus on the in-Profile verification control remains focused. +70. Confirm removing a deliberately focused control later cannot trigger delayed input focus. +71. Confirm one successful handoff moves focus at most once across same-context rerenders. +72. Confirm a missing input consumes the intent without later focus. +73. Confirm a disabled input consumes the intent without later focus. +74. Confirm a Firebase-application-only change cannot reuse an older intent. +75. Confirm a Firestore-service-only change cannot reuse an older intent. +76. Confirm an identity-service-only change cannot reuse an older intent. +77. Confirm a UID-only change cannot reuse an older intent. +78. Confirm profile load or reload clears the older intent. +79. Confirm an unavailable-to-same-context generation change cannot reuse an older intent. +80. Confirm **Try profile again** clears the older intent before loading. +81. Confirm Save clears the Edit-focus intent before validation or work. +82. Confirm Cancel clears the Edit-focus intent. +83. Confirm unmount clears the intent and makes the old input inert. +84. Confirm no later target removal or rerender can reuse a consumed intent. +85. Confirm entering Edit performs no profile validation. +86. Confirm entering Edit performs no profile update. +87. Confirm entering Edit creates no extra profile setup, read, or registration call. +88. Confirm the focus handoff creates no request, retry, provider call, log, or stored value. +89. Confirm AUTH-006F one-attempt and context fences remain unchanged. +90. Confirm AUTH-006G confirmed-save copy and focus remain unchanged. +91. Confirm AUTH-006H unconfirmed-save copy, retry, and focus remain unchanged. +92. Confirm the name-only payload and existing service-call counts remain unchanged. +93. Confirm Cancel-to-Edit focus remains a separate outcome. +94. Confirm validation-error association and focus remain separate outcomes. +95. Confirm Try-profile-again settlement focus remains a separate outcome. +96. Confirm save-pending announcement remains a separate outcome. +97. Confirm the source diff changes no Function, Firestore Rule, schema, index, service, package, or workflow. +98. Confirm the source diff changes no provider configuration, account, sign-in state, or production data. +99. Confirm the source diff adds no membership, dues, role, payment, entitlement, or roster claim. +100. Confirm the source diff adds no photo query, facial recognition, matching, embedding, similarity, or biometric processing. +101. Confirm active #616 Strava runtime blobs remain `2eb48945061ab5ad2a896c6835f308cefd7fe63a` and `d1792d390bd57608fecab84341a6e5916fcf8196`. +102. Confirm active #616 OAUTH-001A2L/RISK-024 documentation remains unchanged. +103. Confirm the optional-directory availability blob remains `295909b0df8dd7c54a0f164f99d4943656aece4b`, whose value is byte-for-byte `false`. +104. Confirm the last verified production directory deployment remains inert #623 deploy `6a7e072f8f346b0008510d29`. +105. Confirm #507 still owns every optional-directory connection and live-proof gate. +106. Record the source change as its own state. +107. Record the named test results as their own state. +108. Record whether the change merged as its own state. +109. Record whether any website artifact was published as its own state. +110. Record the exact `runmprc.com` revision as its own state. +111. Record whether Firebase was deployed as its own state. +112. Record whether an outside provider was configured as its own state. +113. Record whether an account or sign-in state changed as its own state. +114. Record whether production data changed as its own state. +115. Record whether the Edit-to-input handoff is live as its own state. +116. Record whether connected profile-photo or officer-finder behavior is live as its own state. +117. Stop before changing Firebase, a provider, an account, production data, directory availability, or the live website. + +**Expected result:** a connected focused current **Edit** opens the existing editor, clears prior Save feedback, keeps the authoritative current name, and returns otherwise-lost focus once to the connected enabled **Full name** input. Full name remains a native labeled input with its linked description, autocomplete, maximum length, existing global visible-focus rule, and position before Save and Cancel; Save is the next ordinary Tab stop. An unfocused or programmatic Edit opens the editor without moving focus. The layout effect consumes the generation-only intent before checks, preserves any deliberate connected focus, and cannot reuse stale or consumed intent after a target disappears, context changes, profile reloads, Save, Cancel, Try, unmount, or a later render. Editor entry creates no validation, read, write, request, retry, provider call, log, stored value, CSS change, or page node. AUTH-006F/G/H and exact calls remain unchanged. The behavior is source only and **NOT LIVE** until separately published and verified. Directory availability remains `false`, and live #623 remains inert. + +**Stop conditions:** a real account, profile, name, email, UID, screenshot, or production sign-in; direct production Firebase access; a provider configuration or production-data action; a non-native Edit or Full name control; changed input value, label, description, autocomplete, maximum length, enabled state, or input/Save/Cancel order; missing existing visible-focus behavior; any Account or global CSS change; an intent containing more than the profile generation; `autoFocus`, timeout, animation frame, text selection, or new React state; an unfocused or programmatic Edit that moves focus; a focused current Edit whose otherwise-lost focus does not reach Full name; focus sent directly to Save or Cancel; a connected deliberate target that loses focus; delayed or repeated focus after a missing, disabled, disconnected, stale, removed, or consumed target; stale application, Firestore, identity, UID, profile generation, load, reload, Try, Save, Cancel, or unmounted work that moves focus; validation, a read, write, request, retry, provider call, log, stored value, or page node caused by entry focus; changed AUTH-006F/G/H, payload, call count, Cancel-to-Edit focus, validation-error behavior, Try settlement, save-pending announcement, directory, or Strava behavior; a Function, Rule, schema, index, service, package, workflow, provider, account, sign-in, production-data, deployment, publication, membership, dues, role, payment, entitlement, roster, photo-query, facial-recognition, matching, embedding, similarity, or biometric change; an availability flip; use of a terminal or test harness by the backup officer; or a claim that source, tests, merge, preview, or green CI proves the behavior live. + +**Success proof:** record the exact #655 issue, reviewed pull request and commit; trustworthy unchanged-runtime result with one expected failure, 288 skipped tests, 289 total tests, focus left on the body, and test-only RED digest `3de742e72b8ef0d847c0a5811da0000631ecd926dcd1e9e5c589932e9b78efb4`; green 17-of-17 AUTH-006I block; green 305-of-305 Account suite; passing type-check, zero-output scoped ESLint, and diff-check; unchanged `Account.css` and `index.css` blobs plus existing global visible-focus evidence; runtime-only digest `a5bddd143b62af06b2357b30f3045117009b3bc827b1753bbb7a34f6c42cd5b4`; final test-diff digest `b5966220783abca0c5cfed832752471f167f0dcf48d636ab2eafd2c07896c3f3`; combined two-path digest `24dcd9003bb12fdc3abccfa19820682473644fa38d8a1b606a8e824277a51dd1`; green full frontend result with 18-of-18 suites and 1,314-of-1,314 tests; green 105-of-105 repository Node result; passing unchanged lint baseline and diagnostic build; workflow and security checks when complete; independent frontend/accessibility, security/privacy/race, and backup-officer reviews; and exact-main CI if merged. Record source, tests, merge, website publication, exact `runmprc.com` revision, Firebase deployment, provider configuration, account/sign-in change, production-data action, and live behavior separately. Record the unchanged `false` directory availability and unchanged #623 deploy separately. Source and tests do not prove merge. Merge does not prove publication. Publication does not prove `runmprc.com`, Firebase, provider, account, data, or live behavior. + +**Undo:** use one reviewed frontend-and-documentation revert or safe roll-forward. No Firebase, provider, account, sign-in, production-data, or CSS undo is needed because #655 changes source focus behavior and documentation only. An undo must preserve AUTH-006F/G/H, the authoritative current name and name-only payload, the inert directory default, and active #616 work. Do not undo by editing or deleting a profile or account. + +**Escalation:** membership lead plus identity/privacy and platform/security owners. Use the private incident path if a real profile detail appeared, focus crossed application or account contexts, stale or delayed focus appeared, deliberately selected connected focus was stolen, an intent retained private or service data, editor entry created a read or write, or any source-only behavior became connected or live without separate approval. + ## Provider-neutral membership authority — SOURCE ONLY, UNUSED **Status: NOT AVAILABLE YET** diff --git a/src/pages/account/Account.test.tsx b/src/pages/account/Account.test.tsx index 2c0bbdb..c0b976b 100644 --- a/src/pages/account/Account.test.tsx +++ b/src/pages/account/Account.test.tsx @@ -559,6 +559,330 @@ describe('Account profile recovery', () => { expect(screen.queryByTestId('strava-section')).not.toBeInTheDocument(); }); + describe('AUTH-006I profile Edit-to-input focus', () => { + const servicesA = { + firebaseResources: { app, firestore }, + identityService: { signOut, resendVerificationEmail }, + }; + const userB = { + uid: 'synthetic-user-b', + email: 'member-b@example.test', + role: 'unverified' as const, + }; + + async function readyEdit() { + const edit = await screen.findByRole('button', { name: 'Edit' }); + await waitFor(() => expect(listMyRegistrations).toHaveBeenCalledTimes(1)); + return edit; + } + + function focusInstances(focus: jest.SpyInstance) { + return focus.mock.instances as HTMLElement[]; + } + + test('hands otherwise-lost keyboard Edit focus to the existing full-name input', async () => { + renderAccount(); + const edit = await readyEdit(); + edit.focus(); + fireEvent.click(edit, { detail: 0 }); + + const input = screen.getByLabelText('Full name'); + expect(screen.queryByRole('button', { name: 'Edit' })).not.toBeInTheDocument(); + expect(input).toHaveValue(PROFILE.fullName); + expect(input).toHaveFocus(); + expect(input).toBeEnabled(); + expect(ensureMyProfile).toHaveBeenCalledTimes(1); + expect(getMyProfile).toHaveBeenCalledTimes(1); + expect(listMyRegistrations).toHaveBeenCalledTimes(1); + expect(updateMyProfile).not.toHaveBeenCalled(); + }); + + test.each([ + ['no active element', null], + ['the document root', document.documentElement], + ['a disconnected element', document.createElement('button')], + ])('restores Edit-to-input focus from %s', async (_label, lostFocus) => { + renderAccount(); + const edit = await readyEdit(); + const focus = jest.spyOn(HTMLElement.prototype, 'focus'); + const activeElement = jest.spyOn(document, 'activeElement', 'get') + .mockImplementation(() => (edit.isConnected ? edit : lostFocus)); + try { + fireEvent.click(edit, { detail: 0 }); + } finally { + activeElement.mockRestore(); + } + + const input = screen.getByLabelText('Full name'); + expect(focusInstances(focus)).toContain(input); + expect(updateMyProfile).not.toHaveBeenCalled(); + }); + + test('does not refocus an input that already owns focus', async () => { + renderAccount(); + const edit = await readyEdit(); + const focus = jest.spyOn(HTMLElement.prototype, 'focus'); + const activeElement = jest.spyOn(document, 'activeElement', 'get') + .mockImplementation(() => (edit.isConnected + ? edit + : document.querySelector('#profile-full-name'))); + try { + fireEvent.click(edit, { detail: 0 }); + } finally { + activeElement.mockRestore(); + } + + const input = screen.getByLabelText('Full name'); + expect(focusInstances(focus)).not.toContain(input); + }); + + test('opens for an unfocused programmatic activation without moving focus now or later', async () => { + const view = renderAccount(); + const edit = await readyEdit(); + const outside = document.createElement('button'); + document.body.append(outside); + outside.focus(); + outside.remove(); + expect(document.body).toHaveFocus(); + + fireEvent.click(edit); + + const input = screen.getByLabelText('Full name'); + expect(input).not.toHaveFocus(); + expect(document.body).toHaveFocus(); + view.rerender(accountView()); + expect(input).not.toHaveFocus(); + expect(document.body).toHaveFocus(); + expect(updateMyProfile).not.toHaveBeenCalled(); + }); + + test.each([ + ['outside Profile', () => { + const target = document.createElement('button'); + document.body.append(target); + return target; + }], + ['inside Profile', () => screen.getByRole('button', { + name: 'Request another verification email', + })], + ])('preserves connected deliberate focus %s and consumes the intent', async ( + _label, + getTarget, + ) => { + const view = renderAccount(); + const edit = await readyEdit(); + const deliberateTarget = getTarget(); + const focus = jest.spyOn(HTMLElement.prototype, 'focus'); + const activeElement = jest.spyOn(document, 'activeElement', 'get') + .mockImplementation(() => (edit.isConnected ? edit : deliberateTarget)); + try { + fireEvent.click(edit, { detail: 0 }); + } finally { + activeElement.mockRestore(); + } + + const input = screen.getByLabelText('Full name'); + expect(focusInstances(focus)).not.toContain(input); + if (!deliberateTarget.closest('.account-content')) deliberateTarget.remove(); + view.rerender(accountView()); + expect(focusInstances(focus)).not.toContain(input); + }); + + test('consumes a successful focus handoff once across later same-context renders', async () => { + const view = renderAccount(); + const edit = await readyEdit(); + edit.focus(); + const focus = jest.spyOn(HTMLElement.prototype, 'focus'); + + fireEvent.click(edit, { detail: 0 }); + + const input = screen.getByLabelText('Full name'); + expect(focusInstances(focus).filter((target) => target === input)).toHaveLength(1); + view.rerender(accountView()); + expect(focusInstances(focus).filter((target) => target === input)).toHaveLength(1); + }); + + test.each([ + [ + 'Firebase app', + { + services: { + firebaseResources: { app: { name: 'synthetic-app-b' }, firestore }, + identityService: servicesA.identityService, + }, + user: USER, + }, + ], + [ + 'Firestore service', + { + services: { + firebaseResources: { app, firestore: { name: 'synthetic-firestore-b' } }, + identityService: servicesA.identityService, + }, + user: USER, + }, + ], + [ + 'identity service', + { + services: { + firebaseResources: { app, firestore }, + identityService: { + signOut: jest.fn(), + resendVerificationEmail: jest.fn(), + }, + }, + user: USER, + }, + ], + [ + 'UID', + { + services: servicesA, + user: userB, + }, + ], + ])('cannot reuse a consumed Edit focus intent after a %s-only change', async ( + _label, + nextContext, + ) => { + const nextProfile = { + ...PROFILE, + uid: nextContext.user.uid, + email: nextContext.user.email, + fullName: 'Next Context Synthetic Member', + }; + (getMyProfile as jest.Mock) + .mockResolvedValueOnce(PROFILE) + .mockResolvedValueOnce(nextProfile); + const view = renderAccount(); + const edit = await readyEdit(); + const deliberateTarget = document.createElement('button'); + document.body.append(deliberateTarget); + const focus = jest.spyOn(HTMLElement.prototype, 'focus'); + const activeElement = jest.spyOn(document, 'activeElement', 'get') + .mockImplementation(() => (edit.isConnected ? edit : deliberateTarget)); + try { + fireEvent.click(edit, { detail: 0 }); + } finally { + activeElement.mockRestore(); + } + const oldInput = screen.getByLabelText('Full name'); + expect(focusInstances(focus)).not.toContain(oldInput); + deliberateTarget.remove(); + + (useServiceLocator as jest.Mock).mockReturnValue({ + services: nextContext.services, + isReady: true, + }); + view.rerender(accountView(nextContext.user)); + expect(await screen.findByText(nextProfile.fullName)).toBeInTheDocument(); + expect(screen.queryByLabelText('Full name')).not.toBeInTheDocument(); + expect(focusInstances(focus)).not.toContain(oldInput); + }); + + test('uses the authoritative reread name after AUTH-006G success', async () => { + const confirmed = { + ...PROFILE, + fullName: 'Confirmed Synthetic Member', + }; + (getMyProfile as jest.Mock) + .mockResolvedValueOnce(PROFILE) + .mockResolvedValueOnce(confirmed); + renderAccount(); + fireEvent.click(await readyEdit()); + fireEvent.change(screen.getByLabelText('Full name'), { + target: { value: confirmed.fullName }, + }); + fireEvent.click(screen.getByRole('button', { name: 'Save' })); + expect(await screen.findByText('Profile name saved.')).toBeInTheDocument(); + + const edit = screen.getByRole('button', { name: 'Edit' }); + edit.focus(); + fireEvent.click(edit, { detail: 0 }); + + expect(screen.queryByText('Profile name saved.')).not.toBeInTheDocument(); + expect(screen.getByLabelText('Full name')).toHaveValue(confirmed.fullName); + expect(screen.getByLabelText('Full name')).toHaveFocus(); + expect(updateMyProfile).toHaveBeenCalledTimes(1); + expect(getMyProfile).toHaveBeenCalledTimes(2); + }); + + test('clears an AUTH-006G result without focusing for an unfocused Edit', async () => { + const confirmed = { + ...PROFILE, + fullName: 'Confirmed Programmatic Synthetic Member', + }; + (getMyProfile as jest.Mock) + .mockResolvedValueOnce(PROFILE) + .mockResolvedValueOnce(confirmed); + const view = renderAccount(); + fireEvent.click(await readyEdit()); + fireEvent.change(screen.getByLabelText('Full name'), { + target: { value: confirmed.fullName }, + }); + fireEvent.click(screen.getByRole('button', { name: 'Save' })); + expect(await screen.findByText('Profile name saved.')).toBeInTheDocument(); + + const edit = screen.getByRole('button', { name: 'Edit' }); + const outside = document.createElement('button'); + document.body.append(outside); + outside.focus(); + outside.remove(); + expect(document.body).toHaveFocus(); + fireEvent.click(edit); + + const input = screen.getByLabelText('Full name'); + expect(screen.queryByText('Profile name saved.')).not.toBeInTheDocument(); + expect(input).toHaveValue(confirmed.fullName); + expect(input).not.toHaveFocus(); + expect(document.body).toHaveFocus(); + + view.rerender(accountView()); + expect(input).not.toHaveFocus(); + expect(updateMyProfile).toHaveBeenCalledTimes(1); + expect(getMyProfile).toHaveBeenCalledTimes(2); + }); + + test('keeps the existing input semantics, keyboard order, and global focus rule', async () => { + renderAccount(); + const edit = await readyEdit(); + edit.focus(); + fireEvent.click(edit, { detail: 0 }); + + const input = screen.getByLabelText('Full name'); + const save = screen.getByRole('button', { name: 'Save' }); + const cancel = screen.getByRole('button', { name: 'Cancel' }); + expect(input.tagName).toBe('INPUT'); + expect(input).toHaveAccessibleDescription('Up to 200 characters.'); + expect(input).toHaveAttribute('autocomplete', 'name'); + expect(input).toHaveAttribute('maxlength', '200'); + expect(input.compareDocumentPosition(save)) + .toBe(Node.DOCUMENT_POSITION_FOLLOWING); + expect(save.compareDocumentPosition(cancel)) + .toBe(Node.DOCUMENT_POSITION_FOLLOWING); + + const globalCss = readFileSync(join(__dirname, '../../index.css'), 'utf8'); + expect(globalCss).toMatch(/:focus-visible,[^{]*\{[^}]*outline:\s*3px\s+solid\s+var\(--color-secondary\);[^}]*outline-offset:\s*3px;[^}]*box-shadow:\s*0\s+0\s+0\s+6px\s+var\(--color-gray-600\);[^}]*\}/); + expect(updateMyProfile).not.toHaveBeenCalled(); + }); + + test('leaves an already-consumed focus handoff inert after unmount', async () => { + const view = renderAccount(); + const edit = await readyEdit(); + edit.focus(); + fireEvent.click(edit, { detail: 0 }); + const input = screen.getByLabelText('Full name'); + expect(input).toHaveFocus(); + + view.unmount(); + + expect(input.isConnected).toBe(false); + expect(updateMyProfile).not.toHaveBeenCalled(); + }); + }); + describe('AUTH-006G profile-save success feedback and focus', () => { const CONFIRMED_PROFILE = { ...PROFILE, diff --git a/src/pages/account/Account.tsx b/src/pages/account/Account.tsx index af5b70d..5c67175 100644 --- a/src/pages/account/Account.tsx +++ b/src/pages/account/Account.tsx @@ -302,6 +302,8 @@ export function AccountContent({ const profileSaveResultRef = useRef(null); const profileSaveUnconfirmedResultRef = useRef(null); const profileSaveButtonRef = useRef(null); + const profileEditFocusIntentRef = useRef(null); + const profileFullNameInputRef = useRef(null); const profileContextGenerationRef = useRef(0); const profileContextMountedRef = useRef(false); const currentProfileContextRef = useRef({ @@ -339,6 +341,7 @@ export function AccountContent({ pendingProfileSaveFocusIntentRef.current = null; profileSaveResultFocusIntentRef.current = null; profileSaveUnconfirmedFocusIntentRef.current = null; + profileEditFocusIntentRef.current = null; return () => { profileContextMountedRef.current = false; @@ -347,6 +350,7 @@ export function AccountContent({ pendingProfileSaveFocusIntentRef.current = null; profileSaveResultFocusIntentRef.current = null; profileSaveUnconfirmedFocusIntentRef.current = null; + profileEditFocusIntentRef.current = null; }; }, [firebaseApp, firebaseFirestore, identityService, user.uid]); @@ -393,6 +397,7 @@ export function AccountContent({ pendingProfileSaveFocusIntentRef.current = null; profileSaveResultFocusIntentRef.current = null; profileSaveUnconfirmedFocusIntentRef.current = null; + profileEditFocusIntentRef.current = null; try { await ensureMyProfile(activeServices.firebaseResources.app); @@ -468,6 +473,7 @@ export function AccountContent({ } async function handleSave() { + profileEditFocusIntentRef.current = null; if (!services) return; const validation = validateMemberProfileFields({ fullName }); if (!validation.valid) { @@ -723,6 +729,33 @@ export function AccountContent({ && registrationsContext.firebaseFirestore === firebaseFirestore && registrationsContext.generation === profileContextGenerationRef.current; + useLayoutEffect(() => { + const focusIntent = profileEditFocusIntentRef.current; + profileEditFocusIntentRef.current = null; + if (focusIntent === null) return; + if ( + !profileContextMountedRef.current + || profileContextGenerationRef.current !== focusIntent + || !profileBelongsToCurrentContext + || profileState !== 'ready' + || !profile + || !editing + || saving + ) return; + + const target = profileFullNameInputRef.current; + if (!target?.isConnected || target.disabled) return; + const { activeElement } = document; + if (activeElement === target) return; + if ( + activeElement + && activeElement.isConnected + && activeElement !== document.body + && activeElement !== document.documentElement + ) return; + target.focus(); + }, [editing, profile, profileBelongsToCurrentContext, profileState, saving]); + if (currentSignOutOutcome) { const isRetry = currentSignOutOutcome.status === 'retry'; const isTerminal = currentSignOutOutcome.status === 'terminal'; @@ -842,7 +875,12 @@ export function AccountContent({ {!editing && profileState === 'ready' && profile && (