Skip to content

Promo card overlay on game selector #4575

Open
troshujin wants to merge 6 commits into
openfrontio:mainfrom
troshujin:main
Open

Promo card overlay on game selector #4575
troshujin wants to merge 6 commits into
openfrontio:mainfrom
troshujin:main

Conversation

@troshujin

@troshujin troshujin commented Jul 11, 2026

Copy link
Copy Markdown

Add approved & assigned issue number here:

Resolves #4573

Description:

Adds promo cards read from a json file overlay to specified game lobby select slots.

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory

Please put your Discord username so you can be contacted if a bug or regression is found:

troshujin

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds configurable promotional overlays to lobby slots. Overlay settings are schema-validated, fetched with a local fallback, scheduled from lobby changes, animated through video phases, and dismissible from the rendered card.

Changes

Lobby Card Overlays

Layer / File(s) Summary
Overlay schema, data, and loading
src/core/ApiSchemas.ts, resources/lobby-card-overlays.json, src/client/Api.ts
Defines the overlay schema and type, adds two overlay configurations, and loads validated data with fallback handling.
Overlay scheduling and lifecycle
src/client/GameModeSelector.ts
Loads overlays, tracks per-slot lobby changes, schedules display intervals, manages video and TTL transitions, clears timers, and updates map data lookup.
Overlay card rendering
src/client/GameModeSelector.ts
Routes lobby slots through overlay-aware rendering and displays media, reveal content, and a dismiss button.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GameModeSelector
  participant getLobbyCardOverlays
  participant lobby-card-overlays.json
  participant LobbySlot
  GameModeSelector->>getLobbyCardOverlays: Load overlay definitions
  getLobbyCardOverlays->>lobby-card-overlays.json: Fetch configuration
  lobby-card-overlays.json-->>getLobbyCardOverlays: Return overlay data
  getLobbyCardOverlays-->>GameModeSelector: Return validated overlays
  LobbySlot->>GameModeSelector: Report changed lobby gameID
  GameModeSelector->>GameModeSelector: Evaluate interval and trigger overlay
  GameModeSelector-->>LobbySlot: Render overlay card
Loading

Suggested labels: UI/UX

Suggested reviewers: evanpelle

Poem

Lobby cards bloom with a bright new show,
Timed invitations gently glow.
Videos play, then prizes appear,
A click sends each one away clear.
Slots now sparkle, row by row.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement a dismissible promo overlay on lobby slots with JSON-based frequency and alternating configs, matching #4573.
Out of Scope Changes check ✅ Passed The reviewed changes stay focused on the promo overlay feature and do not show clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly matches the main change: promo overlay cards in the game selector.
Description check ✅ Passed The description is on-topic and describes adding promo cards from JSON overlays to lobby slots.

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: 5

🧹 Nitpick comments (2)
src/core/ApiSchemas.ts (1)

353-371: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Tighten numeric field constraints.

slot, interval, offset, and ttl accept any number, including negative or fractional values. slot is used as an array index and interval/offset feed a modulo check in checkOverlayTriggers (GameModeSelector.ts) — fractional or negative values there could make promotions never trigger or trigger unpredictably.

