StackTrading Docs

SRS: UC_4.6.2 — Settings: Profile & Preferences

UC_4.6.2: Settings — Profile & Preferences

FieldValue
BA in ChargeAnh Hoang
Date Created2026-08-11
Versionv14
Document ReferencesRFQ_ Stack Trading Prop Tech V7.pdf (§Page 36 — Get User Profile / Update User Profile) · RFQ_ Website and Dashboard Implementation V7.pdf (§Part C — Settings & Profile Module) · Zapier Integration V7.pdf (§Users table schema — profile_photo_url, phone_number, ui_preferences)

Document References

#Original DocumentKey Sections Used
1RFQ_ Stack Trading Prop Tech V7.pdf§Page 36 — Get User Profile (GET user_id{profile_photo_url, email, phone_number, ui_preferences}, "Fetches profile and configuration data from the Users table to populate the Account Settings screen") and Update User Profile (POST user_id, profile_photo_url (Optional), email (Optional), phone_number (Optional), ui_preferences (Optional JSON) → HTTP 200 OK, "Supports image uploads via S3 signed URL execution before submission")
2RFQ_ Website and Dashboard Implementation V7.pdf§Part C — Settings & Profile Module ("Includes Change Password, AI Chat Toggle, Billing Portal Link, Update Email, Update Phone… Must include a 'Reduce Motion' accessibility toggle")
3Zapier Integration V7.pdf§Users table schema — profile_photo_url (String), phone_number (String), ui_preferences (JSON Object)
3bSotatek BE team — API review, 2026-08-21Three changes to the User Profiles endpoints, superseding the verb and shape given in row 1: (a) POSTPATCH for RESTful correctness; (b) the photo upload and its submission are one uninterruptible operation, not "upload before submission"; (c) the single write endpoint is split into PATCH /profile-photo and PATCH /profile. Recorded in BR_4.6.2.15.
3cBA / client answers, 2026-08-27 (QnA Init Docs — UC_4.6.1–4.6.4: Settings, Phần II)Closes the four items UC_4.6.2 carried into launch. [PP-EP-01] confirmed — the BE team's PATCH + endpoint-split + merged-upload proposal is adopted in full, retiring the WBS's PUT /profile and the RFQ's POST Update User Profile (BR_4.6.2.15). Phone change ships with no cool-down and no security gate — regex + uniqueness, then a direct write (BR_4.6.2.12). Dashboard and Discord avatars are fully independent — StackTrading never reads an avatar or a name from the Discord API, to avoid broken images and expired CDN links (BR_4.6.2.13). One discrepancy in the same answer — PATCH /profile writing ui_preferences — is not adopted, and is recorded in BR_4.6.2.15.
3dClient answer, 2026-09-03 ([CTRL-EP-05] — the purpose of ui_preferences)Settles what the field is for, and therefore why this screen leaves it alone. Verbatim: "There are currently no active parameters to parse for this field today. It is intentionally included in the schema strictly as a future-proofing measure. Please maintain this column as an optional JSON object. This ensures the frontend team can easily save arbitrary cross-device layout preferences in future updates without requiring structural database schema changes later." The same answer re-confirms that the Desk Manager keeps a dedicated endpoint "because it controls core operational logic and costs" and that Reduce Motion "does not require an API and should be using local storage". → the field is a deliberately dormant column, not a gap; BR_4.6.2.15 updated, payload unchanged.
4QnA from clients — STAGE 2: Dashboard & Evaluation (Settings)Client-confirmed answers on Definition, Trigger, Business logic, UI/UX, Endpoints for the Profile & Preferences category (Stage 2 - Dashboard & Fomula (Settings).csv, rows 303–330; Adrian Stack, 2026-08-10)
5BA design set + direct BA instruction, 2026-08-117 screen frames (default / saved / duplicate phone / same-as-current phone / invalid format / Crop Image modal / photo removed) — Figma node 7878-116947. Source of the avatar-removal fallback rule and of the three Settings-specific phone error strings.

Design asset note: the 7 frames above are not yet committed to References/Wireframe/Stage 2/Settings/; this document cites the Figma node until the BA commits them (Ref: QnA_init_docs.md A-19).


1. Overview

