SRS: UC_4.14.1 — Discord Chat Stream (TWO-WAY)
UC_4.14.1: Discord Chat Stream (TWO-WAY)
| Field | Value |
|---|---|
| BA in Charge | Anh Hoang |
| Date Created | 2026-08-26 |
| Version | v1.3 |
| Document References | RFQ_ Website and Dashboard Implementation V7.pdf (§Community Page Implementation — Live Chat Feed) · RFQ_ Stack Trading Prop Tech V7.pdf (§2.3 Discord Bot & Message Queueing, §Community, Trustpilot and Discord Bot Endpoints) · Discord Server Details.md (§#live-broadcast-chat, §#verify, §Compiled Discord Roles and Badges) · CR-20260818-002 (Flow 38 monitoring scope) · CR-20260903-001 (Webhook primary route / Bot batching fallback, outbound identity) |
Document References
| # | Original Document | Key Sections Used |
|---|---|---|
| 1 | RFQ_ Website and Dashboard Implementation V7.pdf | §Community Page Implementation — Live Chat Feed: "Messages submitted here must POST to the backend Discord bridge. Read-only messages from other users must be streamed to the UI via the WebSocket connection to maintain synchronization with the live-broadcast Discord channel." |
| 2 | RFQ_ Stack Trading Prop Tech V7.pdf | §2.3 Discord Bot & Message Queueing (dedicated Node.js Discord Bot, Redis memory queue, Batch Worker concatenation above 1 msg/s, Discord rate limit of 5 messages / 5 seconds / channel, Automated Support Interception & Routing) · §Community, Trustpilot and Discord Bot Endpoints (Send Broadcast Chat → HTTP 202 Accepted; Revoke All Roles). ⚠️ The delivery half of §2.3 is superseded by CR-20260903-001 — batching is now the fallback route, not the only one (Ref: BR_4.14.1.3). The 202 / Redis / rate-limit half stands |
| 3 | Discord Server Details.md | §Channel Specifications — #live-broadcast-chat (User Scopes: Public = Read, Associate Trader and above = Read/Write; "Subject to Discord API rate limits") · #verify (link-token landing channel) · §Compiled Discord Roles and Badges (Associate Trader = Levels 1–5; everyone = read-only public) |
| 4 | Zapier Integration V7.pdf | Flow 1 Step 6 (link token generation) · Flow 7 §Digital Eviction (POST /discord/revoke-all-roles) · Flow 38 (Support Interception — trigger words → Gemini sentiment gate → Freshdesk ticket) |
| 5 | QnA from clients — STAGE 2: Community & Live Broadcast | Client-confirmed answers [DCS-DEF-01] → [DCS-BIZ-21], [DCS-UI-01] → [DCS-UI-03], [DCS-EP-01] / [DCS-EP-02] (Stage 2 - Dashboard & Fomula (Community).csv, rows C-9 → C-16; Adrian Stack / Adam, 2026-08-04 → 2026-08-18) — official endpoint path, rate limit, 202 lifecycle, composition constraints |
| 6 | CR-20260818-002 | Flow 38 — Discord Bot must also monitor #live-broadcast-chat (2026-08-18, Adrian Stack) — monitoring scope 2 → 3 channels |
| 8 | CR-20260903-001 | Discord Live Chat — Webhook thành route chính, Bot Batching hạ xuống fallback (2026-09-03, Adrian Stack + Backend team) — outbound delivery becomes dual-route; webhook username / avatar_url override; per-message cap 1,916; avatar priority order for the Discord app |
| 7 | Client design pack — Community page (LIVE) | Six finalized states supplied to BA on 2026-08-26: Live + linked · Offline · Live + Discord not linked · New Messages badge · long message expanded (Read less) · Reconnecting…. ⚠️ Not yet checked into References/Wireframe/ — see §10 |
1. Overview
| Field | Content |
|---|---|
| ID | UC_4.14.1 |
| Use Case | Discord Chat Stream (TWO-WAY) |
| Description | On the Community page, next to the livestream player, a custom chat panel bridges the Dashboard and the Discord #live-broadcast-chat channel in both directions. Inbound (Discord → Dashboard) streams every channel message to the panel in real time over the DISCORD_CHAT_STREAM WebSocket — identical to the SIM feed and delegated to UC_4.9.1 (Ref: BR_4.14.1.7). Outbound (Dashboard → Discord) is what this UC adds: a LIVE trader at Level 1+ whose Discord account is linked composes a message, the frontend posts it to the backend bridge, the backend queues it in Redis, and a dispatcher relays it to Discord through a Discord Webhook that posts under the trader's own name and photo — falling back to bot batching only while the queue is backed up (Ref: BR_4.14.1.3, BR_4.14.1.12). The panel never optimistically renders the sender's own message — it appears only when it round-trips back over the WebSocket (Ref: BR_4.14.1.4). Posting is gated by two independent conditions, and the input row carries a distinct locked state for each (Ref: BR_4.14.1.1, BR_4.14.1.2). |
| Zapier Flow | Not triggered by this panel. Context only — Flow 38 (Support Interception) monitors #live-broadcast-chat and can delete a message before it ever reaches the Dashboard (Ref: BR_4.14.1.10); Flow 7 §Digital Eviction revokes the Discord roles that grant posting (Ref: BR_4.14.1.11); Flow 1 Step 6 generates the link token consumed by the unlinked state (Ref: BR_4.14.1.1) |
| Zapier Table | — |
| 3rd Party | Discord — #live-broadcast-chat channel (message source and relay target, server nickname and role metadata, native AutoMod moderation, rate limit of 5 requests / 5 seconds / channel) · Discord Webhook on that channel (primary outbound route; username / avatar_url override per payload; rate limit of 30 requests / 60 seconds) · Discord custom Node.js bot (fallback batching route, Flow 38 monitor, /link slash command) · Gemini (Flow 38 sentiment gate) · Freshdesk (Flow 38 ticket target) |
Design states covered by this UC (client design pack, 2026-08-26):
| # | State | What the panel shows | Ref |
|---|---|---|---|
| 1 | Live · Discord linked | Message feed + input Type a message + [Send] active | §5 |
| 2 | Live · Discord not linked | Message feed unchanged + input locked with a 🔒 icon and the placeholder Link your Discord account first to unlock chat; [Send] disabled | §6.1 |
| 3 | Broadcast Offline | No live chat / Chat will open when the session goes live; input locked with the placeholder Waiting for session... | §6.2 |
| 4 | Scrolled up, new traffic arriving | New Messages badge floating at the bottom of the list | BR_4.14.1.7 |
| 5 | Long message expanded | Full body rendered inside the panel's fixed height, with Read less | BR_4.14.1.7, §10 |
| 6 | WebSocket dropped | Reconnecting… chip beside the Live chat header; existing bubbles retained, not cleared | BR_4.14.1.9 |
2. Trigger
- The trader navigates to the Community page via the Community tab in the persistent sidebar (Ref: UC_4.1.2 §2 Screen Description) → the chat component mounts and issues its one-time hydration request.
- A new message is posted in the Discord
#live-broadcast-chatchannel, by any source, → theDISCORD_CHAT_STREAMWebSocket pushes it to the Dashboard (Ref: BR_4.14.1.7). - The trader submits a message from the panel — by pressing
Enteror clicking [Send] (Ref: BR_4.14.1.6).
3. Pre-conditions
- The user is authenticated and is on the Community page.
GET /community/live-broadcasthas returned the currentis_livevalue. This endpoint and its contract are owned by UC_4.5.1 and are not redefined here (Ref: BR_4.14.1.8).- To read the feed: nothing further. The feed is visible to every authenticated user, SIM and LIVE alike (Ref: BR_4.9.1.1).
- To post: the account is LIVE at Level 1 or above and
discord_user_idis populated on the user record. Both conditions must hold — see the matrix in BR_4.14.1.1.
4. Post-conditions
- The panel renders in exactly one of the six states listed in §1.
- A message accepted by the backend has been enqueued in Redis. It is not guaranteed to appear on any Dashboard — it appears only if the dispatcher posts it successfully, over the webhook route or over the bot fallback route, and it survives both moderation layers (Ref: BR_4.14.1.4, BR_4.14.1.10).
- No message is ever transmitted to Discord from an account that fails either gate in BR_4.14.1.1.
5. Basic Flow — Broadcast Live, Discord linked (LIVE Level 1+)
- The Community page renders.
is_live == trueis returned byGET /community/live-broadcast(Ref: UC_4.5.1 §5), so the chat panel opens together with the video player (Ref: BR_4.14.1.8). - On component mount, the frontend executes a single hydration request and renders the 50 most recent messages held in the backend Redis cache, then opens the
DISCORD_CHAT_STREAMWebSocket subscription. Feed rendering, ordering, the rolling window, author naming, the Host highlight, auto-scroll andRead morebehave exactly as on the SIM panel (Ref: BR_4.14.1.7). - The account passes both gates in BR_4.14.1.1, so the input row renders enabled, with the placeholder
Type a messageand an active [Send] control (Ref: BR_4.14.1.2). - The trader types a message. Composition is constrained to plain text and Unicode emoji, hard-capped at 1,916 characters by the input's
maxlength, with no character counter and no emoji picker, GIF picker, attachment control or@mentionauto-complete (Ref: BR_4.14.1.6). - The trader presses
Enter, or clicks [Send].Shift+Enterinserts a line break instead of submitting (Ref: BR_4.14.1.6). - The frontend calls
POST /community/chat/sendwith{ user_id, message_text }. The backend validates the session, checks the per-user rate limit, pushes the message onto the Redis broadcast queue, and returns HTTP 202 Accepted immediately (Ref: BR_4.14.1.3, BR_4.14.1.5). - On the 202 the frontend clears the input and re-enables it for the next message. It does not render the message in the feed, does not show a pending bubble, and does not show a delivery spinner (Ref: BR_4.14.1.4).
- The dispatcher drains the queue. While the queue holds 5 or fewer pending messages and the webhook budget is available, it sends one webhook request per message, overriding
usernameandavatar_urlfrom the sender's Dashboard profile — so the message lands in#live-broadcast-chatunder the trader's own name and photo, not as a bot transcript line (Ref: BR_4.14.1.3, BR_4.14.1.12). - The message passes both server-side moderation layers — Discord native AutoMod, then the Flow 38 monitor (Ref: BR_4.14.1.10).
- The posted message is pushed over
DISCORD_CHAT_STREAM. It now appears in the panel of every connected trader, including the sender's own, as an ordinary bubble (Ref: BR_4.14.1.4, BR_4.14.1.7). Which name and Host styling that bubble carries for a webhook-posted message is not yet settled — see §10 item 15.
6. Alternative Flows
6.1 — Live, Discord account not linked
Applies when is_live == true and discord_user_id is null. This is the most common state for a trader who has just been provisioned onto a live account and has not yet opened their onboarding email.
- The message feed renders in full and behaves exactly as in §5 steps 1–2 — reading is never gated (Ref: BR_4.9.1.1).
- The input row renders locked: a 🔒 icon inside the field, the placeholder
Link your Discord account first to unlock chat, and a disabled [Send] control. The field cannot receive focus (Ref: BR_4.14.1.2). - In the same page view, The PIT card replaces its
Community Chat/Discord Gatewayblock with the three-step link-token instruction block headedLink your Discord account first. That block is owned by BR_4.5.2.7 and is not duplicated by this UC — the chat panel renders no instructions, no CTA and no token of its own. - The trader completes
/link [token]in#verifyinside Discord. The bot writesdiscord_user_idto the user record and assigns the trader's Discord role (Ref: BR_4.5.2.6). - The input row unlocks on the next page load or Community-page mount. The panel does not subscribe to a linking event and does not poll
discord_user_id, so the unlock is not live within the current view (Ref: BR_4.14.1.1).
6.2 — Broadcast Offline
Applies whenever is_live == false, for every account regardless of level or linking state.
- The chat panel clears the active messages and transitions to the empty state.
- The panel body displays
No live chatwith the supporting lineChat will open when the session goes live. - The input row stays present but locked, and its placeholder changes to
Waiting for session.... [Send] is disabled. This placeholder wins over the unlinked placeholder of §6.1 (Ref: BR_4.14.1.2). - When the broadcast returns to
is_live == true, the panel re-opens and the flow restarts at §5 step 2 — the feed is re-hydrated from Redis; cleared messages are not restored from the client (Ref: BR_4.14.1.8).
7. Exceptional Flow
- [If the trader submits faster than the per-user rate limit] — the backend rejects the request and the panel displays the inline notice
Message rate limit reached. Please wait a moment.The composed text is retained in the input so the trader can resubmit without retyping (Ref: BR_4.14.1.5). - [If the queue backs up past 5 pending messages, or the webhook's 30-requests-per-60-seconds budget is exhausted] — delivery switches to the bot batching route for as long as the condition holds. Messages sent in that window arrive in Discord as a single concatenated post under the bot's identity, not under the trader's name and photo. Nothing on the Dashboard signals the switch — no notice, no badge, no change to the input row — and the feed will therefore show both bubble styles inside one session (Ref: BR_4.14.1.3, BR_4.14.1.12). See §10 item 16.
- [If the trader has no Dashboard photo] — the webhook payload falls back to the linked Discord account's avatar, and if that does not exist either, to the webhook's own configured avatar. The Dashboard panel itself is unaffected: it renders no avatar in any case (Ref: BR_4.14.1.12).
- [If Discord cannot fetch the
avatar_urlsupplied in the payload] — for example a private or expired S3 URL — Discord renders the webhook's own avatar and the message is still delivered. The trader is not told, and no retry is performed. See §10 item 13. - [If the message is blocked by Discord AutoMod] — it never reaches the channel, so
DISCORD_CHAT_STREAMnever fires for it. The message simply never appears, on the sender's Dashboard or anyone else's. No error, no toast, no flagged bubble and no removal animation is built. This silence is the confirmed design, not a defect (Ref: BR_4.14.1.10) — see §10 item 4 for the open question on sender feedback. - [If the message is intercepted by Flow 38 as an operational complaint] — the Discord bot deletes it inside Discord and opens a Freshdesk ticket at Priority = High. The Dashboard renders nothing for it (Ref: BR_4.14.1.10). ⚠️ For a message that arrived over the webhook route, the ticket step has no
discord_user_idto resolve the trader's email from — see §10 item 14. - [If the WebSocket connection drops mid-session] — the panel shows the
Reconnecting…chip beside theLive chatheader, retains the bubbles already rendered, and reconnects automatically with exponential backoff. On reconnect it re-hydrates the 50 most recent messages from Redis. The input row's own state is unaffected — a linked LIVE trader can still compose and submit, because the outbound path is HTTP and does not depend on the WebSocket (Ref: BR_4.14.1.9). - [If
POST /community/chat/sendfails with a network or 5xx error] — the composed text is retained and the input stays enabled. No message was queued, so nothing will arrive over the WebSocket. - [If the broadcast goes offline while the trader's message is still in the Redis queue] — the message may still be posted to Discord by the worker, but the panel has already cleared and locked, so the sender never sees it land. No cancellation of queued messages is performed (Ref: BR_4.14.1.8).
- [If the trader's Discord roles are revoked by Flow 7 Digital Eviction] — posting stops working from the Discord side. The Dashboard reflects this only after the account's level/status is re-read on the next page load (Ref: BR_4.14.1.11).
- [If a message contains a custom Discord server emoji, an
@mention, or a link] — it renders as raw text. Standard Unicode emojis render natively (Ref: BR_4.14.1.6, BR_4.9.1.7).
8. Business Rules
BR_4.14.1.1: Posting Requires Two Independent Gates — Level and Discord Link
Reading the feed is universal (Ref: BR_4.9.1.1). Posting requires both of the following, and neither substitutes for the other:
| Gate | Condition | Why |
|---|---|---|
| G1 — Channel scope | The account is LIVE at Level 1 or above, which carries the Associate Trader Discord role or higher | #live-broadcast-chat grants Read/Write to Associate Trader and above, and Read only to Public (Source: Discord Server Details.md §#live-broadcast-chat; Associate Trader = Levels 1–5 per §Compiled Discord Roles and Badges). A SIM trader is Level 0 and can never pass this gate — that is what makes UC_4.9.1 one-way |
| G2 — Identity | discord_user_id is populated on the user record | "Only after the user has linked their Discord account." The relay has no identity to post under otherwise, and an unlinked user holds only the everyone role in the server (Source: Stage 2 - Dashboard & Fomula (Community).csv — Discord Chat Stream / Business logic A18, Adam) |
Resulting input-row state:
| G1 (Level 1+) | G2 (linked) | Input row |
|---|---|---|
| ✅ | ✅ | Enabled — Type a message, [Send] active |
| ✅ | ❌ | Locked — Link your Discord account first to unlock chat |
| ❌ | ✅ | Locked — Pass the Associate Track to unlock chat. Owned by BR_4.9.1.2, not by this UC |
| ❌ | ❌ | Locked — the Level copy wins over the linking copy, because linking a Discord account would not unlock posting for a Level 0 account. Owned by BR_4.9.1.2 |
- The gates are evaluated on page load / component mount only. The panel does not poll
discord_user_idand does not subscribe to a linking event, so a trader who links mid-session must reload to unlock the input. - The link-token instruction block that tells the trader how to link lives on The PIT card and is owned by BR_4.5.2.7. The chat panel states the blocker; it does not restate the remedy.
BR_4.14.1.2: Input Row State Machine — Precedence Order
The input row is never removed from the layout. Locking it keeps the panel height and the page grid stable, which is the constraint the client stated twice for this page ("so the UI grid does not break", "Hiding the component will break your frontend CSS grid layout").
Several lock conditions can hold at once. They are resolved in this fixed order — the first match wins, and lower rows are not evaluated:
| Priority | Condition | Field state | Placeholder | [Send] |
|---|---|---|---|---|
| 1 | is_live == false | Disabled, no focus | Waiting for session... | Disabled |
| 2 | Level 0 (SIM) | Disabled, no focus, 🔒 | Pass the Associate Track to unlock chat | Disabled |
| 3 | discord_user_id == null | Disabled, no focus, 🔒 | Link your Discord account first to unlock chat | Disabled |
| 4 | Both gates passed | Enabled | Type a message | Enabled |
Why session state outranks eligibility. When the broadcast is offline nobody can post, so the session reason is both the nearer cause and the one that will clear first. Showing an eligibility message while the chat is closed for everyone would tell the trader to fix something that is not currently blocking them.
Why the Level copy outranks the linking copy. Linking Discord does not raise a Level 0 account, so Link your Discord account first to unlock chat would be actively misleading advice for a SIM trader.
✅ Locked, not hidden — confirmed by BA on 2026-08-26, following the finalized design. Client answer A18 had said that for an unlinked user "the ability to send messages should be hidden" (hidden outright, not merely disabled). The design pack of 2026-08-26 instead draws the row present and locked, with a 🔒 icon and an explanatory placeholder, and that is the behaviour this UC specifies. Two reasons carried the decision: a disappearing input explains nothing to a trader who does not know why they cannot type, and removing the row contradicts the client's own grid-stability constraint ("so the UI grid does not break", "Hiding the component will break your frontend CSS grid layout"). A18 is superseded on this point and must not be re-opened from the QnA file — see the closed-item table in §10.
BR_4.14.1.3: Outbound Delivery — Two Routes, Webhook First, Bot Batching as the Pressure Valve
The Dashboard never talks to Discord directly. Every outbound message is queued in Redis and answered with HTTP 202 before any Discord call is attempted (Source: RFQ_ Stack Trading Prop Tech V7.pdf §2.3). What CR-20260903-001 changes is how the queue is drained: a Discord Webhook is now the default delivery route, and the Batch Worker concatenation of §2.3 survives only as a fallback for burst traffic.
The client's reason for the change, verbatim: "That UI is pretty ugly... I realize this batching method was originally outlined in the V7 spec to manage rate limits, but the visual result is.. yikes."
Pipeline
| Stage | Component | Behaviour |
|---|---|---|
| 1 | Frontend | POST /community/chat/send with { user_id, message_text } |
| 2 | Backend middleware | Validates session auth → checks the per-user rate limit (Ref: BR_4.14.1.5) → pushes the message onto the Redis broadcast queue → returns HTTP 202 Accepted immediately |
| 3 | Dispatcher | Reads the queue depth and the two token buckets, then picks route A or route B per the selection table below |
| 4a | Route A — Discord Webhook (default) | One webhook request per message. username and avatar_url are overridden from the sender's Dashboard profile, so the message renders natively as the trader rather than as a bot transcript line (Ref: BR_4.14.1.12) |
| 4b | Route B — Bot batching (fallback) | The bot measures the pending messages, concatenates as many as fit under Discord's 2,000-character post ceiling into a single post, newline-separated in the format [User A]: Message \n [User B]: Message, and leaves the remainder queued for the next tick roughly one second later. The post carries the bot's identity |
| 5 | Discord → back | The posted message is pushed over DISCORD_CHAT_STREAM (Ref: BR_4.14.1.7) |
Route selection
| Condition | Route |
|---|---|
| Queue depth ≤ 5 pending and the webhook budget is not exhausted | A — Webhook |
| Queue depth > 5 pending or the webhook budget is exhausted | B — Bot batching |
| Queue drained | Reverts to A automatically — no operator action, no manual switch |
Limits the dispatcher must respect. Both routes are paced by two separate token buckets, one per route, so that Discord never rejects a send:
| Limit | Value | Applies to |
|---|---|---|
| Channel limit | 5 requests / 5 seconds | Both routes, per channel |
| Webhook limit | 30 requests / 60 seconds | Route A |
| Single Discord post | 2,000 characters | The concatenated post of route B |
| Single trader message | 1,916 characters | Both routes (Ref: BR_4.14.1.6) |
- Why 1,916 and not 2,000. A single message is capped below the Discord ceiling so that it can never overflow the post it is wrapped in when route B concatenates it. The 84-character headroom absorbs the
[Name]:prefix and the newline separator of the batched format. The cap therefore exists for route B but is enforced on every message, whichever route ends up carrying it. - Endpoint path.
POST /community/chat/sendis the official path, confirmed by the client to supersede thePOST /chat/sendwritten in the WBS, so that it sits in the same group asGET /community/live-broadcast(Source:Stage 2 - Dashboard & Fomula (Community).csv— Discord Chat Stream / Endpoints A1, Adrian Stack). - 202, not 200 — unchanged by this CR. The response carries no message ID, no delivery status, no echo of the payload and no indication of which route will carry the message. It confirms acceptance into the queue and nothing more (Ref: BR_4.14.1.4).
- The route is invisible to the frontend. No field, no header and no UI element tells the trader whether their message went out over the webhook or the bot. The frontend behaves identically in both cases.
- Batching is outbound-only. The inbound stream is never batched or delayed (Ref: BR_4.14.1.7).
- Route B is no longer the normal case. Under v1 aggregation began as soon as two traders posted inside the same second; it now begins only once the queue has actually backed up past five pending messages. At ordinary broadcast traffic the feed should show one webhook message per trader.
⚠️ Route B keeps the v1 problems, at reduced frequency. Whenever the fallback is active, several traders' text is still merged into one bot-authored post — so the attribution question of §10 item 2 and the collateral-moderation risk of item 6 remain live for exactly the burst window the client cares most about. The webhook route removes them; the fallback does not.
BR_4.14.1.4: No Optimistic Rendering — the Feed Is Read-Only in Both Directions
The Live Chat feed is a fully read-only component driven exclusively by DISCORD_CHAT_STREAM. The act of sending writes nothing to it (Source: Stage 2 - Dashboard & Fomula (Community).csv — Discord Chat Stream / Business logic A20, Adrian Stack, 2026-08-18).
| Moment | What the sender sees |
|---|---|
Click [Send] / press Enter | Nothing appears in the feed |
| HTTP 202 returned | Input clears and re-enables. Still nothing in the feed. No pending bubble, no greyed-out placeholder message, no spinner, no "sending…" state |
| Message posted to Discord and streamed back | The message appears at the bottom of the feed like any other message, attributed to the sender's Discord nickname |
| Message blocked by moderation | The message never appears, and no failure is surfaced (Ref: BR_4.14.1.10) |
- The sender's own message is not styled differently from anyone else's. There is no "me" bubble alignment, no sent/delivered tick and no local echo.
- Consequence for QA: a short delay between pressing
Enterand the message appearing is expected behaviour, not a bug. The round trip covers a Redis hop, a worker cycle, a Discord API call and a WebSocket push.
BR_4.14.1.5: Per-User Rate Limit and Its Inline Notice
The endpoint enforces a standard per-user rate limit of 1 request per second per user, to stop a single trader flooding the Redis memory queue ahead of the batch worker (Source: Stage 2 - Dashboard & Fomula (Community).csv — Discord Chat Stream / Endpoints A2, Adrian Stack).
- This is the only custom gate at the endpoint. There is no custom content-moderation layer in the backend — content filtering is entirely Discord's (Ref: BR_4.14.1.10).
- On rejection the panel renders the inline notice
Message rate limit reached. Please wait a moment.inside the chat panel, adjacent to the input row. It is an inline notice, not a modal and not a global toast. - The composed text is retained in the input on rejection — the trader must not have to retype. The notice clears on the next successful submission or when the trader edits the input.
- The limit is enforced server-side. The frontend must not implement a competing client-side timer as the source of truth, though it may disable [Send] briefly to reduce obvious double-clicks.
BR_4.14.1.6: Composition Constraints — a Light Text Relay, Not a Discord Client
The client's framing for the whole panel: "we are building a lightweight text relay, not re-creating Discord."
| Constraint | Rule |
|---|---|
| Maximum length | 1,916 characters. Enforced by the HTML maxlength attribute on the input — character 1,917 cannot be typed. Changed from 2,000 by CR-20260903-001: the cap sits below Discord's own 2,000-character message limit so that one message can never overflow the concatenated post that the fallback route wraps it in (Ref: BR_4.14.1.3). ⚠️ Whether the cap is enforced as an FE maxlength or only server-side is §10 item 17 |
| Character counter | Not built. "Do not build a custom character counter UI component" |
| Submit key | Enter submits. Shift+Enter inserts a line break, as in Discord |
| Supported content | Plain text and standard Unicode emoji only (e.g. Let's go! 🚀). The payload is a plain string, so Unicode passes through and the browser renders it natively |
| Emoji picker | Not built. The trader uses their operating system's native emoji keyboard (Windows + ., or the native iOS/Android keyboard) |
| Custom Discord emoji | Not supported — sent or received, it renders as the raw :emoji_name: string |
| Image / video / file / GIF / sticker | Not supported. No upload control and no GIF picker outbound; inbound media is stripped by the backend before it reaches the panel (Ref: BR_4.9.1.7) |
| URL | Sent and rendered as plain, non-clickable text — no anchor tag, no auto-linkify, no preview, in either direction. This is a security control and is owned by BR_4.9.1.15 |
@mention | No auto-complete and no tag resolution. @ is an ordinary character; it raises no Discord notification and no Dashboard notification |
| Emoji reaction | Not supported. Reactions exist only on the native Discord side and are not synchronised to the Dashboard |
| Message edit / delete / reply | Not supported. A sent message cannot be edited, deleted or replied to from the Dashboard, and an edit or deletion performed later in the Discord app is not synchronised back to the panel (Ref: BR_4.9.1.14) |
- CR-02 does not apply to this field. CR-02 §2.2 sets a 255/500-character default with an out-click error, and CR-02 §2.3 prescribes an
Enter [field's label]placeholder. Both are overridden here by the client's explicit answers: the cap is 1,916 with block-on-limit behaviour (no error message), and the placeholders are the four strings fixed in BR_4.14.1.2. - Whitespace-only input is treated as empty and is not submitted; [Send] performs no request for it.
BR_4.14.1.7: Inbound Render Contract — Shared with UC_4.9.1, Not Restated Here
The inbound direction is identical for SIM and LIVE. It is owned by UC_4.9.1 and referenced here rather than duplicated, so that a change lands in one place.
| Concern | Rule (owned by UC_4.9.1) | Summary |
|---|---|---|
| WebSocket transport and payload | BR_4.9.1.5 | DISCORD_CHAT_STREAM, one payload per message, real-time 1:1, never batched. Payload { author, message_text, timestamp, is_host }; FIFO on UTC receipt timestamp |
| Hydration and rolling window | BR_4.9.1.4 | One hydration request on mount, 50 most recent messages from Redis; fixed 50-message DOM window; no pagination, no infinite scroll |
| Display name | BR_4.9.1.6 | Exact Discord server nickname plus role suffix; not settable from the Dashboard |
| Host highlight | BR_4.9.1.13 | Gold author name when is_host == true, standard white otherwise; binary, name only |
| Content rendering | BR_4.9.1.7 | Plain text and Unicode emoji only; custom emoji and mentions render raw. Image, video, file, GIF and sticker payloads are stripped by the backend before broadcast |
| Append-only feed | BR_4.9.1.14 | The stream only appends. An edit or deletion made in Discord is not synchronised; the message stays as first rendered until the 50-message window purges it |
| URLs as plain text | BR_4.9.1.15 | Non-clickable in both directions — a security control against phishing, not a styling choice |
Auto-scroll and New Messages | BR_4.9.1.8 | Auto-scroll while anchored at the bottom; hold position and show a clickable badge while scrolled up |
| Long messages | BR_4.9.1.9 | 4-line clamp, Read more / Read less, panel keeps its fixed height and scrolls internally, one message expanded at a time |
| No online count | BR_4.9.1.11 | No per-channel online pill in any state. Unrelated to The PIT's server-wide Traders online |
LIVE-specific deltas on top of that contract:
- The badge label follows the count of unseen messages:
New Messagefor exactly one,New Messagesfor two or more. Both labels appear in the design pack. See §10 item 3. - The
Read lesscontrol is rendered inline at the end of the expanded body, inside the bubble, in the 2026-08-26 design pack. BR_4.9.1.9 instead specifies a sticky control at the bottom right of the chat window. The two panels are the same component and must not diverge — see §10 item 3. - The trader's own messages arrive through this same inbound path and receive no special treatment (Ref: BR_4.14.1.4).
BR_4.14.1.8: Chat Lifecycle Bound to the Broadcast is_live Flag
The chat panel opens and closes at the exact same moment as the video broadcast. Both are driven exclusively by the is_live flag from GET /community/live-broadcast; there is no separate schedule, field or flag that could open the chat earlier or hold it open later. The endpoint contract and its two-call cadence are owned by BR_4.5.1.3 and are not duplicated here.
- On the transition to offline the panel clears the active messages immediately and switches to the empty state of §6.2. Messages are not retained as a read-only archive; there is no post-session or intermediate state.
- On the transition back to live the panel re-hydrates from Redis. Cleared messages are never restored from client memory.
- Outbound-specific consequence. The lifecycle is a UI rule. Messages already accepted with a 202 stay in the Redis queue and are not cancelled when the session ends — the worker may still post them to
#live-broadcast-chatafter the panel has closed. The Dashboard offers no cancellation, no "unsent" state and no confirmation of what happened to an in-flight message. Discord is the system of record; the Dashboard is a window onto it that closes with the session. - Only the chat component re-renders on a session transition. Pod and The PIT hold their state, per BR_4.5.1.8.
BR_4.14.1.9: WebSocket Reconnection — the Reconnecting… State
The panel applies the shared reconnection rule CR-14: exponential backoff, automatic reconnection, and last-known values retained rather than cleared.
- Indicator. While the socket is down or reconnecting, a
Reconnecting…chip with a small spinner renders beside theLive chatpanel header, per the 2026-08-26 design pack. It is not a full-width banner and not an overlay. - The message list is not cleared and not dimmed. Bubbles already rendered stay exactly as they are. This is the point of the rule — a dropped socket must not look like an ended session, which has its own distinct empty state (Ref: BR_4.14.1.8).
- On successful reconnect the panel re-runs the single hydration request for the 50 most recent messages, so any message missed while disconnected is recovered, then resumes streaming. The chip disappears.
- The input row is unaffected. Outbound submission is plain HTTP and does not depend on the WebSocket, so an eligible trader can still send while the chip is showing. Their message will surface once the socket recovers.
- The trader is never asked to refresh the page manually.
BR_4.14.1.10: Moderation Is Two Server-Side Layers, and Both Are Silent to the Sender
There is no client-side profanity filter and no custom moderation layer in the backend. Filtering happens on the Discord side, in two independent layers, before a message can ever reach DISCORD_CHAT_STREAM.
| Layer | Component | Catches | Action | Dashboard UI |
|---|---|---|---|---|
| 1 | Discord native AutoMod — rules configured on the server, applying to every channel | Explicit profanity, slurs, banned spam links. It has no trading context and will not catch a complaint about slippage or a frozen platform | Discord blocks the post outright | None. The message never enters the channel, so the WebSocket never fires |
| 2 | Custom Node.js Discord Bot (Flow 38 — Support Interception) | Operational-complaint trigger words — slippage, lag, frozen, scam, missing payout, Rithmic down, MT5 down, TradingView down, connection dropped — routed to a Gemini sentiment gate that classifies Complaint vs Positive/Neutral, to avoid false positives such as "Stack Trading has no slippage!" | On Complaint: deletes the message in Discord, posts a public placeholder reply tagging the user, and fires a webhook that resolves the trader's email from discord_user_id and opens a Freshdesk ticket at Priority = High | None on the Dashboard. The deletion happens inside Discord |
- Flow 38 monitors
#live-broadcast-chat. Its scope was extended from#the-pit+#verifyto three channels by CR-20260818-002. The client's reasoning: AutoMod does not understand trading context, so without layer 2 a complaint about lag would be published globally on the dashboard with no ticket raised — and "a live broadcast is the worst possible time for a public support escalation." The rest of the Flow 38 pipeline is unchanged. - Ticketing is Freshdesk, not Zendesk. The source documents say Zendesk; CR-20260720-004 supersedes them.
- The sender receives no feedback in either layer. No error state, no
flaggedbubble, no removal animation, no toast. From the Dashboard's point of view the message never existed. This is the confirmed design and must be recorded so QA does not raise it as a defect — but the client has not yet ruled on whether some feedback is wanted; see §10 item 4. - Two unresolved consequences of relaying through a bot are recorded in §10 items 5 and 6: whether AutoMod evaluates bot-authored messages at all, and what happens to innocent messages bundled into an aggregated payload alongside a blocked one. Item 5 now applies to both delivery routes — neither a webhook post nor a bot post is authored by the trader's own Discord account (Ref: BR_4.14.1.3).
- ⚠️ Layer 2 loses its lookup key on the webhook route. Flow 38 opens its Freshdesk ticket by resolving the trader's email from the
discord_user_idof the message author. A webhook-posted message has no Discord user as its author — only an overridden display name — so that key does not exist. Read literally, an operational complaint sent from the Dashboard would still be deleted but would raise no ticket, which is precisely the outcome CR-20260818-002 was raised to prevent. Blocking for BE — see §10 item 14.
BR_4.14.1.11: Posting Privilege Follows Discord Roles — Digital Eviction
Posting rights live on the Discord side, so they are removed on the Discord side. Flow 7 §Digital Eviction calls POST /discord/revoke-all-roles, and the bot strips Pod Member, Pod Leader and all Title Roles — including Associate Trader — from the trader's discord_user_id.
- The trader is not kicked from the server and
discord_user_idis not deleted. They drop to the defaulteveryonerole, which grants read-only public access. - Effect on this panel: gate G2 in BR_4.14.1.1 still passes — the account is still linked — but gate G1 no longer does, because the account is no longer a LIVE Level 1+ trader. On the next page load the input row renders locked with the Level copy of priority 2 in BR_4.14.1.2.
- The change is not live within the current view. The panel evaluates the gates on mount only. Until the trader reloads, the input can still appear enabled; a submission then reaches the queue, and the bot's post is refused by Discord because the author lacks Write scope on the channel. The message silently never arrives — the same silent outcome as a moderated message (Ref: BR_4.14.1.10).
- If
discord_user_idisNULLthe revoke endpoint drops the action and returns 200 OK; nothing on this panel changes, because such an account was already locked by gate G2. - A SIM resignation revokes too, through a different caller. The revoke endpoint is reached from Flow 20, not Flow 7, and it strips the same roles;
discord_user_idand the trader's server membership are kept, and the role is reassigned via/manage-discord-roleon repurchase (Ref: BR_4.11.2.9). The effect on this panel is identical to the LIVE path above.
BR_4.14.1.12: Outbound Identity — the Webhook Posts as the Trader, Not as the Bot
A Discord webhook lets an external application override exactly two fields on every payload it sends: username and avatar_url. Route A populates both from the trader's Dashboard profile, so the message appears in #live-broadcast-chat as though the trader had typed it in the Discord app themselves (Source: CR-20260903-001, Adrian Stack — "The message will appear in the Discord channel looking exactly as if the trader posted it directly, displaying their actual name and profile picture.").
Webhook payload
| Field | Value | Source |
|---|---|---|
username | The trader's name held in the Dashboard database | Users record. ⚠️ Which name — the Dashboard Full Name or the Discord nickname — is not settled; see §10 item 11 |
avatar_url | Resolved by the three-step priority order below | Users record, then the linked Discord account |
content | message_text, at most 1,916 characters (Ref: BR_4.14.1.6) | The trader's composed message |
Avatar priority — what renders in the Discord app, not on the Dashboard. Evaluated top-down; the first case that resolves wins and the rest are not consulted.
| Priority | Source | Condition | What Discord shows |
|---|---|---|---|
| 1 | Dashboard photo — profile_photo_url on the Users record, the photo the trader uploaded in Account Settings (Ref: BR_4.6.2.7) | profile_photo_url IS NOT NULL | The trader's Dashboard photo |
| 2 | Discord photo — the avatar set on the trader's own linked Discord account | profile_photo_url IS NULL and the linked Discord account has an avatar | The trader's Discord avatar |
| 3 | Nothing supplied | Neither exists | The webhook's own configured avatar — see the note below |
- This governs the Discord surface only. The Dashboard chat bubble renders no avatar at all, in this UC or in UC_4.9.1 — it is author name, time and body (Ref: §9 row 4). This rule changes what Discord renders, and adds nothing to the panel.
- Messages typed natively in the Discord app are untouched. They keep Discord's own author metadata — nickname, role suffix and role colour — exactly as before (Ref: BR_4.9.1.6). Only the Dashboard-originated direction is overridden.
- Priority 3 is not "no picture". A Discord message always carries an avatar. Omitting
avatar_urldoes not fall through to the trader's Discord avatar — the webhook has no idea which user is behind the payload — it falls through to the image configured on the webhook itself in the Discord server settings. Priority 3 is therefore an ops-config item for Stack Trading (which image sits on the webhook), not a code branch. See §10 item 13. - Priority 2 is an active Discord API read, not a default. For the same reason, reaching priority 2 requires the backend to look the trader's avatar up (
GET /users/{discord_user_id}→ avatar hash → CDN URL) and pass it in explicitly. This is new backend scope, and it reverses BR_4.6.2.13, closed on 2026-08-27 with the client's own rule: "StackTrading never pulls an avatar or a name from the Discord API, on any surface, as a primary value or as a fallback." CR-20260903-001 is the newer client instruction and therefore wins, but the reversal must be confirmed rather than assumed — see §10 item 12. - Route B carries no identity. While the bot fallback is active the post reverts to the bot's name and avatar, with each line prefixed
[Name]:(Ref: BR_4.14.1.3). Identity is a property of the route, not of the message. - The trader still cannot choose what is shown. Both fields are derived server-side from the Users record. There is no chat-identity setting anywhere in the Dashboard, and no way to post under a name the trader has not already been assigned.
9. Screen Description
| No. | Field Name | Field Type | Displaying rule / Behaviour rule |
|---|---|---|---|
| 1 | Panel header | Label | Displaying rule:- Static header text Live chat. Displayed in both the Live and the Offline state.- Overflow: Wrap text.- No online-member count in any state. Ref: BR_4.9.1.11.Behaviour rule:- Read-only. No user interaction. |
| 2 | Reconnecting chip | Label | Displaying rule:- A small chip with a spinner and the text Reconnecting…, rendered beside the Live chat header.- Displayed only while the DISCORD_CHAT_STREAM socket is disconnected or retrying. Hidden once the socket is live. Ref: BR_4.14.1.9.Behaviour rule:- Read-only, not clickable. Reconnection is automatic with exponential backoff; the trader is never asked to refresh. The message list is neither cleared nor dimmed while it shows. Ref: CR-14. |
| 3 | Message list | Label | Displaying rule:- Vertical feed of message bubbles, newest at the bottom. Fixed rolling window of the 50 most recent messages; older nodes purged. No pagination, no infinite scroll. Ref: BR_4.9.1.4.- Populated on mount by a single hydration request from Redis, then appended live over DISCORD_CHAT_STREAM. Ref: BR_4.14.1.7.- Empty state (is_live == false): cleared immediately, body shows No live chat with the supporting line Chat will open when the session goes live. Ref: BR_4.14.1.8.Behaviour rule:- Scrollable. When anchored at the bottom, auto-scrolls on each new message; when scrolled up, holds position. Ref: BR_4.9.1.8.- Read-only, including for the sender. A message the trader just sent does not appear here until it round-trips from Discord. No pending or optimistic bubble is ever rendered. Ref: BR_4.14.1.4.- The trader cannot edit, delete, react to, or reply to any message, including their own. |
| 4 | Message bubble | Label | Displaying rule:- Three elements per bubble: author name, message time, message body.- Author name — the Discord server nickname from the author payload field, with the role designation appended from Discord role metadata (for example Coach T.). The sender's own messages use the same format, with no "me" styling. Overflow: Truncate with tooltip. Ref: BR_4.9.1.6.- Author name style — is_host == true → gold; otherwise standard white. Binary, name only. Ref: BR_4.9.1.13.- Message time — the timestamp, displayed in HH:mm. Ref: CR-08.- No avatar. The bubble renders no profile picture in any state. The photo resolved by BR_4.14.1.12 is passed to Discord and is shown in the Discord app only — it is never rendered in this panel.- Message body — plain text plus Unicode emoji, up to 1,916 characters. Custom Discord emoji, @mentions and links render as plain text; images, GIFs and rich previews are not rendered. Overflow: Wrap text, clamped at 4 rendered lines with a Read more control. Ref: BR_4.9.1.7, BR_4.9.1.9.Behaviour rule:- Read-only. No reactions, no reply, no context menu. |
| 5 | Read more / Read less | Button (Text) | Displaying rule:- Read more is displayed at the end of the visible text of a message only when the body is measurably clipped by the 4-line clamp (scroll height > visible height), re-evaluated on panel resize. Never decided from a character count. Ref: BR_4.9.1.9.- Read less is displayed only while a message is expanded. Position per the 2026-08-26 design pack: inline at the end of the expanded body. ⚠️ Diverges from BR_4.9.1.9, which specifies a sticky control at the bottom right of the chat window — see §10 item 3.Behaviour rule:- On click Read more: expands the message inline. The chat panel keeps its fixed height — the body is read by scrolling inside the panel; the Live Broadcast tile does not resize and no card below is pushed down. Not a modal, not a side panel.- Only one message is expanded at a time; expanding a second collapses the first.- Expanding must not trigger auto-scroll — the top of the expanded message holds its on-screen position.- On click Read less: collapses the message back to its truncated form. |
| 6 | New Messages badge | Button (Text) | Displaying rule:- A small floating badge at the bottom of the message list. Label New Message when exactly one message is unseen, New Messages when two or more. Ref: BR_4.14.1.7.- Displayed only while the trader has scrolled up away from the bottom and at least one new message has arrived. Hidden while the view is anchored at the bottom. Ref: BR_4.9.1.8.Behaviour rule:- On click: jumps the list to the newest message and re-anchors the view at the bottom; the badge is then hidden. |
| 7 | Message input | Textbox | Displaying rule:- Never removed from the layout. Four mutually exclusive states, resolved by the precedence order in BR_4.14.1.2: 1. is_live == false → Disabled, placeholder Waiting for session... 2. Level 0 (SIM) → Disabled + 🔒, placeholder Pass the Associate Track to unlock chat (owned by BR_4.9.1.2) 3. discord_user_id == null → Disabled + 🔒, placeholder Link your Discord account first to unlock chat 4. Both gates passed → Enabled, placeholder Type a message- Overflow: Block input — character 1,917 cannot be entered, and no message is shown. No character counter. Cap lowered from 2,000 by CR-20260903-001. Ref: BR_4.14.1.6.Behaviour rule:- In any disabled state the field cannot receive focus and accepts no keystrokes.- When enabled: Enter submits, Shift+Enter inserts a line break. Plain text and Unicode emoji only; no emoji picker, no attachment control, no @mention auto-complete.- Cleared automatically on HTTP 202. Retained on a rate-limit rejection or a network error, so the trader never retypes. Ref: BR_4.14.1.5.- Whitespace-only input is treated as empty and is not submitted. |
| 8 | Send | Button (Primary) | Displaying rule:- Label Send. Enabled only in state 4 of Row 7; Disabled in states 1–3. Ref: BR_4.14.1.2.Behaviour rule:- On click (enabled): calls POST /community/chat/send with { user_id, message_text } and awaits HTTP 202 Accepted. Ref: BR_4.14.1.3.- Impact: the message is queued in Redis and drained by the dispatcher — over the Discord Webhook by default, or over the bot batching route while the queue is backed up. Nothing is written to the message list — the message appears only when it streams back over DISCORD_CHAT_STREAM, and never appears at all if either moderation layer blocks it. The route in use is not surfaced anywhere in the UI. Ref: BR_4.14.1.3, BR_4.14.1.4, BR_4.14.1.10.- On click (disabled): no action, no request. |
| 9 | Rate limit notice | Label | Displaying rule:- Inline notice inside the chat panel, adjacent to the input row: Message rate limit reached. Please wait a moment.- Displayed only when POST /community/chat/send is rejected by the per-user rate limit of 1 request per second. Not a modal and not a global toast. Ref: BR_4.14.1.5.- Overflow: Wrap text.Behaviour rule:- Read-only. Clears on the next successful submission or when the trader edits the input. The composed text is retained behind it. |
Related behaviour owned elsewhere:
- The link-token instruction block shown on The PIT card when
discord_user_idisnullis owned by BR_4.5.2.7. This panel states the blocker only.- When the trader navigates into the Community screen, the Newsquawk audio stream in the persistent header switches to Off automatically. Owned by BR_4.1.4.4.
10. Open Items Pending Client Confirmation
| # | Item | Written in this UC as | Why it needs a ruling | Severity |
|---|---|---|---|---|
| 2 | Author attribution on the bot fallback route. (Narrowed by CR-20260903-001 — the webhook route resolves it; route B does not.) Under fallback the bot still posts one Discord message carrying several traders' text, under its own identity. No source says how author and is_host are recovered for the inbound payload in that case. | BA proposal unchanged: carry author / is_host through the Redis queue and split an aggregated post into N separate WebSocket payloads (Ref: BR_4.14.1.3) | Only bites while the fallback is active, but that is peak traffic. Without a ruling the feed renders one bubble authored by the bot containing several people's lines, breaking BR_4.9.1.5 and the Host highlight. Blocking for BE. | 🔴 |
| 3 | Two divergences between the LIVE design pack and UC_4.9.1, which are the same component: (a) Read less inline in the bubble vs. sticky at the panel's bottom right; (b) the badge label New Message vs New Messages. | (a) per the design pack, flagged; (b) singular by count | The panels must not diverge. (a) needs a design ruling; (b) needs only a confirmation that the count-based label is intended. | 🟡 |
| 4 | Sender feedback when a message is blocked. A message blocked by AutoMod never returns, so the sender sees it vanish with no explanation. (Carried over as Open Item #15 in QnA_STAGE2_COMMUNITY_LIVE_BROADCAST.md.) | Accepted silence, documented explicitly so QA does not log it as a defect (Ref: BR_4.14.1.10) | If the client wants feedback, the backend must read AutoMod events and push a new WebSocket event — new scope for BE and FE. | 🟡 |
| 5 | Does Discord AutoMod evaluate messages posted by a bot — or by a webhook? The entire layer-1 moderation model assumes it does, but no Dashboard-originated message reaches the channel through the trader's own account: it arrives either as a webhook post or as a bot post. (Scope widened by CR-20260903-001 to cover both routes.) | Written as the client described it (Ref: BR_4.14.1.10) | To be validated by BE against Discord's platform behaviour, for each route separately. If AutoMod does not apply, the Dashboard has no profanity filter at all and a custom pre-post filter becomes new scope. | 🔴 |
| 6 | Collateral loss in an aggregated payload. (Narrowed by CR-20260903-001 to the fallback route.) If several traders' messages are concatenated into one Discord post and any one line trips AutoMod or Flow 38, the whole payload is blocked or deleted — including the innocent messages bundled with it. | Not resolved; consequence recorded (Ref: BR_4.14.1.3, BR_4.14.1.10) | Now only occurs while the queue is backed up, so the frequency drops sharply — but that is exactly peak traffic, the moment the client most wants to avoid a support escalation. Item 2's per-sender split would also resolve this. | 🟡 |
| 7 | Flow 38 public placeholder reply during a live broadcast. In #the-pit the bot posts a public reply tagging the user. In #live-broadcast-chat that reply would itself stream onto every Dashboard. (Open question 1 in CR-20260818-002.) | Written as the existing Flow 38 behaviour (Ref: BR_4.14.1.10) | A public reply mid-broadcast may draw more attention than the deleted message did. A silent-delete + DM variant may be wanted for this channel. | 🟡 |
| 9 | Mobile layout of the chat panel. [DCS-UI-02] was never answered. [LB-UI-03] implies video stacked above chat. (Open Item #5 in the QnA file.) | Not specified in this UC | Responsive spec for FE. | 🟢 |
| 10 | Design pack not in the repository. The six states in §1 came to BA as images on 2026-08-26 and are not under References/Wireframe/Stage 2.2/. | Cited as "client design pack, 2026-08-26" | The UC currently cites a source that cannot be opened from the repo. The files should be committed under References/Wireframe/Stage 2.2/Community & Live Broadcast/. | 🟢 |
| 11 | Which name goes into the webhook username? The CR says "the trader's name from the database", which reads as the Dashboard Full Name. [DCS-BIZ-01] and BR_4.9.1.6 fixed the display name as the Discord nickname plus role suffix. | Written as the CR states it, with the conflict flagged (Ref: BR_4.14.1.12) | Two consequences, both needing a client ruling. (a) Privacy: #live-broadcast-chat is a public channel — publishing a trader's legal, KYC-captured name into it is a privacy change, not a UI change. (b) Identity: the Full Name carries no role suffix, so Coach/Mentor designations disappear from every Dashboard-sent message. BA proposal: use the Discord nickname, and fall back to the Dashboard name only when no nickname exists. | 🔴 |
| 12 | Avatar priority 2 reverses a rule the client themself closed. BR_4.6.2.13, closed 2026-08-27: "StackTrading never pulls an avatar or a name from the Discord API, on any surface, as a primary value or as a fallback" — and "Discord server — StackTrading neither reads it nor influences it." Priority 2 requires reading it; the webhook override is influencing it. | CR wins as the newer instruction; priority order written as supplied, reversal flagged (Ref: BR_4.14.1.12) | The client's own 2026-08-27 rationale — "expired CDN links... a dead link would be a visible defect" — is still technically true; only the surface has moved from the Dashboard to Discord. Needs a conscious confirmation, plus BE scope for a Discord API read with avatar-hash refresh. | 🔴 |
| 13 | Avatar priority 3 does not exist as stated. "Không hiển thị gì" is not reachable — a Discord message always shows an avatar, and omitting avatar_url yields the image configured on the webhook itself, not a blank. Also unresolved: whether the S3 profile_photo_url is publicly fetchable by Discord's servers, since a private or signed URL silently degrades every trader to this case. | Recorded as the webhook's own configured avatar (Ref: BR_4.14.1.12) | Two decisions: an ops-config choice for Stack Trading (which image to put on the webhook — Stack Trading logo, or Discord's grey default), and a BE confirmation on S3 URL visibility. The Dashboard's own initials fallback (BR_4.6.2.7) cannot be reproduced in Discord. | 🟡 |
| 14 | Flow 38 loses its lookup key on the webhook route. The ticket step resolves the trader's email from the message author's discord_user_id. A webhook message has no Discord user as its author. | Consequence recorded, no mechanism specified (Ref: BR_4.14.1.10) | Read literally, an operational complaint sent from the Dashboard is deleted but raises no Freshdesk ticket — the exact failure CR-20260818-002 exists to prevent. Possible fixes: match back on the overridden username, or have BE persist a webhook_message_id → user_id map. Blocking for BE. | 🔴 |
| 15 | author and is_host for a webhook-posted message on the way back in. A webhook message carries no Discord role metadata, so neither the role suffix nor the Host role can be read from it. | Not specified (Ref: BR_4.14.1.7, BR_4.9.1.5) | Left as-is, every Dashboard-sent message streams back with is_host = false and no role suffix — including one sent by the Host. BE must say whether it re-resolves both from the user_id still held in the queue, or reads them back from Discord. | 🟡 |
| 16 | Mixed rendering inside one session while the fallback is active. Traders will see native per-trader bubbles, then a single concatenated bot post during a burst, then native bubbles again — with no explanation. | Recorded as accepted behaviour (Ref: BR_4.14.1.3, §7) | This is the "yikes" UI the CR was raised to remove, reappearing at exactly the busiest moment. The client should confirm it is acceptable unannounced, or ask for a marker. | 🟡 |
| 17 | Where the 1,916-character cap is enforced, and the shape of the fallback trigger. (a) FE maxlength = 1916, or 2,000 in the input with server-side truncation or rejection? (b) Is "> 5 pending" measured on the whole channel queue or per user? (c) Is there a ceiling on how long a remainder may wait across successive one-second ticks? (d) What happens if Discord rejects a send anyway — retry, or drop? | (a) written as FE maxlength = 1916; (b)–(d) not specified | (a) changes a visible input constraint and must not be a silent truncation. (b)–(d) determine the worst-case delay a trader can observe at peak, and are needed before QC can write test cases. | 🟡 |
Closed by this document:
| # | Was open | Now |
|---|---|---|
| 1 | Unlinked state: hide the input, or lock it? Client answer A18 said the ability to send "should be hidden"; the 2026-08-26 design pack drew the row present and locked with a 🔒 icon and the placeholder Link your Discord account first to unlock chat. | ✅ Closed by BA, 2026-08-26 — locked, not hidden. The design wins: a vanishing input gives the trader no reason for the block, and removing the row breaks the grid the client twice asked to keep intact. A18 is superseded on this point. Ref: BR_4.14.1.2. 📋 Follow-up: annotate [DCS-BIZ-18] in QnA_STAGE2_COMMUNITY_LIVE_BROADCAST.md so the next reader does not re-open it. |
Changelog
| Date | Version | Updated item | Before | After | Notes |
|---|---|---|---|---|---|
| 2026-09-06 | v1.3 | §10 item 8 removed | Item 8 "Race between render and deletion" still listed as pending, closed on mechanism but open on compliance | Closed and removed. A message deleted in Discord after it was broadcast stays on screen until the 50-message window purges it, and that is accepted — no retract event, no frontend withdrawal behaviour | Ruling relayed by BA, 2026-09-06. The rule lives in BR_4.9.1.14 / BR_4.9.1.10 and is not restated here. Also closes open question 4 of CR-20260818-002 |
| 2026-09-04 | v1.2 | BR_4.14.1.6 composition table · BR_4.14.1.7 delegation table · §10 item 8 | Media covered as "image / GIF / attachment not supported" outbound only; a URL described as having "no link preview", which did not forbid a clickable link; no rule on edits or deletions performed in the Discord app; item 8 left the retract mechanism undecided | Rows now point at the inbound owners in UC_4.9.1: media dropped by the backend (BR_4.9.1.7), append-only feed with no edit / delete sync (BR_4.9.1.14), URLs plain and non-clickable in both directions as a security control (BR_4.9.1.15). Item 8 closed on mechanism, still open on compliance | Reference only — the three rules are written once, in UC_4.9.1, and are not restated here. Applies the client answers [DCS-BIZ-23], [DCS-BIZ-24] and [DCS-BIZ-25] (Source: QnA_STAGE2_COMMUNITY_LIVE_BROADCAST.md — Discord Chat Stream, Adrian Stack, 2026-09-04) |
| 2026-09-03 | v1.1 | BR_4.14.1.3 rewritten · new BR_4.14.1.12 · BR_4.14.1.6 cap · §1 · §4 · §5.4/5.6/5.8/5.10 · §7 · BR_4.14.1.10 · §9 rows 4/7/8 · §10 items 2, 5, 6 + new 11–17 | Single outbound route. Redis queue → Node.js Discord Bot worker → posts under the bot's identity; concatenates several senders into one payload ([User A]: Message \n [User B]: Message) whenever inbound velocity exceeded 1 message per second. Composition capped at 2,000 characters. No outbound identity rule — attribution was Open Item #2, 🔴 blocking BE | Dual route, webhook first. Route A (default) = Discord Webhook, one request per message, username + avatar_url overridden from the trader's Dashboard profile so the message renders natively as the trader. Route B (fallback) = bot batching, entered when the queue exceeds 5 pending or the webhook budget of 30 requests / 60 seconds is exhausted, concatenating up to Discord's 2,000-character post ceiling and leaving the remainder for the next ~1-second tick; reverts to A once the queue drains. Two separate token buckets pace the routes. Composition cap lowered to 1,916 characters. New BR_4.14.1.12 fixes the outbound identity and the avatar priority for the Discord app: (1) Dashboard photo → (2) Discord photo → (3) the webhook's own avatar | Applies CR-20260903-001 (Adrian Stack, meeting + Backend team confirmation). Client's reason: the batched bot transcript was "pretty ugly... the visual result is.. yikes." Supersedes RFQ_ Stack Trading Prop Tech V7.pdf §2.3 (batching as the sole route) and the delivery half of [DCS-BIZ-20]; the 202 / Redis / no-optimistic-render contract is unchanged. Open Item #2 narrowed to route B, #5 widened to both routes, #6 narrowed to route B. Seven new open items (11–17), four of them 🔴 — the username source and its PII exposure, the reversal of BR_4.6.2.13 implied by avatar priority 2, Flow 38 losing its discord_user_id lookup key, and author/is_host resolution on the way back in |
| 2026-08-26 | v1 | §10 item 1 · BR_4.14.1.2 — unlinked-state input closed | ⏳ Open item: client answer A18 said the send capability "should be hidden", while the 2026-08-26 design pack drew it locked; the UC followed the design and flagged the divergence for confirmation | ✅ Locked, not hidden. The input row stays in the layout, disabled, with a 🔒 icon and the placeholder Link your Discord account first to unlock chat. A18 superseded on this point; the ⚠️ divergence note in BR_4.14.1.2 replaced by a confirmed-decision note, and the item moved to the closed table in §10 | BA decision, 2026-08-26. Rationale unchanged from the original flag: a disappearing input explains nothing, and removing the row contradicts the client's own grid-stability constraint. Not logged as a CR — both sources are client-originated and the newer (design pack) wins; no scope or endpoint change results. Open items 2–10 are unaffected and keep their numbering. |
| 2026-08-26 | v1 | Initial document | — | — | Created for the LIVE (TWO-WAY) counterpart of UC_4.9.1. Closes the outbound boundary that BR_4.9.1.12 deferred to this UC. Content sourced from client-confirmed answers [DCS-DEF-01] → [DCS-BIZ-21], [DCS-UI-01] → [DCS-UI-03], [DCS-EP-01] / [DCS-EP-02] in Stage 2 - Dashboard & Fomula (Community).csv; RFQ_ Stack Trading Prop Tech V7.pdf §2.3 and §Community Endpoints; Discord Server Details.md; CR-20260818-002 (Flow 38 extended to #live-broadcast-chat); and the client design pack of 2026-08-26. Inbound render rules are referenced, not duplicated, from UC_4.9.1 (Ref: BR_4.14.1.7). Ten open items logged in §10, two of them 🔴 blocking for BE. |