♻️ Suggested constraint tightening
-  slot: z.number(),
-  interval: z.number(),
-  offset: z.number().default(0),
-  ttl: z.number(),
+  slot: z.number().int().min(0),
+  interval: z.number().int().min(0),
+  offset: z.number().int().min(0).default(0),
+  ttl: z.number().int().min(0),
🤖 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/core/ApiSchemas.ts` around lines 353 - 371, The numeric fields in
LobbyCardOverlaySchema are too permissive. Constrain slot, interval, offset, and
ttl to non-negative integers, while preserving offset’s default of 0; use the
resulting validated values unchanged by checkOverlayTriggers.
src/client/GameModeSelector.ts (1)

167-201: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Consider tests for the overlay scheduling state machine.

checkOverlayTriggers/triggerOverlay implement a fairly intricate two-pass counting + phase-timer state machine (four phases, three timer maps). The untracked-timer bug found nearby is a good example of the kind of regression tests would catch.

🤖 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/GameModeSelector.ts` around lines 167 - 201, add tests for the
overlay scheduling state machine centered on checkOverlayTriggers and
triggerOverlay, covering the two-pass slot counting, interval/offset phase
behavior across all four phases, active-overlay suppression, and timer
tracking/cleanup including the untracked-timer regression case.
🤖 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/GameModeSelector.ts`:
- Around line 112-119: Track the 800ms handleVideoReady fallback timer created
by triggerOverlay in a dedicated timer map or the existing overlay timer
tracking, then clear and remove it in both disconnectedCallback and
dismissOverlay alongside the video-length and dismiss timers. Ensure the
fallback cannot invoke setActiveOverlay after the overlay is dismissed or the
component disconnects.
- Around line 598-684: Update the overlay markup in GameModeSelector so the
sibling container holding the count badge and subtitle is rendered only when
showReveal is true, matching the existing title/image block. Keep the count and
subtitle content unchanged and ensure neither reveal block appears during
entering or video phases.
- Around line 341-368: The responsive branches in the GameModeSelector render
duplicate cards, causing overlay videos to mount and play in hidden copies.
Update the rendering around renderCard and the special/ffa/teams slot branches
so an overlay card is created only once for the active viewport, or ensure
hidden branches do not assign video src or autoplay; preserve the existing
desktop and mobile layout behavior.

In `@src/core/ApiSchemas.ts`:
- Around line 352-372: Add tests for LobbyCardOverlaySchema using safeParse,
covering a valid payload and invalid payloads with missing required fields and
incorrect types. Ensure the tests also verify the offset default behavior and
exercise the exported LobbyCardOverlay schema contract.
- Around line 353-371: Restrict the linkTo field in LobbyCardOverlaySchema to
URLs using only the http or https protocols, replacing the unrestricted z.url()
validation. Update src/core/ApiSchemas.ts lines 353-371;
src/client/GameModeSelector.ts lines 592-597 requires no direct change because
the schema validation provides the protection.

---

Nitpick comments:
In `@src/client/GameModeSelector.ts`:
- Around line 167-201: add tests for the overlay scheduling state machine
centered on checkOverlayTriggers and triggerOverlay, covering the two-pass slot
counting, interval/offset phase behavior across all four phases, active-overlay
suppression, and timer tracking/cleanup including the untracked-timer regression
case.

In `@src/core/ApiSchemas.ts`:
- Around line 353-371: The numeric fields in LobbyCardOverlaySchema are too
permissive. Constrain slot, interval, offset, and ttl to non-negative integers,
while preserving offset’s default of 0; use the resulting validated values
unchanged by checkOverlayTriggers.
🪄 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: 264facca-43b4-4a40-981a-bcdfe9a90464

📥 Commits

Reviewing files that changed from the base of the PR and between d766913 and 9312d4f.

⛔ Files ignored due to path filters (2)
  • resources/images/promo/trophy.png is excluded by !**/*.png
  • resources/video/promo/ofm_card_intro.mp4 is excluded by !**/*.mp4
📒 Files selected for processing (4)
  • resources/lobby-card-overlays.json
  • src/client/Api.ts
  • src/client/GameModeSelector.ts
  • src/core/ApiSchemas.ts

Comment on lines +112 to +119
for (const timer of this.overlayTimers.values()) {
clearTimeout(timer);
}
this.overlayTimers.clear();
for (const timer of this.overlayDismissTimers.values()) {
clearTimeout(timer);
}
this.overlayDismissTimers.clear();

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Untracked setTimeout for handleVideoReady — never cleared.

The 800ms fallback timer in triggerOverlay isn't stored anywhere, unlike the video-length timer (via setOverlayTimer) and the dismiss timer (via overlayDismissTimers) set right next to it. disconnectedCallback (lines 112-119) and dismissOverlay only clear those two maps, so this timer keeps running after disconnect/dismissal and can call setActiveOverlay on a torn-down or unrelated overlay state later.

🔧 Suggested fix: track the ready-fallback timer like the others
+  private overlayReadyTimers: Map<number, ReturnType<typeof setTimeout>> = new Map();
+
   private triggerOverlay(overlay: LobbyCardOverlay) {
     const slot = overlay.slot;
     this.setActiveOverlay(slot, { slot, overlay, phase: "entering" });
-    setTimeout(() => this.handleVideoReady(slot), 800);
+    const existingReady = this.overlayReadyTimers.get(slot);
+    if (existingReady) clearTimeout(existingReady);
+    this.overlayReadyTimers.set(
+      slot,
+      setTimeout(() => this.handleVideoReady(slot), 800),
+    );
     this.setOverlayTimer(slot, (overlay.video.videoLength + 2) * 1000, () =>
       this.advanceOverlayToFading(slot),
     );

Then clear it alongside the other maps in disconnectedCallback and in dismissOverlay.

Also applies to: 227-240

🤖 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/GameModeSelector.ts` around lines 112 - 119, Track the 800ms
handleVideoReady fallback timer created by triggerOverlay in a dedicated timer
map or the existing overlay timer tracking, then clear and remove it in both
disconnectedCallback and dismissOverlay alongside the video-length and dismiss
timers. Ensure the fallback cannot invoke setActiveOverlay after the overlay is
dismissed or the component disconnects.