FieldContent
IDUC_4.6.2
Use CaseSettings — Profile & Preferences
DescriptionInside the Account screen, the trader opens the Profile & Preferences tab to view and maintain their personal profile. The General section exposes exactly three items: Profile photo (upload with crop, or remove — editable), Email (read-only — it is the account's unique identity key and is bound to the trading-gateway account and the blacklist, so it cannot be changed after checkout), and Phone (editable, reusing the checkout phone validation block in full — input structure, format/length rules, the system-wide uniqueness check and the error copy; there are no Settings-specific phone strings any more, Ref: BR_4.6.2.3). The screen has two independent commit paths (Ref: BR_4.6.2.14): the photo commits immediately via PATCH /profile-photo — clicking [Done] in the Crop Image modal uploads and applies the new avatar on the spot, and the previously stored image is hard-deleted; the phone is staged and committed only on [Save Changes], via PATCH /profile. The two endpoints are separate by design (Ref: BR_4.6.2.15). A photo-only change therefore never marks the form dirty and never enables [Save Changes]. The saved photo is the single source for the trader's avatar everywhere in the Dashboard — sidebar user chip and the My POD widget on the LIVE Community page (Ref: BR_4.6.2.13). The profile is global per user — identical for SIM and LIVE accounts, no environment branching (Ref: BR_4.6.2.10). The Controls panel (AI Chat / Reduce Motion) rendered alongside every Settings tab is not owned by this UC (Ref: BR_4.6.2.11).
Zapier Flow
Zapier Table
3rd PartyAWS S3 (profile photo object storage — uploaded directly by the browser via a backend-issued signed URL)

Design: Figma — Profile & Preferences, node 7878-116947

2. Trigger

  • The trader clicks the user chip in the sidebar footer (displaying their initials + level, e.g. SD / Lvl 5) and selects Settings from the dropdown (Ref: UC_4.1.2 §2 Screen Description, row 6), then selects the Profile & Preferences tab (Ref: BR_4.6.2.1).

3. Pre-conditions

  • The trader is authenticated and inside the Dashboard shell (Ref: UC_4.1.1 §1 Overview).
  • A Users record exists for the trader, populated at checkout with email, full name, and phone_number (Ref: UC_2.8.3 — Phase 2 Account Claim).

4. Post-conditions

The two commit paths reach their own post-conditions independently (Ref: BR_4.6.2.14).

After a photo commit (Crop Image [Done], or a confirmed [Remove Photo]):

  • The Users record reflects the new profile_photo_url — the new S3 object URL, or null after a removal. No other profile column is touched.
  • The previously stored S3 object is hard-deleted; there is no image history to roll back to (Ref: BR_4.6.2.6).
  • Every surface that renders the trader's avatar — the sidebar user chip and the My POD widget on the LIVE Community page — reflects the new photo, or the trader's initials if the photo was removed (Ref: BR_4.6.2.13).
  • The form's dirty state is unchanged — a photo commit neither enables nor resets [Save Changes] / [Cancel] (Ref: BR_4.6.2.8).
  • The trader has been told the write succeeded: the success toast Ref: TS-DASH-01"Changes saved successfully" — is shown once the overlay closes (Ref: BR_4.6.2.16).

After a phone commit ([Save Changes] returns HTTP 200):

  • The Users record reflects the submitted phone_number; every other profile column is unchanged.
  • The form returns to a clean (non-dirty) state — [Cancel] is hidden and [Save Changes] is Disabled (Ref: BR_4.6.2.8).

5. Basic Flow

  1. The trader lands on the Account screen and selects the Profile & Preferences tab.
  2. Frontend calls GET /profile and receives {profile_photo_url, email, phone_number, ui_preferences}.
  3. The General section renders: the avatar (the stored photo, or the trader's initials when profile_photo_url IS NULL — Ref: BR_4.6.2.7), the read-only Email, and the Phone field split into dial code + number. ui_preferences from the response is not consumed by this screen (Ref: BR_4.6.2.11).
  4. The form starts clean: [Cancel] is hidden and [Save Changes] is Disabled (Ref: BR_4.6.2.8).
  5. The trader edits the phone number. The form becomes dirty → [Cancel] appears and [Save Changes] becomes Enabled.

    Changing or removing the photo is a separate, self-committing path that runs outside this flow — Ref: §6 Alternative Flow A / Alternative Flow B and BR_4.6.2.14. A photo-only change leaves the form clean and [Save Changes] Disabled.

  6. On the phone field's out-click, the frontend runs, in order: format/length check (Ref: BR_4.6.2.3) → same-as-current check (Ref: BR_4.6.2.5) → uniqueness check (Ref: BR_4.6.2.4). The first failing check renders its inline error and re-Disables [Save Changes].
  7. The trader clicks [Save Changes].
  8. Frontend calls PATCH /profile with {user_id, phone_number}. profile_photo_url is never part of this payload — the photo has its own endpoint (Ref: BR_4.6.2.15, §6 Alternative Flow A step 4).
  9. On HTTP 200: success toast Ref: TS-DASH-01 is displayed and the form resets to clean per §4 Post-conditions.

6. Alternative Flows

Alternative Flow A — Change Photo

  1. The trader clicks [Change Photo] → the browser's native file picker opens.
  2. The frontend validates the selected file against BR_4.6.2.6. On failure, the matching error from Ref: CR-07 §7.2 is shown and the flow stops here — the avatar is unchanged and the form does not become dirty.
  3. On success, the Crop Image modal opens with a fixed 1:1 crop frame; the trader may pan and zoom the image within the frame (no rotate).
  4. The trader clicks [Done] → the photo is committed immediately, with no involvement from [Save Changes]:
    1. The modal closes and the blocking loading overlay Ref: OV-09"Updating avatar..." — covers the screen.
    2. The frontend requests a signed upload URL from the backend and PUTs the cropped binary directly to S3 (Ref: BR_4.6.2.6).
    3. The frontend calls PATCH /profile-photo with {user_id, profile_photo_url} — a photo-only endpoint; any phone edit currently staged in the form is not sent and is not affected (Ref: BR_4.6.2.15).
    4. On HTTP 200 the backend hard-deletes the previously stored S3 object (Ref: BR_4.6.2.6).
    5. The overlay closes, the new avatar is rendered on this screen and on every other avatar surface (Ref: BR_4.6.2.13), and the success toast Ref: TS-DASH-01"Changes saved successfully" — is shown (Ref: BR_4.6.2.16). [Remove Photo] becomes visible if it was hidden.
    6. The form's dirty state is untouched — if the trader had not edited the phone, [Save Changes] stays Disabled and [Cancel] stays hidden (Ref: BR_4.6.2.14).
  5. The trader clicks [Cancel] instead → the modal closes and the selection is discarded. Nothing is uploaded, the previously displayed avatar is retained, and the form's dirty state is left exactly as it was before the modal opened.

Alternative Flow B — Remove Photo

  1. [Remove Photo] is rendered only when the trader currently has a photo (profile_photo_url IS NOT NULL); it is hidden entirely otherwise.
  2. The trader clicks [Remove Photo] → the confirmation popup Ref: CF-03 is shown. Because the removal commits immediately and is not reversible via [Cancel], an explicit confirmation is required (BA decision, 2026-08-21). The popup shows the title "Remove your photo?" with no body text, and two vertically stacked buttons: [Confirm] on top (Primary), [Cancel] below (Secondary).
  3. The trader clicks [Cancel] in the popup → the popup closes and nothing happens; the avatar and the form's dirty state are unchanged.
  4. The trader clicks [Confirm] → the removal is committed immediately:
    1. The blocking loading overlay Ref: OV-09 covers the screen.
    2. The frontend calls PATCH /profile-photo with {user_id, profile_photo_url: null} — the photo only; a staged phone edit is not sent (Ref: BR_4.6.2.15).
    3. On HTTP 200 the backend hard-deletes the stored S3 object (Ref: BR_4.6.2.6).
    4. The overlay closes, the avatar reverts to the trader's initials (Ref: BR_4.6.2.7) on this screen and every other avatar surface (Ref: BR_4.6.2.13), the success toast Ref: TS-DASH-01 is shown (Ref: BR_4.6.2.16), and [Remove Photo] is hidden.
    5. The form's dirty state is untouched (Ref: BR_4.6.2.14).
  5. The removal cannot be undone from the UI — the previous image is gone from S3 and the trader must upload a new one.

7. Exceptional Flow

#ConditionSystem behaviour
1GET /profile fails (network error or HTTP 5xx) at §5 step 2The General section does not render its values. Error toast Ref: TE-SYS-01.
2Signed-URL request or the S3 PUT fails at §6 Alternative Flow A step 4.2The photo commit is aborted: PATCH /profile-photo is not called, the overlay Ref: OV-09 closes, and the previous avatar is restored — the cropped result is discarded, not held as a preview. Error toast: "Failed to upload file. Please try again later" Ref: CR-07 §7.2. The old S3 object is untouched. Retry = re-run [Change Photo] from the start. The form's dirty state and any staged phone edit are unaffected.
3The signed URL expires between issuance and the S3 PUTThe frontend requests a fresh signed URL and retries the PUT once. If the retry also fails, behaviour is identical to row 2.
4PATCH /profile-photo fails (network error or HTTP 5xx) at §6 Alternative Flow A step 4.3 or §6 Alternative Flow B step 4.2The overlay closes, the previous avatar is restored, and error toast Ref: TE-SYS-01 is shown. profile_photo_url is not written and no S3 object is deleted — the newly uploaded object may be left orphaned and is cleaned up backend-side. The form's dirty state and any staged phone edit are unaffected.
5PATCH /profile returns HTTP 409 (phone taken by another account between the out-click check and submit)Inline error Ref: IN-06"This phone number is already registered to another account.", the same string the checkout block uses — is rendered under the Phone field, [Save Changes] is Disabled, and the form stays dirty. The backend is the authority on uniqueness — the out-click check is only the first line of defence (Ref: BR_4.6.2.4).
6PATCH /profile fails (network error or HTTP 5xx) at §5 step 8Error toast Ref: TE-SYS-01. phone_number is not persisted, the form keeps the entered value and stays dirty. Any photo already committed earlier in the session is not rolled back — the two paths are independent (Ref: BR_4.6.2.14).
7Session token expired (HTTP 401) on any call in this UCError toast Ref: TE-AUTH-01 → redirect to the Auth0 login. Unsaved changes are lost.

8. Business Rules

BR_4.6.2.1: Screen Entry Point and Tab Context

The Account screen is reached only through the sidebar footer user chip → dropdown → Settings (Ref: UC_4.1.2 §2 Screen Description, row 6). The chip renders the trader's avatar plus their current level — collapsed (avatar over Lvl 5) or expanded (avatar · Full Name · Level 15). The avatar is the saved profile photo, falling back to initials when none is set (Ref: BR_4.6.2.7, BR_4.6.2.13).

On entry, the default active tab is Connections & Credentials — the first entry in the tab list. Profile & Preferences becomes active only when the trader selects it. The full tab list and its owning UCs:

TabOwning UC
Connections & CredentialsUC_4.6.1 — also the default active tab on entry
Market Data ManagementUC_4.17.1 — Market Data Management (not yet documented in this repo). LIVE-only screen — hidden or rendered read-only for any trader in the simulation phase (Source: QnA from clients — Settings, Market Data Management row, Adrian Stack 2026-08-10).
Profile & PreferencesUC_4.6.2 (this document)
BillingUC_4.6.3 — Invoice History table only; no billing portal and no third party (Ref: BR_4.6.3.2). Unlike Market Data Management, this tab is available to SIM and LIVE alike.
Account ActionsNot a single UC — the tab hosts two independent sections, each owned separately and each split by environment:- Reset Associate TrackUC_4.11.1 (SIM). No LIVE counterpart — a LIVE failure follows the Re-Buy path (UC_4.15.3, not yet documented in this repo).- Resign & Close AccountUC_4.11.2 (SIM) · UC_4.17.2 (LIVE, not yet documented in this repo).

Note on the UC_4.6.x range: the WBS assigns only UC_4.6.1UC_4.6.4 to STAGE 2: DASHBOARD - COMMON / Settings, and UC_4.6.4 is Controls (AI chat & Reduce motion) (SRS published) — Ref: BR_4.6.2.11. There is no UC_4.6.5, and Account Actions no longer sits in the common range at all. The containing folder has been renamed UC_4.6.1-4.6.5UC_4.6.1-4.6.4 to match the WBS; every link in the repo was remapped accordingly.

BR_4.6.2.2: Field Editability Matrix

The General section exposes exactly the three items below. No other profile attribute is rendered on this screen.

ItemEditableRationale
Profile photoYes — upload (with crop) and removeSource: QnA from clients — Settings, Profile & Preferences Definition row ("Photo (avatar upload - editable)")
EmailNo — read-onlyIt is the account's unique identity key, bound to the trading-gateway account and to the blacklist at checkout time. Changing it post-checkout carries data-integrity risk for little value, so it is locked for launch (Source: QnA from clients — Settings, Profile & Preferences Business logic row, Adrian Stack). Email is therefore never sent to either write endpoint, even though the RFQ's endpoint signature lists it as an optional parameter (Ref: BR_4.6.2.15).
PhoneYesSource: QnA from clients — Settings, Profile & Preferences Definition + Business logic rows ("Changing Phone Number is ok", Adrian Stack)

Full Name is likewise immutable, and is not rendered on this screen at all. It is still read from the Users record to derive the avatar initials (Ref: BR_4.6.2.7).

BR_4.6.2.3: Phone Format Validation — Reuses the Checkout Block

The phone field reuses the checkout validation block verbatim: two-part input (dial code selector, default +1, scroll-only country list) + digits-only number input, E.164 length of 6–15 digits excluding the dial code, leading zeros auto-trimmed on out-click, validated on out-click and on submit. Ref: CR-09 §9.2.

There is no geo-blocking by region on this field — any country dial code is accepted (Source: QnA from clients — Settings, Profile & Preferences Definition row).

There is no deviation from CR-09 §9.2 — including the error copy. The field renders the shared strings verbatim; Settings has no phone strings of its own (BA instruction, 2026-08-24):

SituationTimingError stringSource
Number input left emptyOut-click / Submit"Please enter phone number"CR-09 §9.2
Invalid length or format (fails the digits-only / 6–15-digit E.164 check)Out-click / Submit"Please enter a valid phone number"CR-09 §9.2
Already registered to another accountOut-click / Submit"This phone number is already registered to another account."Ref: IN-06 — the checkout block's string (Ref: BR_4.6.2.4)
Identical to the currently saved numberOut-click / Submit"The new phone number cannot be the same as your current one"Ref: IN-10the only Settings-specific string, because the checkout field has no saved value to compare against (Ref: BR_4.6.2.5)

This reverses the 2026-08-11 decision recorded in QnA_init_docs.md A-09 / A-10 / A-12, which created the Settings-only strings IN-08 and IN-09. Both are now retired in list-toast-popup.md v3.13 and must not be referenced. IN-08's copy — "Invalid phone number format. Please check and try again" — is no longer used anywhere in the system. CR-09 §9.2 itself is unchanged: this UC conforms to it rather than editing it, so UC_2.8.3 (checkout) is behaviourally unaffected (BA instruction, 2026-08-24).

BR_4.6.2.4: Phone Uniqueness Across the System

A phone number may be associated with only one account. The check is the same one the checkout flow applies (Source: QnA from clients — Settings, Profile & Preferences Business logic row — Q: "Should we apply the same phone number validation logic here as well?" → A: "Yes").

  • Trigger: phone field out-click, and again on submit.
  • Authority: the backend is the final arbiter. The out-click check is only the first line of defence — a number that passes on out-click can still be claimed by another account before the trader clicks [Save Changes], in which case PATCH /profile returns HTTP 409 (Ref: §7 row 5).
  • Error display: inline error Ref: IN-06 under the Phone field — "This phone number is already registered to another account."; [Save Changes] stays Disabled until the value is edited to one that clears the check.

This is the same rule and the same string as the checkout rule BR_2.8.3.4: Phone Number Uniqueness (Ref: UC_2.8.3 §6) — both render Ref: IN-06. The Settings-only duplicate string IN-09 is retired (Ref: BR_4.6.2.3). Checkout behaviour is unchanged; only IN-06's trigger description was widened to name both screens.

BR_4.6.2.5: New Phone Number Must Differ From the Current One

The submitted phone number must not be identical to the one currently stored on the account.

  • Comparison basis: the fully normalised E.164 string — dial code plus number, with all display formatting characters stripped. Changing only the dial code while keeping the same digits (e.g. +1 5551234567+44 5551234567) therefore counts as a valid change, not a duplicate.
  • Where enforced: client-side, on out-click and on submit — no backend round trip is required.
  • Error display: inline error Ref: IN-10"The new phone number cannot be the same as your current one". [Save Changes] stays Disabled until the value differs.
  • Why this one string stays Settings-specific: the checkout phone field is filled for the first time and has no "currently saved" value, so neither CR-09 §9.2 nor the checkout block defines a counterpart to reuse (Ref: BR_4.6.2.3).

BR_4.6.2.6: Profile Photo — Upload Constraints, S3 Signed-URL Flow, and Old-Object Deletion

Upload constraints (the values CR-07 §7.1 requires this UC to specify):

CriterionValue
Maximum file count1
Allowed formats.jpg, .jpeg, .png, .webp
Max size per file4 MB

Both constraints are enforced client-side before the Crop Image modal opens; violations use the standard messages in Ref: CR-07 §7.2.

Upload mechanics — the image never transits the application backend (Source: RFQ_ Stack Trading Prop Tech V7.pdf §Page 36 — "Supports image uploads via S3 signed URL execution before submission"):

  1. The frontend requests a short-lived signed upload URL from the backend.
  2. The frontend PUTs the cropped image binary directly to S3 using that URL.
  3. The resulting S3 object URL is sent as profile_photo_url to PATCH /profile-photo — a photo-only endpoint that carries no other field (Ref: BR_4.6.2.15).

When these steps run. Steps 1–3 fire the moment the trader clicks [Done] in the Crop Image modal — not on [Save Changes] (Ref: §6 Alternative Flow A step 4, BR_4.6.2.14). The whole sequence runs behind the blocking overlay Ref: OV-09.

On "upload and submission are one step". The RFQ describes the upload as running "before submission" — i.e. as a preparatory step ahead of a separate, trader-initiated save. That separation no longer exists: [Done] is the only trader action, and steps 1–3 run as one uninterruptible sequence behind OV-09 with no intermediate state the trader can see, cancel, or leave un-submitted. The three network hops are an internal implementation detail of a single logical operation. The signed-URL mechanism itself is retained — the image binary still never transits the application backend (BE team decision, 2026-08-21 — Ref: BR_4.6.2.15).

Old-object deletion. A trader has exactly one profile image at any time; the system keeps no history. Once the PATCH /profile-photo write returns HTTP 200, the backend hard-deletes the previously stored S3 object in the same operation — on replacement (Alternative Flow A) and on removal (Alternative Flow B) alike (BA instruction, 2026-08-21). Consequences:

  • Deletion is conditional on the write succeeding. If the upload or the write fails, the old object is left intact and remains the trader's avatar (Ref: §7 rows 2 and 4).
  • A replaced or removed photo is unrecoverable from the UI. There is no undo and no [Cancel] path — the trader must upload the image again. This is why [Remove Photo] is gated by the confirmation popup Ref: CF-03.
  • An object uploaded to S3 for a commit whose PATCH /profile-photo then failed is orphaned; reclaiming it is a backend housekeeping concern with no user-facing effect and is out of scope for this UC.

BR_4.6.2.7: Avatar Fallback to Initials

Whenever profile_photo_url IS NULL — a brand-new account that never uploaded a photo, or an account whose photo was removed — the avatar renders the trader's initials derived from the Full Name held in the Users record. It never renders a generic placeholder figure (BA instruction, 2026-08-11 — Ref: QnA_init_docs.md A-04).

Derivation rule, applied to the Full Name captured at checkout:

CaseResultExample
Two or more wordsFirst letter of the first word + first letter of the last word, uppercasedTaylor SwiftTS; Nguyen Thi Quynh AnhNA
Exactly one wordFirst letter of that word, uppercasedMadonnaM
Full Name empty or nullFirst letter of the email local-part, uppercasedtrader@mail.comT

The same derivation applies on every surface that renders a trader avatar, not just this screen — see BR_4.6.2.13 for the full consumer list. It is applied per displayed user, so a POD member with no photo of their own shows their own initials (e.g. Marcus ChenMC) regardless of whether the viewing trader has a photo.

BR_4.6.2.8: Dirty-State Control of [Save Changes] and [Cancel]

The Phone field is the only input that makes this form dirty. The dial code and the phone number are the sole participants; the profile photo is not, because it commits on its own (Ref: BR_4.6.2.14).

Form state[Cancel][Save Changes]
Clean — phone unchanged since load or since the last successful saveHiddenDisabled
Dirty (phone edited), no inline error activeVisibleEnabled
Dirty (phone edited), at least one inline error active (a CR-09 §9.2 empty/format error, IN-06, or IN-10 — Ref: BR_4.6.2.3)VisibleDisabled
Photo changed or removed, phone untouchedHiddenDisabled

Clicking [Cancel] reverts the staged phone value and dial code back to the last successfully saved state and returns the form to clean. It does not — and cannot — revert a photo change, which was already persisted at the moment it was made. No confirmation popup is shown, because nothing about the phone has been persisted yet.

A change to the Controls panel toggles never affects this dirty state either (Ref: BR_4.6.2.11).

BR_4.6.2.9: Unsaved-Changes Navigation Warning

While the form is dirty — i.e. the phone has been edited (Ref: BR_4.6.2.8) — the trader is warned before their staged changes would be discarded. A committed photo change never triggers this warning, because there is nothing staged left to lose (Ref: BR_4.6.2.14):

  • Switching to another Settings tab, or navigating to another sidebar destination → confirmation popup Ref: CF-01, per CR-05. Choosing Leave discards the staged changes and completes the navigation; choosing Cancel keeps the trader on the tab with the form untouched.
  • Closing or reloading the browser tab → the browser's own native unload prompt. Its copy is controlled by the browser and cannot be customised, so CF-01 does not apply to this case.

BR_4.6.2.10: Profile Is Global Per User — No SIM/LIVE Branching

This screen behaves identically for SIM (Evaluation) and LIVE (funded) accounts: same fields, same editability, same validation, same endpoints. The profile is a property of the user, not of the trading environment (Source: QnA from clients — Settings, Profile & Preferences Definition row, LIVE column: "Same as SIM - the profile is global per user, no SIM/LIVE distinction").

Consequently no rule in this UC is gated on account status, level, or environment.

BR_4.6.2.11: Controls Panel Is Out of Scope

The Controls panel (Desk Manager and Reduce Motion toggles) is rendered next to the tab list on every Settings tab, including this one, but it is not owned by this UC. It belongs to UC_4.6.4 — Controls (AI chat & Reduce motion). The Desk Manager toggle writes through its own dedicated endpoint POST /ai-preference; the Reduce Motion toggle writes nothing to the backend at all — it is frontend-only, persisted per device, and POST /accessibility-preference is withdrawn (client answer 2026-08-24, Ref: BR_4.6.4.2).

Two consequences for this UC:

  • Toggling either switch saves immediately on flip. It does not route through this screen's [Save Changes] button and does not mark this form dirty (Ref: BR_4.6.2.8).
  • The ui_preferences object returned by GET /profile is not read by this screen, and ui_preferences is never sent to either of this screen's write endpoints — even though the RFQ endpoint signature lists it as an optional parameter (BA decision, 2026-08-11 — Ref: QnA_init_docs.md A-18). This boundary is unchanged by the 2026-08-24 client answer; note only that ui_preferences no longer has a consumer in UC_4.6.4 either, since Reduce Motion left the database and the Desk Manager uses the dedicated users.ai_enabled column. ✅ Re-confirmed 2026-09-03 — the client states the field has "no active parameters to parse… today" and exists "strictly as a future-proofing measure", so having no reader and no writer is the intended state, not an omission (Ref: BR_4.6.2.15, [CTRL-EP-05]).

BR_4.6.2.12: Change Frequency and Downstream Effects of a Phone Change

For launch, a phone number change is a plain field update with no additional ceremony:

  • No rate limit and no cool-down — the trader may change their phone number any number of times. Confirmed for launch 2026-08-27 ("ở bản launch, trader có thể thay đổi số điện thoại tự do mà không bị khóa thời gian"). Note the status of the source: the client's QnA row is still marked "STILL OPEN: no rate limit / cool-down was specified" — so this is a build decision taken in the absence of a client limit, not a limit the client has ruled out. If they later specify one, it lands here.
  • No security constraint of any kind — confirmed for launch 2026-08-27. A phone change passes exactly two gates and is then written straight to PostgreSQL: the checkout regex format check (Ref: BR_4.6.2.3) and the system-wide uniqueness check (Ref: BR_4.6.2.4). Nothing else stands between the trader and the write.
  • No OTP verification of the new number. Dashboard authentication is Auth0 passwordless over email, so the phone number is not currently an authentication factor.
  • No security-notification email to the previous address, and no audit-log entry beyond the Users record's own updated_at timestamp.
  • No propagation to Auth0, Rithmic, MT5, or TraderEvolution — the number lives only in the StackTrading Users table.

These four points define launch scope only. The phone number was originally collected at checkout to "enable Two-Factor Authentication (2FA) and coordinate emergency Drawdown Defense protocols" (Ref: UC_2.8.3 §7). If either capability is activated later, an unverified self-service phone change becomes an account-takeover vector and this rule must be revisited.

BR_4.6.2.13: Avatar Propagation — Consumer Surfaces

The photo saved on this screen is the single source for the trader's avatar across the whole Dashboard. There is no second avatar setting anywhere else (BA instruction, 2026-08-11 — Ref: QnA_init_docs.md A-20). Once PATCH /profile-photo returns HTTP 200, the new profile_photo_url is what every consumer surface below renders.

Consumer surfaceWhat it rendersOwning UC
Sidebar user chip — collapsed stateAvatar + level, stacked (e.g. avatar over Lvl 5). Before any photo is uploaded this is the initials block (e.g. SD over Lvl 5)UC_4.1.2
Sidebar user chip — expanded stateAvatar + Full Name + level on one row (e.g. photo · Sam Smith · Level 15)UC_4.1.2
My POD widget — Pod Leader card, Community page (LIVE)Leader's avatar + name + title and level (e.g. Marcus Chen · Senior trader - Lvl 12)UC_4.14.2 — Pod Mentorship Module (LIVE, not yet documented in this repo)
My POD widget — POD members list, Community page (LIVE)One avatar per member + name + track and level (e.g. Cameron Williamson · Associate - Lvl 3)UC_4.14.2 — Pod Mentorship Module (LIVE, not yet documented in this repo)

Mechanics of the propagation:

  • Read-through, not a push. Each surface reads the profile_photo_url of the user it is displaying, straight from the Users record. This UC writes one column and pushes nothing — there is no sync job, no fan-out event, and no per-surface copy of the image to keep consistent.
  • Fallback is per displayed user. Any surface showing a user whose profile_photo_url IS NULL renders that user's own initials per BR_4.6.2.7 — which is why POD member rows can show initials (MC) and a photo side by side in the same list.
  • Refresh timing. The sidebar user chip refreshes immediately on the photo commit's HTTP 200 (Ref: §6 Alternative Flow A step 4.5 / Alternative Flow B step 4.4), because it is part of the shell the trader is already on. Every other surface picks the new value up on its own next data fetch; this UC does not force a re-render of screens the trader is not currently viewing.
  • Level and name are not owned here. The level (Lvl 5, Level 15), the title/track label (Senior trader, Associate), and the Full Name shown next to the avatar all come from each consumer surface's own data contract. This UC contributes the image only.

Dashboard avatar vs. Discord avatar — closed 2026-08-27. The two are fully independent; Discord is never read.

The precedence question is resolved by removing the contest: StackTrading never pulls an avatar or a name from the Discord API, on any surface, as a primary value or as a fallback.

SurfaceAvatar shown
Web Dashboard — this screen, the sidebar user chip, the My POD widgetThe photo the trader uploaded here, or their initials when profile_photo_url IS NULL (Ref: BR_4.6.2.7). Nothing else is ever consulted.
Discord serverWhatever the trader has set on their own Discord accountexcept for a chat message sent from the LIVE Dashboard, which StackTrading posts through a Discord Webhook carrying the trader's profile_photo_url (Ref: the amendment below).
  • Rationale, stated by the client: pulling Discord's CDN image invites broken images and expired CDN links — Discord avatar URLs are not stable third-party assets, and a dead link on the sidebar chip or a POD member row would be a visible defect StackTrading could not fix.
  • A trader will therefore often show two different pictures — one on the Dashboard, one in Discord — and that is the intended outcome, not a sync gap to be closed later.
  • Consequence for UC_4.14.2 (Pod Mentorship, LIVE): the My POD widget's data contract is now constrained, not open. It renders profile_photo_url with an initials fallback like every other surface; a Discord-sourced secondary fallback is excluded, so that branch does not need designing there. Adding one would be a change request against this rule.
  • The same rule applies to names: the Full Name beside an avatar comes from the Users record, never from a Discord nickname or global display name (Ref: BR_4.5.2.6 — StackTrading does not manage Discord display names).

⚠️ Amendment — CR-20260903-001, 2026-09-03. The one-way boundary above is no longer absolute in the Discord direction.

Live-broadcast chat messages sent from the Dashboard now leave through a Discord Webhook that overrides username and avatar_url per payload, so that they render in Discord as the trader rather than as a bot transcript. Two clauses of this BR are affected:

Clause as closed 2026-08-27Status after the CR
"StackTrading neither reads nor influences" the Discord surface🔴 Superseded. profile_photo_url is now pushed to Discord on every Dashboard-sent chat message (Ref: BR_4.14.1.12)
"StackTrading never pulls an avatar or a name from the Discord API, on any surface, as a primary value or as a fallback"🔴 Superseded in principle — the CR's avatar priority 2 (Discord photo when profile_photo_url IS NULL) requires exactly such a read. ⚠️ Not yet confirmed by the client as a deliberate reversal, and the 2026-08-27 rationale ("expired CDN links... a dead link would be a visible defect") still stands technically. Tracked as UC_4.14.1 §10 item 12
Everything governing Dashboard surfaces — this screen, the sidebar chip, My POD, the initials fallback of BR_4.6.2.7, and the My POD constraint on UC_4.14.2🟢 Unchanged. No Dashboard surface reads Discord, and none renders a chat avatar

New constraint this creates on PATCH /profile-photo: the stored profile_photo_url must be publicly fetchable by Discord's servers, since Discord retrieves avatar_url itself. A private bucket or a signed URL with a TTL silently degrades every trader to the webhook's default image. BE confirmation pending — UC_4.14.1 §10 item 13.

BR_4.6.2.14: Photo and Phone Are Two Independent Commit Paths

The General section looks like one form but behaves as two unrelated transactions. This is the governing rule of the screen; BR_4.6.2.6 and BR_4.6.2.8 are its consequences (BA instruction, 2026-08-21).

Profile photoPhone
Commit triggerCrop Image [Done], or [Confirm] in Ref: CF-03[Save Changes]
EndpointPATCH /profile-photo {user_id, profile_photo_url}PATCH /profile {user_id, phone_number}
Marks the form dirty?NoYes
Enables [Save Changes]?NoYes
Revertible via [Cancel]?No — already persistedYes — while uncommitted
Progress feedbackBlocking overlay Ref: OV-09 during the commit, then Ref: TS-DASH-01 toast on success (Ref: BR_4.6.2.16)Ref: TS-DASH-01 toast on success

Rules that follow from this split:

  1. A photo-only session never lights up [Save Changes]. If the trader opens the tab, changes their avatar and changes nothing else, [Save Changes] stays Disabled and [Cancel] stays hidden for the whole session — there is nothing left to save.
  2. The two payloads never merge. They cannot — they are different endpoints (Ref: BR_4.6.2.15). profile_photo_url is never sent by [Save Changes], and phone_number is never sent by a photo commit. A phone value staged in the form is simply not part of the photo call, and survives it untouched.
  3. A failure on one path does not roll back the other (Ref: §7 rows 4 and 6).
  4. Order does not matter. The trader may commit a photo before, after, or in the middle of editing the phone; each path is evaluated on its own.
  5. The unsaved-changes guard covers the phone only. Navigating away with a committed photo but an untouched phone triggers no warning, because the form is clean (Ref: BR_4.6.2.9).

BR_4.6.2.15: API Contract — Endpoint Split and Verb

This screen talks to three endpoints. The write side deliberately deviates from the single Update User Profile operation described in RFQ_ Stack Trading Prop Tech V7.pdf §Page 36 (Sotatek BE team decision, API review 2026-08-21).

EndpointVerbPayloadCalled fromResponse
/profileGETuser_id§5 step 2 — screen load{profile_photo_url, email, phone_number, ui_preferences}
/profilePATCH{user_id, phone_number}[Save Changes] — §5 step 8HTTP 200 · 409 on duplicate phone
/profile-photoPATCH{user_id, profile_photo_url} — the S3 object URL, or null to removeCrop Image [Done] — §6 Alt Flow A step 4.3 · [Confirm] in CF-03 — §6 Alt Flow B step 4.2HTTP 200

The three deviations from the RFQ, and why:

#RFQ_ Stack Trading Prop Tech V7.pdf §Page 36This documentRationale
1POST Update User ProfilePATCH on both write endpointsThe operation is a partial update of an existing resource, never a creation. PATCH is the correct verb; POST was imprecise.
2"Supports image uploads via S3 signed URL execution before submission"Upload and submission are one operationThe RFQ wording assumes the trader submits the form separately after the upload has been prepared. That step no longer exists — [Done] is the only trader action and the whole chain runs behind OV-09 (Ref: BR_4.6.2.6, BR_4.6.2.14). The signed-URL mechanism itself is unchanged — the binary still never transits the application backend.
3One endpoint writes profile_photo_url, email, phone_number and ui_preferences togetherTwo endpoints, one field group eachThe photo and the phone commit on different triggers at different times (Ref: BR_4.6.2.14), so a shared endpoint would need every field optional and could not express "photo only" vs "phone only" cleanly. email is read-only (Ref: BR_4.6.2.2) and ui_preferences belongs to UC_4.6.4 (Ref: BR_4.6.2.11), so neither is written here at all.

Naming. Singular /profile and /profile-photo — this is a singleton resource scoped to the authenticated trader, so the plural form would be wrong. GET /profile keeps its existing name and shape; UC_4.6.4 also reads from it and is unaffected by this change.

[PP-EP-01] closed 2026-08-27. The BE team's proposal is adopted in full"Chuyển đổi toàn bộ sang chuẩn RESTful sử dụng PATCH /profile" and "Nhất trí với quyết định phân tách này". All three deviations above are confirmed as the built behaviour: PATCH on both write endpoints, the two-endpoint split, and the merged crop → signed URL → S3 → PATCH /profile-photo chain behind a single [Done]. The competing names are formally retired: the WBS's PUT /profile and the RFQ's POST Update User Profile are both superseded and must not be cited as the contract. WBS row 174 still carries PUT /profile and needs correcting.

ui_preferences — discrepancy resolved the same day; the payload is phone_number only. An earlier answer on 2026-08-27 had described PATCH /profile as updating "số điện thoại (phone_number) và cấu hình giao diện (ui_preferences)". The follow-up answer on UC_4.6.4 settles it the other way: because the Desk Manager keeps its own dedicated endpoint ("POST /ai-preference — không gộp chung vào profile nữa"), PATCH /profile is reduced to "Chỉ dùng để cập nhật số điện thoại" with the payload given as {"phone_number": "+84901234567"}. This document's payload therefore stands, for two reasons that both post-date the field's last live use:

  • A-18 (2026-08-11) drew the boundary explicitly: UC_4.6.2 does not read or write ui_preferences; it belongs to UC_4.6.4 (Ref: BR_4.6.2.11).
  • The client answer of 2026-08-24 left the column with no consumer at all. Reduce Motion is stored locally per device and never server-side; Desk Manager uses its own boolean users.ai_enabled via POST /ai-preference. So ui_preferences is not written by UC_4.6.4 either — writing it from this screen would resurrect a dead field and give it a writer but still no reader.

The column remains in the Zapier Users schema and in the GET /profile response shape, which is unchanged. ui_preferences now has neither a reader nor a writer anywhere in the Settings module — it is a dormant column, not a live field.

Confirmed by the client on 2026-09-03 — the column is dormant by design, and this is not a documentation gap. Asked directly what the field is for, the client answered: "There are currently no active parameters to parse for this field today. It is intentionally included in the schema strictly as a future-proofing measure. Please maintain this column as an optional JSON object. This ensures the frontend team can easily save arbitrary cross-device layout preferences in future updates without requiring structural database schema changes later." (Source: [CTRL-EP-05], QnA STAGE 2 — Settings §9 Controls & Concierge.) Three build consequences:

  • Keep the column and keep it in the contract. users.ui_preferences is JSONB, nullable / optional. GET /profile returns it as-is; a write endpoint that accepts it treats it as an optional pass-through with no validation of its inner shape. Nothing on the backend parses it in this release.
  • This screen still never sends it. The payload of PATCH /profile stays {user_id, phone_number} — the client's own answer scopes that endpoint to the phone number, and the field has no reader to serve.
  • The earlier "future use = change request" note is superseded. Adding a frontend-only, cross-device layout key later is explicitly pre-authorised by this answer — no DB migration and no CR. A key that carries backend logic or cost (anything a worker or a Zapier flow must read) is a different matter and does need a CR, because it makes the column live.

user_id stays in the payload — confirmed 2026-08-28. The 2026-08-27 payload examples omitted it ({"phone_number": …}, {"profile_photo_url": …}) while POST /ai-preference in the same answer carried it. The inconsistency is resolved in favour of keeping user_id on all three write endpointsPATCH /profile, PATCH /profile-photo and POST /ai-preference"để đảm bảo tính nhất quán cao nhất với tài liệu đặc tả RFQ gốc". The payload table above stands as written; the omission in those examples was shorthand, not a contract change.

This is a consistency decision, not an authorisation one: the endpoints remain authenticated, and user_id in the body must never be trusted as the authority for whose record is written. BE resolves the acting trader from the session and rejects a mismatch — a payload user_id that differs from the authenticated trader is an error, never an instruction to write someone else's row.

BR_4.6.2.16: Photo Commit — Success Confirmation Toast

A profile-photo commit ends with the same success toast as every other Settings save: Ref: TS-DASH-01"Changes saved successfully" (BA instruction, 2026-08-28).

  • When it fires. On HTTP 200 from PATCH /profile-photo — on both commit triggers: Crop Image [Done] (Ref: §6 Alternative Flow A step 4.5) and [Confirm] in Ref: CF-03 (Ref: §6 Alternative Flow B step 4.4). The two triggers share one endpoint and one commit model, so they share one confirmation.
  • Sequence. The blocking overlay Ref: OV-09 closes first, then the toast is raised. The two are never on screen together: OV-09 reports work in progress, the toast reports work finished.
  • Copy. Identical to the phone save — the string is not specialised per field. TS-DASH-01 is the single Settings-wide success toast (Ref: BR_4.6.2.14).
  • On failure, no success toast is shown. The failure branches raise their own error toast and restore the previous avatar (Ref: §7 rows 2 and 4).
  • This does not change the commit model. The toast is confirmation only: the photo path still commits on its own trigger, still never marks the form dirty, and still never involves [Save Changes] (Ref: BR_4.6.2.14).

9. Screen Description

9.1 Account screen — Profile & Preferences tab

No.Field NameField TypeDisplaying rule / Behaviour rule
1Screen titleLabelDisplaying rule:- Static text: "Account". Constant across all Settings tabs.
2Settings tab listTabDisplaying rule:- Five tabs in fixed order: Connections & Credentials · Market Data Management · Profile & Preferences · Billing · Account Actions. Default active tab on entry = Connections & Credentials. Exactly one tab active at a time. Tab → owning UC mapping: Ref: BR_4.6.2.1.Behaviour rule:- On click: switches the right-hand content pane to that tab. If this form is dirty, the switch is intercepted by Ref: CF-01 first (Ref: BR_4.6.2.9).
3Controls panelToggle/SwitchDisplaying rule:- Persistent panel below the tab list, containing the Desk Manager and Reduce Motion toggles. Rendered on this tab but owned by UC_4.6.4 — Ref: BR_4.6.2.11.Behaviour rule:- Each toggle saves immediately on flip via its own endpoint. Does not participate in this screen's [Save Changes] / dirty-state logic.
4Section headerLabelDisplaying rule:- Static text: "General".
5Profile photoLabelDisplaying rule:- Read-only avatar display. Shows the image at profile_photo_url when set; otherwise the trader's initials Ref: BR_4.6.2.7.- There is no "staged preview" state — the value shown is always the persisted one, because every photo change commits on the spot Ref: BR_4.6.2.14.Behaviour rule:- Not clickable — the photo is changed only through the two buttons below it.
6[Change Photo]Button (Secondary)Displaying rule:- Always Enabled.Behaviour rule:- On click: opens the native file picker, then the Crop Image modal on a valid selection — Ref: §6 Alternative Flow A.Impact:- Does not mark the form dirty and does not enable [Save Changes] at any point in the flow — the commit happens inside the modal Ref: BR_4.6.2.14.
7[Remove Photo]Button (Danger)Displaying rule:- Rendered only when profile_photo_url IS NOT NULL; hidden entirely when the avatar is already showing initials — including immediately after a successful removal, without a page reload.Behaviour rule:- On click: opens the confirmation popup Ref: CF-03"Remove your photo?", buttons [Confirm] / [Cancel] stacked vertically. [Cancel] → nothing happens. [Confirm] → commits immediately via PATCH /profile-photo {user_id, profile_photo_url: null} behind overlay Ref: OV-09 — Ref: §6 Alternative Flow B.Impact:- Does not mark the form dirty and is not reversible via [Cancel] — Ref: BR_4.6.2.14. The stored S3 object is hard-deleted, so the previous image cannot be restored from the UI; the trader must upload it again. Ref: BR_4.6.2.6.- Every avatar surface reverts to the trader's initials, including the sidebar user chip and the My POD widget on the LIVE Community page. Ref: BR_4.6.2.13.
8Upload hintLabelDisplaying rule:- Static text: "Pick a photo up to 4MB." Does not enumerate the accepted extensions; those are enforced silently and surfaced only on error — Ref: BR_4.6.2.6.
9EmailTextboxDisplaying rule:- Read-only — Ref: BR_4.6.2.2. Rendered inside a disabled input container; not focusable, not editable, no copy action, no clear action.- Value from GET /profileemail. Max 100 characters per CR-09 §9.1; overflow: truncate with tooltip.- Never sent to either write endpoint — Ref: BR_4.6.2.15.
10Phone — Country CodeDropdown (Single-selection)Displaying rule:- Ref: CR-09 §9.2. Default on first load: +1. Scroll-only country list, no search input. Populated from the stored number's dial code when one exists.Behaviour rule:- On change: marks the form dirty and re-runs the phone validation chain (Ref: §5 step 6).
11Phone — NumberTextboxDisplaying rule:- Ref: CR-09 §9.2. Digits only, 6–15 digits excluding dial code, leading zeros auto-trimmed on out-click. Overflow: block input past the 15-digit limit.Validation: — all copy is the shared checkout / Common Rule copy; no Settings-specific string except IN-10. Ref: BR_4.6.2.3.- Empty → CR-09 §9.2: "Please enter phone number".- Invalid format or length → CR-09 §9.2: "Please enter a valid phone number".- Identical to the currently saved number → Ref: IN-10. Ref: BR_4.6.2.5.- Already registered to another account → Ref: IN-06 — the checkout string. Ref: BR_4.6.2.4.- Checks run on out-click and again on submit, in the order above; the first failure is the one displayed.- IN-08 / IN-09 are retired and must not be used.Behaviour rule:- On change: marks the form dirty. Any active inline error clears as soon as the value is edited.Impact:- Ref: BR_4.6.2.12 — no OTP verification, no notification email, no propagation to Auth0 / Rithmic / MT5 / TraderEvolution, and no change-frequency limit at launch.
12[Cancel]Button (Secondary)Displaying rule:- Hidden while the form is clean; visible only while the phone is dirty — Ref: BR_4.6.2.8. A photo change alone never reveals it.Behaviour rule:- On click: reverts the phone value and dial code to the last saved state and returns the form to clean. No confirmation popup — nothing about the phone has been persisted.Impact:- Has no effect on the profile photo, which was already committed at the moment it was changed — Ref: BR_4.6.2.14.
13[Save Changes]Button (Primary)Displaying rule:- Disabled while the form is clean or while any inline error is active; Enabled only when the phone is dirty and valid — Ref: BR_4.6.2.8.- Stays Disabled when the trader only changes or removes the photo — the photo is not part of this button's scope Ref: BR_4.6.2.14.Behaviour rule:- On click: calls PATCH /profile with {user_id, phone_number}. Never calls PATCH /profile-photo and never triggers an S3 upload — Ref: BR_4.6.2.15.- On HTTP 200: success toast Ref: TS-DASH-01; form returns to clean.- On failure: Ref: §7 Exceptional Flow.Impact:- Writes phone_number on the Users record. No other profile column is touched; profile_photo_url, email and ui_preferences are never sent by this button.- Ref: BR_4.6.2.12 — no OTP, no notification email, no propagation to Auth0 / Rithmic / MT5 / TraderEvolution.

9.2 Crop Image modal

No.Field NameField TypeDisplaying rule / Behaviour rule
1Modal titleLabelDisplaying rule:- Static text: "Crop Image".
2Crop viewportLabelDisplaying rule:- Shows the selected image inside a fixed 1:1 crop frame. Opens only after the file passes the format and size checks in Ref: BR_4.6.2.6.Behaviour rule:- The trader may pan and zoom the image within the frame. Rotate is not supported.
3[Done]Button (Primary)Behaviour rule:- On click: closes the modal and commits the photo immediately — overlay Ref: OV-09 → signed URL → PUT to S3 → PATCH /profile-photo {user_id, profile_photo_url} → backend hard-deletes the old S3 object. The three hops run as one uninterruptible sequence — there is no separate submission step. On HTTP 200 the overlay closes and the success toast Ref: TS-DASH-01"Changes saved successfully" — is shown. Ref: §6 Alternative Flow A, BR_4.6.2.6, BR_4.6.2.16.Impact:- Does not mark the form dirty and does not enable [Save Changes] — Ref: BR_4.6.2.14.- Not reversible: the previous image is deleted on success.
4[Cancel]Button (Secondary)Behaviour rule:- On click: closes the modal and discards the selection. Nothing is uploaded, the previously displayed avatar is retained, and the form's dirty state is unchanged.

9.3 Remove photo confirmation popup — Ref: CF-03

No.Field NameField TypeDisplaying rule / Behaviour rule
1Modal titleLabelDisplaying rule:- Static text: "Remove your photo?". There is no body text — the title is the entire message.
2[Confirm]Button (Primary)Displaying rule:- Top of the vertical button stack. Always Enabled while the popup is open.Behaviour rule:- On click: closes the popup and commits the removal — overlay Ref: OV-09PATCH /profile-photo {user_id, profile_photo_url: null} → backend hard-deletes the stored S3 object. On HTTP 200 the overlay closes and the success toast Ref: TS-DASH-01 is shown. Ref: §6 Alternative Flow B, BR_4.6.2.16.Impact:- Irreversible — no undo, and [Cancel] on the form behind cannot revert it. Ref: BR_4.6.2.6, BR_4.6.2.14.- Does not mark the form dirty and does not enable [Save Changes].
3[Cancel]Button (Secondary)Displaying rule:- Below [Confirm] in the stack.Behaviour rule:- On click: closes the popup. Nothing is called, the avatar is unchanged, and the form's dirty state is untouched.

Layout note. This popup stacks its two buttons vertically with the affirmative action on top, unlike CF-01 / CF-02, which place them side by side with Cancel first. The design set is the authority here (BA design frame, 2026-08-21).


Changelog

DateVersionUpdated itemBeforeAfterNotes
2026-09-03v14BR_4.6.2.15ui_preferences reclassified from "dormant, re-introducing it is a CR" to "dormant by design, pre-authorised for future FE-only keys" · BR_4.6.2.11 · Document References row 3d (new)The closing blockquote said the column "has neither a reader nor a writer anywhere in the Settings module" and that "if a future preference genuinely needs it, adding it to this payload is a change request". The document could not say why the field existed at all — it read as an unexplained leftover in the RFQ signature.The client's own rationale is recorded: the field is "intentionally included in the schema strictly as a future-proofing measure" with "no active parameters to parse… today", and BE must "maintain this column as an optional JSON object" so the FE can later save "arbitrary cross-device layout preferences… without requiring structural database schema changes". Three build consequences spelled out: keep the JSONB column as an optional pass-through with no inner validation; this screen's payload is unchanged ({user_id, phone_number}); and a future FE-only layout key needs no CR and no migration — only a key carrying backend logic or cost still does.No behavioural change to this UC — the payload, the endpoints and the boundary with UC_4.6.4 all stand exactly as in v13. This entry replaces a BA inference with the client's stated intent, and removes a false CR trigger that would otherwise have been raised during build. Source: client answer 2026-09-03 → [CTRL-EP-05] in QnA_STAGE2_SETTINGS_CAREER_VIRAL_EN.md §9.
2026-09-03v13BR_4.6.2.13 — Dashboard vs. Discord avatar boundary"StackTrading never pulls an avatar or a name from the Discord API, on any surface, as a primary value or as a fallback"; Discord surface = "neither reads it nor influences it"Amendment block added: the influences clause is superseded — profile_photo_url is now pushed into the Discord Webhook payload for every chat message sent from the LIVE Dashboard; the never pulls clause is superseded in principle by avatar priority 2 (Discord photo as fallback), flagged as not yet confirmed as a deliberate reversal. All Dashboard surfaces, the initials fallback and the My POD constraint are unchangedApplies CR-20260903-001. New constraint recorded on PATCH /profile-photo: the stored URL must be publicly fetchable by Discord's servers, otherwise every trader silently degrades to the webhook's default image. Two items pending: UC_4.14.1 §10 items 12 and 13
2026-08-28v12BR_4.6.2.16 (new) · §4 Post-conditions · §6 Alt Flow A step 4.5 · §6 Alt Flow B step 4.4 · BR_4.6.2.14 · §9.2 row 3 · §9.3 row 2 — photo commit now raises a success toastA photo commit showed no success confirmation at all. The only feedback was the blocking overlay OV-09 disappearing and the avatar changing. TS-DASH-01 was explicitly scoped away from this path — its context line read "a photo commit uses OV-09 and fires no toast on success" — so a trader who clicked [Done] was never told the write had persisted.On HTTP 200 from PATCH /profile-photo, the overlay closes and Ref: TS-DASH-01"Changes saved successfully" — is shown, exactly as on a phone save. Applies to both photo triggers: Crop Image [Done] and [Confirm] in CF-03. Overlay and toast are never on screen together. Failure branches are unchanged (previous avatar restored + error toast, no success toast).BA instruction 2026-08-28. Parity with the phone save — the two commit paths differ in trigger and endpoint (BR_4.6.2.14), not in how success is reported. No new toast IDTS-DASH-01 is reused, its context line widened in list-toast-popup.md v3.20. The commit model is untouched: the photo still never marks the form dirty.
2026-08-27v11BR_4.6.2.15[PP-EP-01] closed · ui_preferences discrepancy recordedThe BR ended with a ❓ blockquote: [PP-EP-01] PENDING, the client had never confirmed an endpoint name, and the WBS (PUT /profile), the RFQ (POST Update User Profile) and this document gave three different answers. The BR was explicitly said not to close it.Closed. The BE proposal is adopted in full — PATCH on both write endpoints, the two-endpoint split, and the merged crop → signed URL → S3 → PATCH /profile-photo chain. PUT /profile and POST Update User Profile are formally superseded and must not be cited as the contract; WBS row 174 flagged for correction. A second 🔴 blockquote records that the same answer described PATCH /profile as also writing ui_preferencesnot adopted, with the two reasons why (A-18's boundary, and the field having had no consumer at all since 2026-08-24). Payload stays {user_id, phone_number}.BA/client answers 2026-08-27, Phần II. Contract itself unchanged from v8 — this entry closes the open question and blocks a re-introduction during build.
2026-08-27v11BR_4.6.2.13 — Dashboard vs. Discord avatar precedence closedClosing blockquote deferred the question: the My POD data contract "including whether a Discord-sourced profile picture is ever used as a secondary fallback" was owned by UC_4.14.2, the client had not answered, and it "must be resolved when UC_4.14.2 is authored".Closed by removing the contest. StackTrading never reads an avatar or a name from the Discord API — not as a primary value, not as a fallback, on any surface. A table splits the two worlds: Dashboard renders the uploaded photo or initials; Discord renders whatever the trader set there. Client rationale recorded: broken images and expired CDN links. Stated consequence for UC_4.14.2 — the My POD contract is now constrained, not open; the Discord-fallback branch is excluded and needs no design there. Extended to names as well.BA/client answer 2026-08-27, Phần II — closes QnA A-20, and removes the corresponding item from UC_4.14.2's inbound question list.
2026-08-27v11BR_4.6.2.12 — phone change: launch scope confirmedStated no rate limit, no OTP, no notification email, no propagation — sourced to the client's "STILL OPEN" QnA row, i.e. written as an assumption.Confirmed for launch. No cool-down, and no security constraint of any kind: a phone change passes exactly two gates — the checkout regex and the system-wide uniqueness check — then writes straight to PostgreSQL. The rate-limit bullet now states plainly that this is a build decision taken in the absence of a client limit, not a limit the client ruled out, so a later client answer lands cleanly.BA/client answers 2026-08-27, Phần II — closes QnA A-16 and A-17 for launch. The 2FA / Drawdown-Defense caveat already in the BR is unaffected and still applies if either capability is activated.
2026-08-24v10Phone error copy. BR_4.6.2.3 rewritten. Also updated: §1 Description, §7 row 5, BR_4.6.2.4, BR_4.6.2.5, BR_4.6.2.8, §9.1 row 11. IN-08 and IN-09 retired and IN-06 widened in list-toast-popup.md v3.13.The phone field reused the checkout rules (CR-09 §9.2 structure, E.164 6–15 digits, uniqueness) but deliberately not the checkout copy: BR_4.6.2.3 declared "the one deliberate deviation from CR-09 §9.2 is the error string" and Settings rendered its own IN-08 "Invalid phone number format. Please check and try again" and IN-09 "This phone number is already registered with another account". There was no empty-field error at all.The deviation is removed — the field now conforms to CR-09 §9.2 and the checkout block completely, copy included. BR_4.6.2.3 now carries a 4-row error table: empty → "Please enter phone number" (CR-09 §9.2, new case — previously unspecified); invalid format/length → "Please enter a valid phone number" (CR-09 §9.2); duplicate → IN-06 "This phone number is already registered to another account." (the checkout string); same-as-current → IN-10, kept as the only Settings-specific string because neither CR-09 §9.2 nor the checkout field has a counterpart. IN-08 and IN-09 are retired system-wide.BA instruction 2026-08-24: "chỉnh lại validation rule cho phone number, refer đến 9.2 Phone Number Field của Common Rule và checkout block, không sử dụng validation rule text hiện tại của field phone number hiện tại nữa. Ví dụ như 'Invalid phone number format. Please check and try again' sẽ không sử dụng nữa." This reverses the 2026-08-11 decision in QnA_init_docs.md A-09/A-10/A-12, which had rejected reusing CR-09/IN-06 and mandated Settings-only strings. CR-09 §9.2 is not edited — Settings conforms to it, so UC_2.8.3 (checkout) is behaviourally unchanged; only IN-06's trigger text broadened to name both screens. The two-independent-commit-paths model (BR_4.6.2.14, v7) was re-confirmed by the BA in the same instruction and is unchanged — photo commits on Crop Image [Done], phone commits on [Save Changes].
2026-08-21v9CF-03 copy and buttons; new §9.3 Screen Description table. Also updated: §6 Alternative Flow B steps 2–4, BR_4.6.2.14, BR_4.6.2.15, §9.1 row 7. CF-03 rewritten in list-toast-popup.md v3.11.v7 introduced CF-03 as a BA-authored placeholder, flagged ❓ because no design frame existed: body copy "Remove your profile photo? Your avatar will revert to your initials across the Dashboard and the current image cannot be recovered." and buttons Cancel / Remove. The popup had no Screen Description table.Replaced with the actual design: Title "Remove your photo?", no body text, buttons [Confirm] / [Cancel] stacked vertically with Confirm on top (Primary/gold). Every reference to the affirmative button renamed [Remove][Confirm]. Added §9.3 with the three-row field table and a note that this popup's button layout deliberately differs from CF-01/CF-02. ❓ flag removed.BA supplied the design frame 2026-08-21, confirming the popup exists as derived in v7. Only the copy and the button treatment changed — the behaviour specified in v7 (immediate + irreversible commit, no participation in the dirty state, PATCH /profile-photo with profile_photo_url: null) is unchanged.
2026-08-21v8API contract. New BR_4.6.2.15. Also updated: §Document References (new row 3b), §1 Description, §5 step 8, §6 Alt Flow A step 4.3, §6 Alt Flow B step 4.2, §7 rows 2/4/5/6, BR_4.6.2.2, BR_4.6.2.4, BR_4.6.2.6, BR_4.6.2.11, BR_4.6.2.13, BR_4.6.2.14, §9.1 rows 7/9/13, §9.2 row 3. OV-09 and CF-03 re-pointed in list-toast-popup.md v3.10.One write endpoint, POST /profile, taking {user_id, profile_photo_url (Optional), phone_number (Optional)} — the shape given verbatim in RFQ_ Stack Trading Prop Tech V7.pdf §Page 36. Both commit paths introduced in v7 called that same endpoint with a different subset of fields.Three changes, all from the Sotatek BE team. (1) POSTPATCH on every write — the operation is a partial update, never a creation. (2) Upload and submission are one operation. The RFQ's "S3 signed URL execution before submission" described a preparatory step ahead of a separate save; that save no longer exists, so signed URL → PUT S3 → PATCH now run as one uninterruptible sequence behind OV-09. The signed-URL mechanism is retained — the binary still never transits the application backend. (3) The write endpoint is split in two: PATCH /profile-photo {user_id, profile_photo_url} for the photo path, PATCH /profile {user_id, phone_number} for the phone path. GET /profile is unchanged.Sotatek BE team API review, 2026-08-21, relayed by the BA: "POST /profiles → PATCH /profiles (chuẩn RESTful) · phải gộp bước image upload và bước submission làm 1 · suggest chuyển sang 2 api PATCH /profile-photo và PATCH /profiles riêng biệt". Two readings were clarified with the BA before writing: (a) Mechanism — "gộp làm 1" is about the trader-facing step count, not about collapsing the network hops; the signed-URL flow stays, so the RFQ's own upload mechanism is not contradicted, only its "before submission" framing. (b) Path naming — BE wrote /profiles (plural) but also /profile-photo (singular); the doc and RFQ use /profile. Normalised to singular across all three, since this is a singleton resource scoped to the authenticated trader. ❓ [PP-EP-01] (client confirmation of endpoint names) stays PENDING — this is a BE decision, not a client answer, and WBS/RFQ/this doc still disagree.
2026-08-21v7Commit model of the whole screen. New BR_4.6.2.14. Rewritten: §1 Description, §4 Post-conditions, §5 steps 5–9, §6 Alternative Flow A, §6 Alternative Flow B, §7 rows 2–7, BR_4.6.2.6, BR_4.6.2.8, §9.1 rows 5/6/7/12/13, §9.2 rows 3/4. Two new IDs in list-toast-popup.md v3.9: OV-09, CF-03.One form, one commit. Photo and phone were both staged locally: the Crop Image [Done] button only produced a preview and marked the form dirty, the S3 upload ran at §5 step 8 as part of [Save Changes], and a single POST /profile carried {profile_photo_url, phone_number} together. [Remove Photo] likewise only staged profile_photo_url = null, was reversible via [Cancel], and had no confirmation popup. The disposition of the old S3 object was declared out of scope.Two independent commit paths. (1) Photo commits immediately. [Done] closes the modal and runs signed URL → PUT S3 → POST /profile {user_id, profile_photo_url} behind the new blocking overlay OV-09 "Updating avatar..."; on HTTP 200 the backend hard-deletes the previous S3 object. (2) Phone still commits on [Save Changes], now with a payload of {user_id, phone_number} only. (3) The photo no longer participates in the dirty state — a photo-only change leaves [Save Changes] Disabled and [Cancel] hidden, and [Cancel] can no longer revert a photo. (4) [Remove Photo] is now gated by the new confirmation popup CF-03 and commits immediately + irreversibly. §7 gained separate failure rows for the photo path and the phone path, both stating that a failure on one path never rolls back the other.BA instruction 2026-08-21, with 3 reference frames (default screen with phone inline error · Crop Image modal · full-screen "Updating avatar..." overlay): "photo sẽ thay riêng, còn phone sẽ là thay riêng… khi trader click Done thì sẽ là upload và applied luôn ảnh mới, auto xoá ảnh cũ trong Database… photo không liên quan gì đến save changes cả. Nếu trader chỉ thay photo thôi không sửa phone number thì nút save changes không có sáng". Two points were derived rather than stated and confirmed with the BA in the same session: [Remove Photo] must also commit immediately (with [Save Changes] out of the picture there is no other path to persist a removal) and therefore needs a confirmation popup, since it is no longer undoable via [Cancel]; and the old S3 object is hard-deleted synchronously rather than via a cleanup job. ❓ Open with the client: the CF-03 copy and the "Updating avatar..." overlay copy are BA-authored — the design set has no frame for the removal confirmation.
2026-08-12v6BR_4.6.2.1 — tab list, Billing rowPlain text UC_4.6.3 (not yet documented in this repo)Real link to the now-published UC_4.6.3_v1.md, plus the two facts a reader of this tab list needs: the tab holds only the Invoice History table, and it is not LIVE-gated the way Market Data Management isUC_4.6.3 SRS created (Init Flow — Agent 3, vòng 4). Every tab in the Account screen now points at a published UC. Link-only change — no behavioural change in this document. Ref: QnA_init_docs.md D-18.
2026-08-12v5BR_4.6.2.11, §9.1 row 3, and the UC_4.6.x range note — Controls panel owner and labelUC_4.6.4 referenced as plain text and marked "not yet documented in this repo"; panel labelled "AI Chat and Reduce Motion"Real link to the now-published UC_4.6.4_v1.md; panel labelled "Desk Manager and Reduce Motion"Two changes, neither behavioural for this UC. (1) The Controls SRS now exists, so the placeholder note is replaced by a link. (2) Client-directed label change AI ChatDesk Manager (Source: QnA from clients — Settings, Controls & Concierge UI/UX row, Adrian Stack 2026-08-10); the data contract (ai_enabled) is unchanged. Ref: QnA_init_docs.md C-01.
2026-08-11v4BR_4.6.2.1 — tab list, Market Data Management and Account Actions rows, plus a new note on the UC_4.6.x range; BR_4.6.2.11 and §9.1 row 3 — Controls panel ownerMarket Data Management = "No UC_ID assigned in the UC_4.6.x range; owning UC not yet identified in the WBS." · Account Actions = UC_4.6.4 — Resign Account (not yet documented in this repo) · Controls panel = UC_4.6.5Market Data Management = UC_4.17.1 (LIVE-only, not yet documented). Account Actions = not a single UC — Reset Associate TrackUC_4.11.1 (SIM, no LIVE counterpart), Resign & Close AccountUC_4.11.2 (SIM) / UC_4.17.2 (LIVE). Controls panel = UC_4.6.4. Added a note that the WBS common Settings range is only UC_4.6.1UC_4.6.4, that no UC_4.6.5 exists, and that the folder name is not an index.ID-only remap against the current WBS (References/WBS/[BA Internal] Stacktrading.csv lines 81–84, 99–100, 117–118), approved by BA 2026-08-11. The WBS moved Account Actions out of the common range and re-used UC_4.6.4 for Controls, so the two stale mappings contradicted each other and each other's neighbours. No screen behaviour, field, or business rule changed in this document. Ref: UC_4.11.1 QnA_init_docs.md A-19.
2026-08-11v3New BR_4.6.2.13 — Avatar Propagation. Also updated: §1 Overview, §4 Post-conditions, §5 step 10, BR_4.6.2.1, BR_4.6.2.7, §9.1 rows 7 and 13.The avatar was described only as a field on this screen. The only stated consumer was "the sidebar user chip", with no detail on its collapsed vs. expanded states and no mention of the POD system at all. BR_4.6.2.7's closing line scoped the initials rule to this screen plus the chip.Added BR_4.6.2.13 with the full consumer table (sidebar chip collapsed + expanded, My POD Pod Leader card, My POD members list on the LIVE Community page) plus the propagation mechanics: read-through rather than push, per-displayed-user fallback, refresh timing, and the fact that level/track/name labels are owned by each consumer surface rather than by this UC. BR_4.6.2.7 now scopes the initials rule to every avatar surface. The [Remove Photo] and [Save Changes] rows gained a matching Impact line.BA instruction 2026-08-11 (Ref: QnA_init_docs.md A-20), with 3 reference frames: sidebar chip collapsed (SD / Lvl 5), sidebar chip expanded (photo · Sam Smith · Level 15), My POD widget (leader Marcus Chen photo + member rows showing MC initials). This closes the client question "Which screens will this avatar be used on?" left unanswered in QnA from clients — Settings, Profile & Preferences Definition row. The Dashboard-avatar vs. Discord-avatar precedence half of that question stays open and is deferred to UC_4.14.2.
2026-08-11v2BR_4.6.2.1 — tab list, Connections & Credentials rowPlain text UC_4.6.1 (not yet documented in this repo)Real link to the now-published UC_4.6.1_v1.md, plus the note that it is the default active tab on entryUC_4.6.1 SRS created (Init Flow — Agent 3, vòng 2). ID/link-only change — no behavioural change in this document.
2026-08-11v1Initial versionUC_4.6.2 created: §1–§9, 12 Business Rules (BR_4.6.2.1 → BR_4.6.2.12), 2 Screen Description tables (main tab + Crop Image modal).Init Flow (Agent 1 Auditor → Agent 2 Challenger → Agent 3 Architect). BA answers recorded in QnA_init_docs.md A-01 → A-19. Four new IDs added to list-toast-popup.md v3.4 for this UC: TS-DASH-01, IN-08, IN-09, IN-10CR-09 §9.2 and IN-06 were deliberately left unchanged so UC_2.8.3 (checkout) is unaffected.

On this page