Skip to content

feat(bans): show the ban reason to the banned player#4659

Open
iiamlewis wants to merge 4 commits into
mainfrom
feat-ban-reason-screen
Open

feat(bans): show the ban reason to the banned player#4659
iiamlewis wants to merge 4 commits into
mainfrom
feat-ban-reason-screen

Conversation

@iiamlewis

Copy link
Copy Markdown
Contributor

Why

Completes the player-facing half of the account-bans feature (infra side: openfrontio/infra bans PRs). The API already returns the caller's active ban on GET /users/@me:

"ban": { "category": "cheating", "reason": "aimbot in ranked", "expiresAt": "2026-08-01T…" } // or null

…but the game ignored it. A banned player only ever saw the WebSocket close string "Account Banned" — none of the category, reason, or expiry we now store reached them. This surfaces it.

What

  • ApiSchemas — add ban to UserMeResponseSchema (top-level, .nullable().optional() so an older API is treated as "no ban"). category is kept as a plain string so a value a newer server introduces degrades gracefully instead of failing the whole @me parse.

  • New <banned-modal> — reads the userMeResponse document event (same pattern as <marketing-consent-toast>), and when a ban is present opens an <o-modal> showing:

    • the localized category (ban_notice.category.*),
    • the moderator's player-facing reason (if any),
    • "Banned until {date}" or "This ban is permanent.",
    • a line pointing to Discord to appeal.

    It opens once; the player can close it (the server still enforces the ban — this is informational).

  • en.jsonban_notice.* strings, including a label for each of the 12 ban_category values.

  • Wired into Main.ts + index.html beside the marketing toast.

Testing

  • Schema (tests/ApiSchemas.test.ts): active temp ban, permanent (null reason/expiry), null / missing (no ban), and a malformed ban with no category.
  • Component (tests/client/BannedModal.test.ts): not-banned → renders nothing; @me failed → nothing; temp ban shows category + reason + lift date; permanent ban shows "permanent" and omits the reason line; an unknown category falls back to the generic ToS label.
  • Full suite 2099 passed; tsc --noEmit, ESLint, Prettier, the translation-sync test, and the en.json-sorted test all pass.

Notes

  • No game-server change. Enforcement stays exactly as-is (WS close on a banned token); this only adds the reason display, which depends solely on the already-deployed @me.ban field.
  • Migrated bans (category other, no reason, permanent) render as "Terms of Service violation — This ban is permanent."
  • Dates use the player's locale; the API stores UTC.

🤖 Generated with Claude Code

Completes the player-facing half of the account-bans feature. The infra API
already returns the active ban on GET /users/@me (category, player-facing
reason, expiry); until now the game ignored it and a banned player only saw the
WebSocket close string "Account Banned".

- ApiSchemas: add `ban` to UserMeResponseSchema (top-level, optional/nullable so
  an older API is treated as "no ban"). category kept as a string so an
  unrecognised value degrades gracefully.
- New <banned-modal>: reads the `userMeResponse` event (like the marketing
  toast), and when a ban is present opens an <o-modal> showing the localized
  category, the reason, and "banned until <date>" / "permanent". Opens once; the
  player can close it (the server still enforces the ban).
- en.json: ban_notice.* strings incl. a label per ban_category.

Tests: schema (ban present/permanent/null/malformed) and the component
(temp/permanent/no-ban/@me-failed/unknown-category fallback). Full suite green
(2099); tsc, ESLint, Prettier, translation-sync and en.json-sorted all pass.

Depends on the infra @me `ban` field (already deployed); the game server is
unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d28bbbaa-e83e-4a7c-acd1-48ff7354fa84

📥 Commits

Reviewing files that changed from the base of the PR and between 9e4007b and 5579298.

📒 Files selected for processing (6)
  • index.html
  • resources/lang/en.json
  • src/client/Main.ts
  • src/client/components/BannedModal.ts
  • src/core/ApiSchemas.ts
  • tests/client/BannedModal.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/client/Main.ts
  • tests/client/BannedModal.test.ts
  • resources/lang/en.json

Walkthrough

A new ban schema, English translations, and BannedModal component display temporary or permanent ban details from userMeResponse events. The component is bundled and mounted globally, with tests covering rendering, clearing, validation, and unknown categories.

Changes

Banned player notice