Comment on lines +341 to 368
${this.renderCard(SLOT_FFA, ffa)}
</div>`
: nothing}

<!-- Right col: special + teams (desktop only) -->
<div class="hidden sm:flex sm:flex-col sm:gap-4">
${special
? html`<div class="flex-1 min-h-0">
${this.renderSpecialLobbyCard(special)}
${this.renderCard(SLOT_SPECIAL, special)}
</div>`
: nothing}
${teams
? html`<div class="flex-1 min-h-0">
${this.renderLobbyCard(teams, this.getLobbyTitle(teams))}
${this.renderCard(SLOT_TEAM, teams)}
</div>`
: nothing}
</div>

<!-- Mobile: special, ffa, teams inline -->
<div class="sm:hidden">
${special ? this.renderSpecialLobbyCard(special) : nothing}
${special ? this.renderCard(SLOT_SPECIAL, special) : nothing}
</div>
<div class="sm:hidden">
${ffa
? this.renderLobbyCard(ffa, this.getLobbyTitle(ffa))
: nothing}
${ffa ? this.renderCard(SLOT_FFA, ffa) : nothing}
</div>
<div class="sm:hidden">
${teams
? this.renderLobbyCard(teams, this.getLobbyTitle(teams))
: nothing}
${teams ? this.renderCard(SLOT_TEAM, teams) : nothing}
</div>

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.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the selector and the card renderer around the referenced lines.
ast-grep outline src/client/GameModeSelector.ts --view expanded

# Read the relevant sections with line numbers.
sed -n '300,760p' src/client/GameModeSelector.ts | cat -n

Repository: openfrontio/OpenFrontIO

Length of output: 25573


🌐 Web query:

display:none video autoplay still loads decodes browser behavior hidden video element muted autoplay

💡 Result:

The behavior of a hidden video element (e.g., set to display:none) regarding loading, decoding, and autoplay is not uniform across all browsers, though several key patterns exist. Generally, setting a video element to display:none does not prevent the browser from loading or buffering the video resource [1][2][3]. The browser treats the video element as part of the document regardless of its CSS display property, meaning the network request for the video file will typically still occur unless other factors (such as the preload attribute) intervene [1][4][5]. Regarding autoplay, browser behavior has become increasingly restrictive to save resources and improve user experience: 1. Loading and Decoding: Applying display:none does not inherently stop the browser from downloading or decoding the media [1][2]. If the browser's preload strategy (e.g., preload="auto") is active, the video will still be fetched [4][6]. 2. Autoplay Restrictions: Modern browsers implement strict policies regarding autoplay [7]. While muted autoplay is generally permitted when visible, many browsers have introduced "invisible autoplay" restrictions [8][9]. These policies may pause or prevent playback if a video element is not currently visible within the viewport or is hidden using CSS like display:none [8][10][9]. 3. Implementation Differences: - Safari (WebKit): Historically, Safari on iOS and macOS has been more aggressive in pausing or preventing autoplay for elements that are not visible or are outside the viewport to save battery and data [1][8][9]. - Chrome/Blink: Chrome also enforces strict autoplay policies, and while it may not always block the initial network load simply due to display:none, it may opt to pause or throttle playback for hidden elements to optimize performance [10][7]. - Standards: The HTML specification does not mandate that display:none must prevent media from loading or playing, which leads to the variation in implementation [4][11]. To reliably prevent a video from loading or playing when it is not needed (e.g., on mobile devices), the most robust approach is not to rely on CSS, but to use JavaScript to conditionally inject the video element into the DOM or set its src attribute only when needed [6][2][5][3].

Citations:


Avoid mounting the overlay video in both responsive branches

The sm:hidden / hidden sm:* split keeps both DOM copies alive. When a slot is on the overlay card, the hidden copy still contains an autoplaying <video>, which can still fetch and decode media in the background. Render the active overlay once, or gate src/autoplay behind a viewport check so only the visible branch creates the video.

🤖 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/GameModeSelector.ts` around lines 341 - 368, The responsive
branches in the GameModeSelector render duplicate cards, causing overlay videos
to mount and play in hidden copies. Update the rendering around renderCard and
the special/ffa/teams slot branches so an overlay card is created only once for
the active viewport, or ensure hidden branches do not assign video src or
autoplay; preserve the existing desktop and mobile layout behavior.

Comment on lines +598 to +684
<div class="flex-1 min-h-0 flex items-center gap-3 px-3 py-2">
${showReveal
? html`
<h3
class="flex-1 min-w-0 text-base sm:text-xl font-bold uppercase tracking-wider leading-snug"
>
${overlay.displayInfo.title}
</h3>
${overlay.image.url
? html`<img
src="${overlay.image.url}"
alt=""
class="shrink-0 w-14 h-14 sm:w-20 sm:h-20 object-contain"
/>`
: nothing}
`
: nothing}
</div>
<div
class="relative z-10 shrink-0 flex flex-col px-3 py-2 bg-black/55 backdrop-blur-sm rounded-b-2xl"
style="overflow: visible;"
>
${overlay.displayInfo.count !== undefined
? html`<span
class="absolute bottom-full right-2 mb-1 flex items-center gap-1 text-xs font-bold tracking-widest bg-black/70 backdrop-blur-sm px-2 py-0.5 rounded"
>
${overlay.displayInfo.count}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 shrink-0"
viewBox="0 0 512 512"
fill="currentColor"
>
<path
d="M430.337,231.065H81.674c-29.701,0-53.858,24.16-53.858,53.862v49.884v15.976l15.806,2.262c9.135,1.31,16.03,9.258,16.03,18.483c0,9.225-6.891,17.173-16.022,18.482l-15.814,2.262v15.978v49.892c0,29.693,24.157,53.854,53.858,53.854h348.663c29.701,0,53.862-24.161,53.862-53.854v-49.558V391l-17.571-0.822c-9.982-0.463-17.808-8.655-17.808-18.645c0-9.982,7.826-18.174,17.815-18.646l17.564-0.83v-17.58v-49.55C484.199,255.225,460.038,231.065,430.337,231.065z M465.765,334.477c-19.686,0.936-35.371,17.14-35.371,37.056c0,19.923,15.685,36.135,35.371,37.055v49.558c0,19.565-15.864,35.428-35.428,35.428H81.674c-19.569,0-35.432-15.863-35.432-35.428v-49.892c17.991-2.579,31.836-18.011,31.836-36.722c0-18.703-13.846-34.135-31.836-36.721v-49.884c0-19.573,15.863-35.436,35.432-35.436h348.663c19.564,0,35.428,15.863,35.428,35.436V334.477z"
></path>
<rect
x="133.621"
y="439.419"
width="12.19"
height="31.8"
></rect>
<rect
x="133.621"
y="383.564"
width="12.19"
height="31.792"
></rect>
<rect
x="133.621"
y="327.7"
width="12.19"
height="31.8"
></rect>
<rect
x="133.621"
y="271.846"
width="12.19"
height="31.799"
></rect>
<polygon
points="111.245,180.758 100.592,186.68 116.053,214.461 126.702,208.539"
></polygon>
<path
d="M497.524,179.025l-24.095-43.311l-8.558-15.36l-15.749,7.826c-8.948,4.442-19.768,1.09-24.617-7.639c-4.865-8.721-2.001-19.687,6.492-24.95l14.952-9.266l-8.558-15.368l-24.088-43.294C398.863,1.714,366.006-7.658,340.047,6.79L35.374,176.299c-25.955,14.44-35.318,47.305-20.878,73.256l0.875,1.578c3.27-6.394,7.43-12.243,12.324-17.409c-4.803-15.643,1.762-33.044,16.636-41.326l304.681-169.51c17.1-9.518,38.674-3.368,48.192,13.732l24.088,43.302c-16.751,10.38-22.575,32.182-12.895,49.582c9.681,17.401,31.271,23.942,48.925,15.172l24.095,43.312c7.273,13.056,5.337,28.692-3.571,39.601c4.776,3.961,8.989,8.558,12.65,13.569C505.4,224.524,508.979,199.615,497.524,179.025z"
></path>
</svg>
</span>`
: nothing}
<p
class="flex justify-end items-center gap-1 text-xs text-white/70 uppercase tracking-wider truncate"
>
${overlay.displayInfo.subtitle}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3 shrink-0"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd"
></path>
</svg>
</p>
</div>

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Count badge / subtitle aren't gated by showReveal, unlike the title/image block.