Layer / File(s) Summary
Ban data and translations
src/core/ApiSchemas.ts, resources/lang/en.json, tests/ApiSchemas.test.ts
Player responses accept optional ban details, with English translations and schema tests for temporary, permanent, missing, and invalid ban data.
Ban modal behavior
src/client/components/BannedModal.ts, tests/client/BannedModal.test.ts
BannedModal listens for ban responses, renders localized details, handles unknown categories, clears after an unban response, and opens the modal once.
Page and bundle integration
src/client/Main.ts, index.html
The component is loaded in the client bundle and added to the main page markup.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant UserMeResponse
  participant BannedModal
  participant translateText
  participant o-modal
  UserMeResponse->>BannedModal: dispatch userMeResponse with ban details
  BannedModal->>translateText: resolve localized notice text
  BannedModal->>o-modal: render ban content
  BannedModal->>o-modal: open once
Loading

Suggested labels: UI/UX

Suggested reviewers: evanpelle

Poem

A ban arrives, the modal wakes,
With translated paths and clear mistakes.
Temporary dates or permanent signs,
An appeal shines in friendly lines.
One open only—then calm takes place.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change by describing player-facing ban details, especially showing the ban reason.
Description check ✅ Passed The description is clearly about the ban notice modal, schema update, translations, and tests in this PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
resources/lang/en.json

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/client/components/BannedModal.ts (2)

8-8: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the Ban type.

Ahoy! We only need to use NonNullable once because it automatically removes both null and undefined from the type for us.

✨ Proposed fix
-type Ban = NonNullable<NonNullable<UserMeResponse["ban"]>>;
+type Ban = NonNullable<UserMeResponse["ban"]>;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/components/BannedModal.ts` at line 8, Update the Ban type alias to
apply NonNullable only once to UserMeResponse["ban"], removing the redundant
nested wrapper while preserving the resulting non-null ban type.

70-89: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use Tailwind CSS classes instead of inline styles.

Ahoy! I noticed we are using inline styles for this modal. Since we use Tailwind CSS 4 for our UI, let's switch to Tailwind classes to keep our styling clean, idiomatic, and easy to maintain.

✨ Proposed fix
-        <div style="max-width: 32rem; line-height: 1.5;">
-          <p style="font-weight: 600; font-size: 1.05rem; margin: 0 0 0.5rem;">
+        <div class="max-w-lg leading-normal">
+          <p class="font-semibold text-lg mb-2">
             ${this.categoryLabel(ban.category)}
           </p>
           ${ban.reason
-            ? html`<p style="margin: 0 0 0.5rem;">
+            ? html`<p class="mb-2">
                 ${translateText("ban_notice.reason", { reason: ban.reason })}
               </p>`
             : null}
-          <p style="margin: 0 0 0.75rem;">
+          <p class="mb-3">
             ${ban.expiresAt
               ? translateText("ban_notice.until", {
                   date: new Date(ban.expiresAt).toLocaleString(),
                 })
               : translateText("ban_notice.permanent")}
           </p>
-          <p style="margin: 0; opacity: 0.8;">
+          <p class="m-0 opacity-80">
             ${translateText("ban_notice.appeal")}
           </p>
         </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/components/BannedModal.ts` around lines 70 - 89, Replace the
inline style attributes in the modal content rendered by BannedModal with
equivalent Tailwind CSS utility classes, preserving the existing layout,
typography, spacing, and opacity for the container and all paragraphs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/components/BannedModal.ts`:
- Around line 28-32: Update onUserMeResponse so the no-ban path (detail ===
false or missing detail.ban) clears both this.ban and this.opened before
returning. Preserve the existing assignment of detail.ban for banned users.

---

Nitpick comments:
In `@src/client/components/BannedModal.ts`:
- Line 8: Update the Ban type alias to apply NonNullable only once to
UserMeResponse["ban"], removing the redundant nested wrapper while preserving
the resulting non-null ban type.
- Around line 70-89: Replace the inline style attributes in the modal content
rendered by BannedModal with equivalent Tailwind CSS utility classes, preserving
the existing layout, typography, spacing, and opacity for the container and all
paragraphs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 311ca3de-f493-46d6-871d-916f63f3b91e

📥 Commits

Reviewing files that changed from the base of the PR and between da2e091 and 9e4007b.

📒 Files selected for processing (7)
  • index.html
  • resources/lang/en.json
  • src/client/Main.ts
  • src/client/components/BannedModal.ts
  • src/core/ApiSchemas.ts
  • tests/ApiSchemas.test.ts
  • tests/client/BannedModal.test.ts

Comment thread src/client/components/BannedModal.ts
@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management Jul 21, 2026
@ryanbarlow97 ryanbarlow97 added this to the v33 milestone Jul 21, 2026
Addresses review: the no-ban path now resets ban + opened, so an unban that
lands mid-session clears the modal (and a later re-ban reopens it). Test added.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Development

Development

Successfully merging this pull request may close these issues.

2 participants