The title+image block (599-614) only renders when showReveal is true (fading/card phases), but the sibling div holding the count badge and subtitle (616-684) is unconditional — it renders during entering/video too, undercutting the intended reveal effect described for this feature.

🐛 Suggested fix: gate both blocks together
-          <div class="flex-1 min-h-0 flex items-center gap-3 px-3 py-2">
-            ${showReveal
-              ? html`
-                  <h3 ...>${overlay.displayInfo.title}</h3>
-                  ${overlay.image.url ? html`<img ... />` : nothing}
-                `
-              : nothing}
-          </div>
-          <div class="relative z-10 shrink-0 flex flex-col px-3 py-2 bg-black/55 backdrop-blur-sm rounded-b-2xl" style="overflow: visible;">
-            ${overlay.displayInfo.count !== undefined ? html`...` : nothing}
-            <p>...${overlay.displayInfo.subtitle}...</p>
-          </div>
+          ${showReveal
+            ? html`
+                <div class="flex-1 min-h-0 flex items-center gap-3 px-3 py-2">
+                  <h3 ...>${overlay.displayInfo.title}</h3>
+                  ${overlay.image.url ? html`<img ... />` : nothing}
+                </div>
+                <div class="relative z-10 shrink-0 flex flex-col px-3 py-2 bg-black/55 backdrop-blur-sm rounded-b-2xl" style="overflow: visible;">
+                  ${overlay.displayInfo.count !== undefined ? html`...` : nothing}
+                  <p>...${overlay.displayInfo.subtitle}...</p>
+                </div>
+              `
+            : nothing}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div class="flex-1 min-h-0 flex items-center gap-3 px-3 py-2">
${showReveal
? html`
<h3
class="flex-1 min-w-0 text-base sm:text-xl font-bold uppercase tracking-wider leading-snug"
>
${overlay.displayInfo.title}
</h3>
${overlay.image.url
? html`<img
src="${overlay.image.url}"
alt=""
class="shrink-0 w-14 h-14 sm:w-20 sm:h-20 object-contain"
/>`
: nothing}
`
: nothing}
</div>
<div
class="relative z-10 shrink-0 flex flex-col px-3 py-2 bg-black/55 backdrop-blur-sm rounded-b-2xl"
style="overflow: visible;"
>
${overlay.displayInfo.count !== undefined
? html`<span
class="absolute bottom-full right-2 mb-1 flex items-center gap-1 text-xs font-bold tracking-widest bg-black/70 backdrop-blur-sm px-2 py-0.5 rounded"
>
${overlay.displayInfo.count}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 shrink-0"
viewBox="0 0 512 512"
fill="currentColor"
>
<path
d="M430.337,231.065H81.674c-29.701,0-53.858,24.16-53.858,53.862v49.884v15.976l15.806,2.262c9.135,1.31,16.03,9.258,16.03,18.483c0,9.225-6.891,17.173-16.022,18.482l-15.814,2.262v15.978v49.892c0,29.693,24.157,53.854,53.858,53.854h348.663c29.701,0,53.862-24.161,53.862-53.854v-49.558V391l-17.571-0.822c-9.982-0.463-17.808-8.655-17.808-18.645c0-9.982,7.826-18.174,17.815-18.646l17.564-0.83v-17.58v-49.55C484.199,255.225,460.038,231.065,430.337,231.065z M465.765,334.477c-19.686,0.936-35.371,17.14-35.371,37.056c0,19.923,15.685,36.135,35.371,37.055v49.558c0,19.565-15.864,35.428-35.428,35.428H81.674c-19.569,0-35.432-15.863-35.432-35.428v-49.892c17.991-2.579,31.836-18.011,31.836-36.722c0-18.703-13.846-34.135-31.836-36.721v-49.884c0-19.573,15.863-35.436,35.432-35.436h348.663c19.564,0,35.428,15.863,35.428,35.436V334.477z"
></path>
<rect
x="133.621"
y="439.419"
width="12.19"
height="31.8"
></rect>
<rect
x="133.621"
y="383.564"
width="12.19"
height="31.792"
></rect>
<rect
x="133.621"
y="327.7"
width="12.19"
height="31.8"
></rect>
<rect
x="133.621"
y="271.846"
width="12.19"
height="31.799"
></rect>
<polygon
points="111.245,180.758 100.592,186.68 116.053,214.461 126.702,208.539"
></polygon>
<path
d="M497.524,179.025l-24.095-43.311l-8.558-15.36l-15.749,7.826c-8.948,4.442-19.768,1.09-24.617-7.639c-4.865-8.721-2.001-19.687,6.492-24.95l14.952-9.266l-8.558-15.368l-24.088-43.294C398.863,1.714,366.006-7.658,340.047,6.79L35.374,176.299c-25.955,14.44-35.318,47.305-20.878,73.256l0.875,1.578c3.27-6.394,7.43-12.243,12.324-17.409c-4.803-15.643,1.762-33.044,16.636-41.326l304.681-169.51c17.1-9.518,38.674-3.368,48.192,13.732l24.088,43.302c-16.751,10.38-22.575,32.182-12.895,49.582c9.681,17.401,31.271,23.942,48.925,15.172l24.095,43.312c7.273,13.056,5.337,28.692-3.571,39.601c4.776,3.961,8.989,8.558,12.65,13.569C505.4,224.524,508.979,199.615,497.524,179.025z"
></path>
</svg>
</span>`
: nothing}
<p
class="flex justify-end items-center gap-1 text-xs text-white/70 uppercase tracking-wider truncate"
>
${overlay.displayInfo.subtitle}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3 shrink-0"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd"
></path>
</svg>
</p>
</div>
${showReveal
? html`
<div class="flex-1 min-h-0 flex items-center gap-3 px-3 py-2">
<h3
class="flex-1 min-w-0 text-base sm:text-xl font-bold uppercase tracking-wider leading-snug"
>
${overlay.displayInfo.title}
</h3>
${overlay.image.url
? html`<img
src="${overlay.image.url}"
alt=""
class="shrink-0 w-14 h-14 sm:w-20 sm:h-20 object-contain"
/>`
: nothing}
</div>
<div
class="relative z-10 shrink-0 flex flex-col px-3 py-2 bg-black/55 backdrop-blur-sm rounded-b-2xl"
style="overflow: visible;"
>
${overlay.displayInfo.count !== undefined
? html`<span
class="absolute bottom-full right-2 mb-1 flex items-center gap-1 text-xs font-bold tracking-widest bg-black/70 backdrop-blur-sm px-2 py-0.5 rounded"
>
${overlay.displayInfo.count}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 shrink-0"
viewBox="0 0 512 512"
fill="currentColor"
>
<path
d="M430.337,231.065H81.674c-29.701,0-53.858,24.16-53.858,53.862v49.884v15.976l15.806,2.262c9.135,1.31,16.03,9.258,16.03,18.483c0,9.225-6.891,17.173-16.022,18.482l-15.814,2.262v15.978v49.892c0,29.693,24.157,53.854,53.858,53.854h348.663c29.701,0,53.862-24.161,53.862-53.854v-49.558V391l-17.571-0.822c-9.982-0.463-17.808-8.655-17.808-18.645c0-9.982,7.826-18.174,17.815-18.646l17.564-0.83v-17.58v-49.55C484.199,255.225,460.038,231.065,430.337,231.065z M465.765,334.477c-19.686,0.936-35.371,17.14-35.371,37.056c0,19.923,15.685,36.135,35.371,37.055v49.558c0,19.565-15.864,35.428-35.428,35.428H81.674c-19.569,0-35.432-15.863-35.432-35.428v-49.892c17.991-2.579,31.836-18.011,31.836-36.722c0-18.703-13.846-34.135-31.836-36.721v-49.884c0-19.573,15.863-35.436,35.432-35.436h348.663c19.564,0,35.428,15.863,35.428,35.436V334.477z"
></path>
<rect
x="133.621"
y="439.419"
width="12.19"
height="31.8"
></rect>
<rect
x="133.621"
y="383.564"
width="12.19"
height="31.792"
></rect>
<rect
x="133.621"
y="327.7"
width="12.19"
height="31.8"
></rect>
<rect
x="133.621"
y="271.846"
width="12.19"
height="31.799"
></rect>
<polygon
points="111.245,180.758 100.592,186.68 116.053,214.461 126.702,208.539"
></polygon>
<path
d="M497.524,179.025l-24.095-43.311l-8.558-15.36l-15.749,7.826c-8.948,4.442-19.768,1.09-24.617-7.639c-4.865-8.721-2.001-19.687,6.492-24.95l14.952-9.266l-8.558-15.368l-24.088-43.294C398.863,1.714,366.006-7.658,340.047,6.79L35.374,176.299c-25.955,14.44-35.318,47.305-20.878,73.256l0.875,1.578c3.27-6.394,7.43-12.243,12.324-17.409c-4.803-15.643,1.762-33.044,16.636-41.326l304.681-169.51c17.1-9.518,38.674-3.368,48.192,13.732l24.088,43.302c-16.751,10.38-22.575,32.182-12.895,49.582c9.681,17.401,31.271,23.942,48.925,15.172l24.095,43.312c7.273,13.056,5.337,28.692-3.571,39.601c4.776,3.961,8.989,8.558,12.65,13.569C505.4,224.524,508.979,199.615,497.524,179.025z"
></path>
</svg>
</span>`
: nothing}
<p
class="flex justify-end items-center gap-1 text-xs text-white/70 uppercase tracking-wider truncate"
>
${overlay.displayInfo.subtitle}
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-3 w-3 shrink-0"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
clip-rule="evenodd"
></path>
</svg>
</p>
</div>
`
: nothing}
🤖 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/GameModeSelector.ts` around lines 598 - 684, Update the overlay
markup in GameModeSelector so the sibling container holding the count badge and
subtitle is rendered only when showReveal is true, matching the existing
title/image block. Keep the count and subtitle content unchanged and ensure
neither reveal block appears during entering or video phases.

Comment thread src/core/ApiSchemas.ts
Comment on lines +352 to +372

export const LobbyCardOverlaySchema = z.object({
slot: z.number(),
interval: z.number(),
offset: z.number().default(0),
ttl: z.number(),
video: z.object({
url: z.string(),
videoLength: z.number(),
}),
image: z.object({
url: z.string(),
}),
displayInfo: z.object({
title: z.string(),
subtitle: z.string(),
count: z.number().optional(),
}),
linkTo: z.url(),
});
export type LobbyCardOverlay = z.infer<typeof LobbyCardOverlaySchema>;

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add tests for the new schema.

No tests accompany this new LobbyCardOverlaySchema/LobbyCardOverlay export.

As per coding guidelines, "All changes to src/core/ must include tests." A simple safeParse test asserting valid/invalid payloads (missing fields, wrong types) would satisfy this.

🤖 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/core/ApiSchemas.ts` around lines 352 - 372, Add tests for
LobbyCardOverlaySchema using safeParse, covering a valid payload and invalid
payloads with missing required fields and incorrect types. Ensure the tests also
verify the offset default behavior and exercise the exported LobbyCardOverlay
schema contract.

Source: Coding guidelines

Comment thread src/core/ApiSchemas.ts
Comment on lines +353 to +371
export const LobbyCardOverlaySchema = z.object({
slot: z.number(),
interval: z.number(),
offset: z.number().default(0),
ttl: z.number(),
video: z.object({
url: z.string(),
videoLength: z.number(),
}),
image: z.object({
url: z.string(),
}),
displayInfo: z.object({
title: z.string(),
subtitle: z.string(),
count: z.number().optional(),
}),
linkTo: z.url(),
});

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.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== package metadata ==\n'
git ls-files package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null || true
for f in package.json pnpm-lock.yaml yarn.lock package-lock.json; do
  if [ -f "$f" ]; then
    printf '\n-- %s --\n' "$f"
    sed -n '1,220p' "$f"
  fi
done

printf '\n== locate relevant files ==\n'
git ls-files 'src/core/ApiSchemas.ts' 'src/client/GameModeSelector.ts'

printf '\n== source excerpts ==\n'
sed -n '330,390p' src/core/ApiSchemas.ts
printf '\n---\n'
sed -n '560,620p' src/client/GameModeSelector.ts

printf '\n== zod url semantics probe ==\n'
node - <<'JS'
let zod;
try {
  zod = require('zod');
} catch (e) {
  console.log('require-zod-failed:', e.message);
  process.exit(0);
}
const schema = zod.z ? zod.z.url() : zod.url();
const samples = [
  'https://example.com',
  'http://example.com',
  'mailto:test@example.com',
  'javascript:alert(1)',
  'data:text/plain,hi',
  '//example.com',
  '/relative/path',
];
for (const s of samples) {
  const res = schema.safeParse(s);
  console.log(JSON.stringify({ input: s, success: res.success, error: res.success ? null : res.error.issues.map(i => i.message) }));
}
JS

Repository: openfrontio/OpenFrontIO

Length of output: 17072


Restrict linkTo to http(s) z.url() accepts javascript: and data: URLs, and GameModeSelector.ts renders this value directly into <a href target="_blank">. Narrow the schema here so a bad overlay cannot become an unsafe clickable link.

📍 Affects 2 files
  • src/core/ApiSchemas.ts#L353-L371 (this comment)
  • src/client/GameModeSelector.ts#L592-L597
🤖 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/core/ApiSchemas.ts` around lines 353 - 371, Restrict the linkTo field in
LobbyCardOverlaySchema to URLs using only the http or https protocols, replacing
the unrestricted z.url() validation. Update src/core/ApiSchemas.ts lines
353-371; src/client/GameModeSelector.ts lines 592-597 requires no direct change
because the schema validation provides the protection.

@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management Jul 11, 2026
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.

Promo card overlay on game selector

2 participants