Skip to content

Discord(et al.) embedded URLs#2740

Merged
evanpelle merged 4 commits intomainfrom
discordembedded
Jan 14, 2026
Merged

Discord(et al.) embedded URLs#2740
evanpelle merged 4 commits intomainfrom
discordembedded

Conversation

@ryanbarlow97
Copy link
Copy Markdown
Contributor

@ryanbarlow97 ryanbarlow97 commented Dec 30, 2025

Description:

Changes URL embeds within other platforms, e.g. Discord, WhatsApp & X.

Updates game URLs to /game/<code> instead of /#join=<code> (required for embedded URLs). An added benefit of this is that you would be able to change a url from openfront.io/game/RQDUy8nP?replay to api.openfront.io/game/RQDUy8nP?replay (add api. In front) and be in the right place for the API data.

Updates URLs when joining/leaving private lobbies

Appends a random string to the end of the URL when inside a private lobby and options change - this is to force discord to update the embedded details.

Updates URL in different game states to ?lobby / ?live and ?replay. These do nothing other than being used as a cache-busting solution.


Lobby Info

Discord:
image

WhatsApp:
image

x.com:
image


Game Win Details

Discord:
image

WhatsApp:
image

x.com
image

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
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

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

w.o.n

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 30, 2025

Warning

Rate limit exceeded

@ryanbarlow97 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 24 minutes and 2 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 462b45e and 1222e7c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • nginx.conf
  • package.json
  • src/client/AccountModal.ts
  • src/client/HostLobbyModal.ts
  • src/client/JoinPrivateLobbyModal.ts
  • src/client/Main.ts
  • src/client/components/baseComponents/Modal.ts
  • src/client/graphics/layers/WinModal.ts
  • src/core/Schemas.ts
  • src/server/GamePreviewBuilder.ts
  • src/server/GamePreviewRoute.ts
  • src/server/Master.ts
  • src/server/RenderHtml.ts
  • src/server/Worker.ts

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds a game-preview endpoint and builder, centralizes game ID validation, shifts client join URLs from hash to path with history updates, refactors modal URL/leave behavior, and adjusts server static/mime/caching and nginx worker routing.

Changes

Cohort / File(s) Summary
Core Schemas
src/core/Schemas.ts
Add GAME_ID_REGEX, isValidGameID(), ClientInfoSchema, GameInfoSchema, and ClientInfo type; replace prior inline ID validation with the named regex.
Game preview feature
src/server/GamePreviewBuilder.ts, src/server/GamePreviewRoute.ts
New preview builder and registerGamePreviewRoute() that validate IDs, fetch lobby/public info, build OG/Twitter meta, and inject into base HTML (JSON fallback). Review HTML injection, origin/workerPath logic, and remote fetch/error handling.
Server static & worker routing
src/server/Master.ts, src/server/Worker.ts, nginx.conf
Add Cache-Control header rules by asset type, enforce webp MIME handling, add /maps static route, and move nginx routing to worker-based /wN logic. Verify caching, MIME, and route calculations.
Client URL & lobby flows
src/client/Main.ts, src/client/AccountModal.ts, src/client/HostLobbyModal.ts, src/client/JoinPrivateLobbyModal.ts, src/client/AccountModal.ts, src/client/graphics/layers/WinModal.ts
Switch join/share URLs from hash to path, introduce join-changed event, use GAME_ID_REGEX, add lobbyUrlSuffix & leaveLobbyOnClose, update history.push/replace and clipboard URL construction, and ensure modal open/close leaves behavior.
Modal binding fix
src/client/components/baseComponents/Modal.ts
Wrap click handlers in arrow functions to preserve this binding for Lit event listeners.
Dependency
package.json
Add node-html-parser@^7.0.2. Confirm license and use sites.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Client
    participant Route as GamePreviewRoute
    participant GM as GameManager
    participant Central as RemoteAPI
    participant File as BaseHTML

    Browser->>Route: GET /game/:id
    Route->>Route: validate id (GAME_ID_REGEX)
    Route->>GM: lookup lobby by id
    alt lobby found
        GM-->>Route: lobby data
    else
        Route->>Central: fetch public game info
        Central-->>Route: public info / 404
    end
    Route->>Route: buildPreview(lobby, publicInfo)
    Route->>File: read base HTML template
    Route->>Route: inject OG/Twitter meta into HTML
    Route-->>Browser: HTML response with preview meta
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Bug Fix

Suggested reviewers

  • evanpelle
  • scottanderson

Poem

🎮 A tiny preview wakes on the web,
Paths over hashes, the old ways shed,
IDs kept tidy, previews take flight,
Cache and MIME tuned for pixel-perfect light,
Share the lobby, invite the night ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Title directly relates to the main change: converting game URLs from hash-based to path-based format for embedding on Discord and other platforms.
Description check ✅ Passed Description thoroughly explains the URL format changes, cache-busting strategy, platform-specific examples, and testing completion; directly related to the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/client/JoinPrivateLobbyModal.ts (1)

139-161: Nice refactor using URL API for robust parsing.

The structured URL parsing handles multiple URL shapes gracefully. The try-catch ensures malformed URLs don't crash the flow.

One small observation: the regex /join\/([A-Za-z0-9]{8})/ at line 153 would also match longer strings like /join/ABCD1234extra. Consider anchoring it with $ or word boundary if you want stricter matching:

-      const match = url.pathname.match(/join\/([A-Za-z0-9]{8})/);
+      const match = url.pathname.match(/\/join\/([A-Za-z0-9]{8})$/);

That said, since callers likely validate with ID.safeParse() anyway, this is a minor nitpick.

src/server/Master.ts (1)

173-179: Consider escaping URLs in HTML attributes.

The meta.redirectUrl and joinId are inserted into the content attribute and script without escaping. While redirectUrl is built from origin (which comes from request headers), a malformed host header could potentially break the HTML or introduce injection.

Since joinId is already validated by ID.safeParse(), it's safe. For redirectUrl, you might want to validate or escape the origin:

+const escapeAttr = (value: string): string =>
+  value.replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");

 const refreshTag = botRequest
   ? ""
-  : `<meta http-equiv="refresh" content="0; url=${meta.redirectUrl}#join=${joinId}">`;
+  : `<meta http-equiv="refresh" content="0; url=${escapeAttr(meta.redirectUrl)}#join=${joinId}">`;
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f3d9df and 9b8c9ea.

📒 Files selected for processing (5)
  • src/client/AccountModal.ts
  • src/client/HostLobbyModal.ts
  • src/client/JoinPrivateLobbyModal.ts
  • src/client/Main.ts
  • src/server/Master.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/AccountModal.ts
  • src/client/JoinPrivateLobbyModal.ts
  • src/server/Master.ts
  • src/client/Main.ts
  • src/client/HostLobbyModal.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.

Applied to files:

  • src/client/HostLobbyModal.ts
🧬 Code graph analysis (2)
src/server/Master.ts (2)
src/core/Schemas.ts (2)
  • GameInfo (130-136)
  • ID (208-211)
src/core/configuration/DefaultConfig.ts (1)
  • workerPort (207-209)
src/client/Main.ts (1)
src/core/Schemas.ts (1)
  • ID (208-211)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (11)
src/client/HostLobbyModal.ts (1)

831-834: LGTM!

The clipboard URL now uses the path-based format /join/{lobbyId}, which is consistent with the PR's goal of transitioning to path-based routing. Since lobbyId is an 8-character alphanumeric string (per the ID schema), no URL encoding is needed for the path segment.

src/server/Master.ts (5)

52-57: Verify proxy trust configuration aligns with deployment.

requestOrigin reads x-forwarded-proto directly from headers. While app.set("trust proxy", 3) is configured at line 282, ensure your deployment actually has exactly 3 trusted proxy hops. Misconfiguration could allow clients to spoof the protocol/host.


126-127: Redirect URL uses query param format.

The redirectUrl is built as ${origin}/?join=${gameID}, using query param style. The rest of the PR moves to path-based /join/{id} format. Is this intentional for backwards compatibility with the client's handleUrl which checks query params first?

If intentional, a brief comment explaining the choice would help future readers.


59-78: Good use of AbortController for fetch timeout.

The 1.5 second timeout prevents the preview page from hanging if the worker is unresponsive. Clean pattern with proper cleanup via clearTimeout.


220-245: Preview serves 404 status when lobby not found, but still returns HTML.

Returning a 404 status with a preview page is unconventional. Bots might not index the page, but users still see a functional redirect. If that's the intent (graceful degradation), consider adding a comment. Otherwise, you might want to return a different response for non-existent lobbies.


247-252: Route handler looks good.

The /join/:gameId route delegates to serveJoinPreview and properly catches errors with a 500 response. Clean error logging.

src/client/AccountModal.ts (1)

145-152: LGTM!

The navigation now uses path-based routing with encodeURIComponent for safety. The hash fragment is kept for the client-side join handler. Dispatching hashchange ensures Main.ts processes the navigation.

src/client/Main.ts (4)

612-634: Clean extraction logic with proper validation.

The function checks multiple URL sources (query, path, hash) in a sensible priority order. Using ID.safeParse from zod ensures the extracted value matches the expected 8-character alphanumeric format. Good defensive programming.


636-643: Good URL normalization helper.

updateJoinUrlForShare ensures the URL is in the canonical format before sharing. Using replaceState avoids polluting browser history with duplicate entries.


479-485: LGTM on the refactored join handling.

The flow now extracts the join code, updates the URL to canonical format, opens the modal, and returns early. Clear and readable.


603-607: URL format now matches the canonical path-based pattern.

The pushState call uses /join/${gameID}#join=${gameID}, aligning with the rest of the PR. The hash fragment provides backwards compatibility for client-side routing.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Dec 30, 2025
Comment thread src/server/Master.ts Fixed
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (1)
src/server/Master.ts (1)

284-289: Add rate limiting to prevent abuse.

This route performs file system access but is registered before the rate limit middleware (lines 320-325), making it vulnerable to abuse. Move this route after the rate limiting setup.

🔎 Proposed fix

Move the route registration to after the rate limit middleware:

 app.use(express.json());
-
-app.get("/join/:gameId", (req, res) => {
-  serveJoinPreview(req, res, req.params.gameId).catch((error) => {
-    log.error("failed to render join preview", { error });
-    res.status(500).send("Unable to render lobby preview");
-  });
-});
-
 app.use(
   express.static(path.join(__dirname, "../../static"), {
     maxAge: "1y", // Set max-age to 1 year for all static assets

Then add after line 325:

     max: 20, // 20 requests per IP per second
   }),
 );
+
+app.get("/join/:gameId", (req, res) => {
+  serveJoinPreview(req, res, req.params.gameId).catch((error) => {
+    log.error("failed to render join preview", { error });
+    res.status(500).send("Unable to render lobby preview");
+  });
+});
🧹 Nitpick comments (1)
src/client/Main.ts (1)

608-623: Consider avoiding hardcoded length in the regex.

The regex /^\/join\/([A-Za-z0-9]{8})/ hardcodes the 8-character length. If the ID schema's length changes in the future, this regex could become inconsistent. Since ID.safeParse() validates afterward, this provides a safety net, but the regex could become misleading.

🔎 Option: Make the regex more flexible
  private extractJoinCodeFromUrl(): string | null {
    const searchParams = new URLSearchParams(window.location.search);
    const joinFromQuery = searchParams.get("join");
    if (joinFromQuery && ID.safeParse(joinFromQuery).success) {
      return joinFromQuery;
    }

    const pathMatch = window.location.pathname.match(
-      /^\/join\/([A-Za-z0-9]{8})/,
+      /^\/join\/([A-Za-z0-9]+)/,
    );
    if (pathMatch && ID.safeParse(pathMatch[1]).success) {
      return pathMatch[1];
    }

    return null;
  }

This makes the regex more resilient to schema changes while still relying on ID.safeParse() for validation.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9b8c9ea and 5cb980a.

📒 Files selected for processing (3)
  • src/client/AccountModal.ts
  • src/client/Main.ts
  • src/server/Master.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/AccountModal.ts
  • src/server/Master.ts
  • src/client/Main.ts
🧬 Code graph analysis (2)
src/server/Master.ts (2)
src/core/Schemas.ts (2)
  • GameInfo (130-136)
  • ID (208-211)
src/core/configuration/DefaultConfig.ts (1)
  • workerPort (207-209)
src/client/Main.ts (1)
src/core/Schemas.ts (1)
  • ID (208-211)
🪛 GitHub Check: 🔍 ESLint
src/server/Master.ts

[failure] 191-191:
Prefer using nullish coalescing operator (??) instead of a logical or (||), as it is a safer operator.

🪛 GitHub Check: CodeQL
src/server/Master.ts

[failure] 284-289: Missing rate limiting
This route handler performs a file system access, but is not rate-limited.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (11)
src/client/Main.ts (3)

479-485: LGTM! Clean URL extraction and early return.

The flow correctly extracts the lobby ID, normalizes the URL for sharing, opens the modal, and returns early to prevent further processing. The logic is clear and handles both query parameter and path-based joins.


625-632: LGTM! Proper URL normalization without history pollution.

The method correctly uses replaceState to normalize the URL to /join/{lobbyId} format without adding unnecessary entries to the browser history. The guard condition prevents redundant operations.


603-603: The server is already configured correctly for path-based URLs. The /join/:gameId route is defined at src/server/Master.ts line 284, and a catch-all SPA fallback route at line 532 serves index.html for all unmatched paths. This means direct navigation and page refresh to /join/* paths will work as expected.

src/client/AccountModal.ts (1)

147-148: Clean URL construction looks good.

The URL is properly constructed using encodeURIComponent for the path segment, creating a clean /join/{gameId} format that aligns with the PR's path-based URL standardization.

src/server/Master.ts (7)

39-45: LGTM!

The HTML escaping correctly handles the five key characters needed to prevent XSS in HTML contexts.


47-50: LGTM!

Case-insensitive substring matching with proper null handling is the right approach for bot detection.


52-57: LGTM!

The origin resolution correctly handles x-forwarded-proto with comma-separated values and provides sensible fallbacks for reverse proxy scenarios.


80-97: LGTM!

The type definition appropriately uses optional fields for an external API response where the shape may vary.


117-123: LGTM!

The type definition clearly captures the preview metadata structure.


199-249: LGTM!

The HTML rendering correctly escapes user content, conditionally redirects humans vs bots, and follows Open Graph/Twitter Card specifications.


251-282: LGTM!

The function correctly validates input, fetches data in parallel, and branches appropriately for bot vs human requests with proper cache headers.

Comment thread src/client/AccountModal.ts Outdated
Comment thread src/server/Master.ts Outdated
Comment thread src/server/Master.ts Outdated
Comment thread src/server/Master.ts Outdated
Comment thread src/server/Master.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5cb980a and f7328c8.

📒 Files selected for processing (3)
  • src/client/AccountModal.ts
  • src/client/Main.ts
  • src/server/Master.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/AccountModal.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/server/Master.ts
  • src/client/Main.ts
🧬 Code graph analysis (2)
src/server/Master.ts (4)
src/core/configuration/ConfigLoader.ts (1)
  • getServerConfigFromServer (47-50)
src/core/Schemas.ts (2)
  • GameInfo (130-136)
  • ID (208-211)
src/core/configuration/DefaultConfig.ts (1)
  • workerPort (207-209)
tests/util/TestServerConfig.ts (1)
  • workerPort (67-69)
src/client/Main.ts (1)
src/core/Schemas.ts (1)
  • ID (208-211)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (19)
src/client/Main.ts (5)

379-379: LGTM!

The custom event listener allows programmatic navigation to trigger the URL handler, consistent with the existing popstate and hashchange listeners.


480-486: LGTM!

The join code extraction and URL normalization flow is clean, with an early return that correctly prevents further processing after handling the join link.


604-604: LGTM!

The path-based URL is consistent with the new server route and the overall join link strategy.


609-624: LGTM!

The extraction logic correctly prioritizes query parameters over path patterns and uses schema validation for both sources.


626-633: LGTM!

The URL normalization correctly uses replaceState to avoid polluting history and includes a conditional check to prevent unnecessary updates.

src/server/Master.ts (14)

47-58: LGTM!

The bot list appropriately covers social media and messaging platform crawlers that consume Open Graph tags for link previews.


60-66: LGTM!

The HTML escape utility correctly handles the five essential characters and processes & first to prevent double-escaping.


68-71: LGTM!

The bot detection logic is simple and effective, with case-insensitive matching and a safe default for missing user-agents.


73-78: LGTM!

The origin extraction correctly handles proxy scenarios with x-forwarded-proto and provides sensible fallbacks for both protocol and host.


80-100: LGTM!

The lobby info fetch correctly uses try/finally to prevent timeout resource leaks and includes appropriate error handling.


102-119: LGTM!

The type definition appropriately reflects the external API shape with optional fields for defensive parsing.


121-138: LGTM!

The public game info fetch correctly uses try/finally to prevent timeout resource leaks and includes appropriate error handling.


140-146: LGTM!

The preview metadata type is straightforward with clear, descriptive field names.


148-221: LGTM!

The preview builder correctly prioritizes live lobby data over archived public data, uses nullish coalescing consistently, and handles all game states (finished, active, unknown).


223-273: LGTM!

The HTML renderer correctly differentiates bot vs. human behavior, includes comprehensive Open Graph and Twitter Card tags, and uses HTML escaping to prevent XSS.


275-306: LGTM!

The preview handler correctly validates IDs, fetches data in parallel, and differentiates between bot (static preview) and human (SPA) responses with appropriate cache headers.


308-313: LGTM!

The route handler correctly applies rate limiting middleware and includes appropriate error handling with logging.


347-347: Verify the rate limit increase is intentional and appropriate.

The global rate limit has been increased to 1000 requests per IP per second. Ensure this aligns with your system's capacity and DDoS protection strategy, as this is a significant threshold.


356-438: LGTM!

The startMaster function is now part of the public API. The implementation correctly handles worker lifecycle, scheduling coordination, and server startup.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Dec 30, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f7328c8 and 580a809.

📒 Files selected for processing (2)
  • src/server/GamePreviewBuilder.ts
  • src/server/Master.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/server/Master.ts
🧬 Code graph analysis (2)
src/server/GamePreviewBuilder.ts (4)
src/core/game/GameView.ts (1)
  • gameID (910-912)
src/core/Schemas.ts (1)
  • GameInfo (130-136)
src/core/game/GameImpl.ts (1)
  • map (199-201)
src/core/configuration/DefaultConfig.ts (1)
  • bots (304-306)
src/server/Master.ts (4)
src/core/configuration/ConfigLoader.ts (1)
  • getServerConfigFromServer (47-50)
src/core/Schemas.ts (2)
  • GameInfo (130-136)
  • ID (208-211)
src/core/configuration/DefaultConfig.ts (1)
  • workerPort (207-209)
src/server/GamePreviewBuilder.ts (2)
  • ExternalGameInfo (202-202)
  • GamePreviewBuilder (37-200)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (10)
src/server/Master.ts (4)

67-72: LGTM!

Good use of x-forwarded-proto with fallback chain. The trust proxy setting (line 189) limits spoofing risk.


74-114: LGTM!

Timeout handling is correct with finally block ensuring cleanup. Both fetch functions follow the same clean pattern.


116-152: LGTM!

Clean separation of bot vs. human flows. Input validation with ID.safeParse prevents invalid game IDs from reaching downstream logic. Parallel fetches are efficient.


191-195: Verify the 50x rate limit increase is intentional.

The global rate limit jumped from 20 to 1000 requests per IP per second. While the commit message mentions increasing the rate limit, this is a significant change that could expose the server to abuse.

Consider if a lower value (e.g., 100-200) would suffice, or add a comment explaining why 1000 is needed.

src/server/GamePreviewBuilder.ts (6)

3-35: LGTM!

Types are clean and use optional fields appropriately. Using unknown for stats is a safe choice when the shape isn't needed.


38-47: LGTM!

Duration formatting logic is correct and handles undefined gracefully.


49-73: LGTM!

Winner parsing handles both team and player formats cleanly. The fallback to join(" ") is a reasonable catch-all.


79-86: LGTM!

Standard HTML entity escaping covering all necessary characters.


88-151: LGTM!

Build logic correctly handles finished games vs. active lobbies. Good use of encodeURIComponent for the map name in the image URL.


202-202: LGTM!

Using export type for type-only exports is clean TypeScript practice.

Comment thread src/server/GamePreviewBuilder.ts Outdated
Comment thread src/server/Master.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/server/Master.ts (1)

309-326: Timeout not cleared in fetchLobbies.

Unlike fetchLobbyInfo and fetchPublicGameInfo which use try/finally to clear timeouts, this function never clears the timeout. While not critical (aborting a completed fetch is harmless), it leaks timers and is inconsistent with the pattern used elsewhere.

🔎 Proposed fix
   for (const gameID of new Set(publicLobbyIDs)) {
     const controller = new AbortController();
-    setTimeout(() => controller.abort(), 5000); // 5 second timeout
+    const timeout = setTimeout(() => controller.abort(), 5000);
     const port = config.workerPort(gameID);
     const promise = fetch(`http://localhost:${port}/api/game/${gameID}`, {
       headers: { [config.adminHeader()]: config.adminToken() },
       signal: controller.signal,
     })
       .then((resp) => resp.json())
       .then((json) => {
         return json as GameInfo;
       })
       .catch((error) => {
         log.error(`Error fetching game ${gameID}:`, error);
-        // Return null or a placeholder if fetch fails
         publicLobbyIDs.delete(gameID);
         return null;
+      })
+      .finally(() => {
+        clearTimeout(timeout);
       });
♻️ Duplicate comments (1)
src/server/Master.ts (1)

18-36: Duplicate bot list still present.

The bot user-agent list is defined twice: inline in joinPreviewLimiter.skip (lines 23-34) and again in BOT_USER_AGENTS (lines 49-60). Move BOT_USER_AGENTS above the limiter and reuse it.

🔎 Proposed fix
+const BOT_USER_AGENTS = [
+  "discordbot",
+  "twitterbot",
+  "slackbot",
+  "facebookexternalhit",
+  "linkedinbot",
+  "telegrambot",
+  "applebot",
+  "snapchat",
+  "whatsapp",
+  "pinterestbot",
+];
+
 const joinPreviewLimiter = rateLimit({
   windowMs: 1000, // 1 second
   max: 100, // limit each IP to 100 requests per windowMs
   skip: (req) => {
     const ua = req.get("user-agent")?.toLowerCase() ?? "";
-    return [
-      "discordbot",
-      "twitterbot",
-      "slackbot",
-      "facebookexternalhit",
-      "linkedinbot",
-      "telegrambot",
-      "applebot",
-      "snapchat",
-      "whatsapp",
-      "pinterestbot",
-    ].some((bot) => ua.includes(bot));
+    return BOT_USER_AGENTS.some((bot) => ua.includes(bot));
   },
 });
-
-const BOT_USER_AGENTS = [
-  "discordbot",
-  "twitterbot",
-  "slackbot",
-  "facebookexternalhit",
-  "linkedinbot",
-  "telegrambot",
-  "applebot",
-  "snapchat",
-  "whatsapp",
-  "pinterestbot",
-];

Also applies to: 49-60

🧹 Nitpick comments (1)
src/server/Master.ts (1)

154-159: Consider using an async wrapper or express-async-errors.

The pattern works but mixing callback-style with .catch() on async function is less clean than using an async wrapper or middleware like express-async-errors. This is optional since the current approach handles errors correctly.

🔎 Alternative pattern
-app.get("/join/:gameId", joinPreviewLimiter, (req, res) => {
-  serveJoinPreview(req, res, req.params.gameId).catch((error) => {
-    log.error("failed to render join preview", { error });
-    res.status(500).send("Unable to render lobby preview");
-  });
-});
+app.get("/join/:gameId", joinPreviewLimiter, async (req, res) => {
+  try {
+    await serveJoinPreview(req, res, req.params.gameId);
+  } catch (error) {
+    log.error("failed to render join preview", { error });
+    res.status(500).send("Unable to render lobby preview");
+  }
+});
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 580a809 and 29e520e.

📒 Files selected for processing (2)
  • src/server/GamePreviewBuilder.ts
  • src/server/Master.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/server/GamePreviewBuilder.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/server/Master.ts
🧬 Code graph analysis (1)
src/server/Master.ts (3)
src/core/Schemas.ts (2)
  • GameInfo (130-136)
  • ID (208-211)
src/core/configuration/DefaultConfig.ts (1)
  • workerPort (207-209)
src/server/GamePreviewBuilder.ts (2)
  • ExternalGameInfo (203-203)
  • GamePreviewBuilder (37-201)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (7)
src/server/Master.ts (7)

62-65: LGTM!

Clean helper function with proper nullish coalescing.


67-72: LGTM!

Good handling of X-Forwarded-Proto with proper fallback chain. The comma split correctly handles multiple proxies.


74-94: LGTM!

The try/finally pattern correctly clears the timeout regardless of success or failure. Clean error handling.


96-114: LGTM!

Consistent pattern with fetchLobbyInfo. The API_DOMAIN environment variable provides good flexibility for different deployment environments.


116-152: LGTM!

Good structure:

  • Zod validation with early return on invalid ID
  • Parallel fetching with Promise.all is efficient
  • Clean separation between bot and human responses
  • no-store cache header is correct for dynamic previews

161-166: LGTM!

Consistent with the caching strategy for other static assets.


197-202: Verify the rate limit increase is intentional.

The global rate limit was increased from 20 to 1000 requests per second per IP. This is a 50x increase and quite permissive. Please confirm this is intentional and won't expose the server to abuse.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Dec 30, 2025
@evanpelle
Copy link
Copy Markdown
Collaborator

looks like it has merge conflicts from the new ui update

@ryanbarlow97
Copy link
Copy Markdown
Contributor Author

looks like it has merge conflicts from the new ui update

Yeah 😭 will fix today

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (8)
src/client/styles/components/setting.css (1)

265-267: Don’t remove focus outline without adding a replacement focus style.

Keyboard users will lose focus visibility on sliders.

Proposed focus-visible styling
 .setting-input.slider:focus {
   outline: none;
 }
+
+.setting-input.slider:focus-visible {
+  box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.9);
+}
src/client/styles/components/modal.css (1)

7-13: Remove redundant modal button styling rule.

The o-modal o-button descendant selector duplicates styling already defined in button.css (both apply display: block and margin: 0 auto at the 1024px breakpoint). Since o-button elements inside o-modal inherit button.css styles, this global rule is redundant.

Delete lines 7-13 entirely, or verify in component tests that button centering still works as expected without it.

src/core/execution/TransportShipExecution.ts (1)

257-302: Avoid dstShore! assertions in tick; make the invariant explicit with a guard.
Today it “should be set”, but a future change could make dstShore null (or unset when dst is set) and this will crash hard in the hottest path.

Proposed guard
   tick(ticks: number) {
     if (this.dst === null) {
       this.active = false;
       return;
     }
+    if (this.dstShore === null) {
+      this.active = false;
+      return;
+    }
     if (!this.active) {
       return;
     }
src/client/GameStartingModal.ts (1)

1-12: Extend GameStartingModal from BaseModal to align with modal patterns in this codebase.

All other modals extend BaseModal, which provides Escape key handling and consistent modal lifecycle management. GameStartingModal uses isVisible and manual show()/hide() methods instead. Extend BaseModal to gain automatic Escape key handling, listener cleanup, and consistent naming (isModalOpen).

src/client/components/baseComponents/setting/SettingToggle.ts (1)

16-26: Fix duplicate "change" events (native input + custom event share the same name).

Right now the input’s native change will bubble, and handleChange() emits another bubbling "change"—listeners can see two events and/or the “wrong” event shape.

Proposed fix (keep public event name, suppress native bubble)
   private handleChange(e: Event) {
-    const input = e.target as HTMLInputElement;
+    // Prevent the native input "change" event from bubbling out and causing duplicates.
+    e.stopPropagation();
+    const input = e.currentTarget as HTMLInputElement;
     this.checked = input.checked;
     this.dispatchEvent(
       new CustomEvent("change", {
         detail: { checked: this.checked },
         bubbles: true,
         composed: true,
       }),
     );
   }

Optional: add a visible keyboard focus state (e.g., peer-focus-visible:ring-* on the <span>) since the checkbox is visually hidden.

Also applies to: 28-61

tests/client/components/FluentSlider.test.ts (1)

55-79: Avoid null-deref in tests: assert the range input exists everywhere you use it.

Several tests do const rangeInput = slider.querySelector(...) as HTMLInputElement; and then immediately write rangeInput.valueAsNumber = ... without a guard—if the selector breaks, you’ll get a runtime error instead of a clean test failure.

Example pattern to apply to each test
-      const rangeInput = slider.querySelector(
+      const rangeInput = slider.querySelector(
         'input[type="range"]',
       ) as HTMLInputElement;
+      expect(rangeInput).toBeTruthy();

Also applies to: 83-124, 126-176, 222-248, 251-279

src/client/components/baseComponents/setting/SettingNumber.ts (1)

13-15: Shadow DOM disabled - potential ID conflicts ahead.

Returning this from createRenderRoot() disables shadow DOM encapsulation. This means multiple <setting-number> instances on the same page will have duplicate id="setting-number-input" (lines 43, 52), which breaks HTML semantics and accessibility.

🔧 Use dynamic IDs or re-enable shadow DOM

Option 1: Generate unique IDs per instance

+  private inputId = `setting-number-input-${Math.random().toString(36).substr(2, 9)}`;
+
   render() {
     const rainbowClass = this.easter
       ? "bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)] bg-[length:1400%_1400%] animate-rainbow-bg text-white hover:bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)]"
       : "";
 
     return html`
       <div
         class="flex flex-row items-center justify-between w-full p-4 bg-white/5 border border-white/10 rounded-xl hover:bg-white/10 transition-all gap-4 ${rainbowClass}"
       >
         <div class="flex flex-col flex-1 min-w-0 mr-4">
           <label
             class="text-white font-bold text-base block mb-1"
-            for="setting-number-input"
+            for=${this.inputId}
             >${this.label}</label
           >
           <div class="text-white/50 text-sm leading-snug">
             ${this.description}
           </div>
         </div>
         <input
           type="number"
-          id="setting-number-input"
+          id=${this.inputId}

Option 2: Remove createRenderRoot() to restore shadow DOM (preferred if no global styles are needed)

-  createRenderRoot() {
-    return this;
-  }
src/client/HostLobbyModal.ts (1)

1181-1191: Critical: Second unresolved merge conflict.

Another merge conflict marker at line 1181-1182. This must also be resolved.

🔧 Suggested resolution
   private async copyToClipboard() {
-<<<<<<< HEAD
     try {
       await navigator.clipboard.writeText(this.buildLobbyUrl());
       this.copySuccess = true;
       setTimeout(() => {
         this.copySuccess = false;
       }, 2000);
     } catch (err) {
       console.error(`Failed to copy text: ${err}`);
     }
   }

Note: Remove the <<<<<<< HEAD marker and keep the implementation. Check if there's a matching ======= and >>>>>>> marker further down that was cut off.

🤖 Fix all issues with AI agents
In @src/client/GameStartingModal.ts:
- Around line 17-43: Add proper accessibility attributes and focus/keyboard
handling to the modal elements and its show/hide logic: give the visible modal
container a role="dialog" (or "alertdialog"), aria-modal="true", and either
aria-labelledby pointing to the title element or an aria-label; mark the
backdrop as aria-hidden when visible; update the GameStartingModal.show() and
GameStartingModal.hide() (or equivalent) to move focus into the modal (focus the
primary actionable element or the title), restore focus on close, implement a
focus trap to keep tab navigation inside the modal while isVisible is true, and
add Escape key handling to call hide(); ensure all referenced IDs (e.g., title
element ID used by aria-labelledby) and focus management calls match the modal
DOM nodes in GameStartingModal.ts.

In @src/client/graphics/layers/HeadsUpMessage.ts:
- Line 65: The ternary in HeadsUpMessage that sets the timeout uses a redundant
nullish coalescing: change the expression in the ternary from checking (duration
?? 2000) to just duration when typeof duration === "number"; specifically update
the expression in the code handling duration (the ternary that currently reads
typeof duration === "number" ? (duration ?? 2000) : 2000) to typeof duration ===
"number" ? duration : 2000 so the fallback 2000 remains for non-number values
but the unnecessary ?? is removed.

In @src/client/HostLobbyModal.ts:
- Around line 78-101: In renderOptionToggle, the template opens a <button> but
never closes it; update the TemplateResult returned by renderOptionToggle to
include the missing closing </button> tag (after the inner <div>), ensuring the
HTML template string is properly closed and the function still returns a valid
TemplateResult.
- Around line 934-964: Remove the Git conflict markers and merge both changes:
in HostLobbyModal (the modal close/reset logic), replace the conflict region
with a single block that first calls history.replaceState(null, "",
window.location.origin + "/"); and then resets all transient form state fields
(selectedMap, selectedDifficulty, disableNations, gameMode, teamCount, bots,
spawnImmunity, spawnImmunityDurationMinutes, infiniteGold, donateGold,
infiniteTroops, donateTroops, maxTimer, maxTimerValue, instantBuild,
randomSpawn, compactMap, useRandomMap, disabledUnits, lobbyId, copySuccess,
clients, lobbyIdVisible, nationCount) ensuring the method braces remain correct
and no conflict markers remain.

In @src/client/JoinPrivateLobbyModal.ts:
- Around line 352-367: Remove the unresolved git conflict markers and keep the
BaseModal approach in JoinPrivateLobbyModal.open(): replace the conflicting
block with a call to super.open(), initialize lobbyIdVisible from
userSettings.get("settings.lobbyIdVisibility", true), and if an id argument is
present call setLobbyId(id) and joinLobby(); remove the modalEl?.open() and
modalEl.onClose assignment from the old code path so no conflict markers remain
and the method uses the BaseModal behavior.
- Around line 395-401: In copyToClipboard
(JoinPrivateLobbyModal.copyToClipboard) replace the hash-style URL string
`${location.origin}/#join=${this.currentLobbyId}` with the path-based format
used elsewhere, e.g.
`${location.origin}/game/${encodeURIComponent(this.currentLobbyId)}` (or the
app's canonical /game/{id} path), so the copied URL matches the PR's migration;
keep the same success/failure callbacks when calling the copyToClipboard helper.

In @src/client/styles/components/setting.css:
- Around line 1-8: Reduce the grid item minimum and center the grid to avoid
horizontal overflow and ragged alignment: change the grid-template-columns in
.settings-list from repeat(auto-fit, minmax(360px, 1fr)) to a safer min (e.g.,
repeat(auto-fit, minmax(240px, 1fr))) and add centering rules (e.g.,
justify-content: center; and/or justify-items: center) so cards don’t force
horizontal scroll in narrow containers and remain centered on wide screens; keep
existing gap/padding rules and preserve card max-width elsewhere.

In @src/client/Utils.ts:
- Around line 87-97: The Digit regex only matches single digits and the simple
fallback capitalization produces "Arrowup" instead of "ArrowUp"; update the
Digit pattern from /^Digit\d$/ to accept one-or-more digits (e.g., /^Digit\d+$/)
so multi-digit codes are handled, keep or adjust the Key pattern (/^Key[A-Z]$/)
as needed, and replace the naive fallback (the line using
value.charAt(0).toUpperCase() + value.slice(1)) with a routine that preserves
camelCase by splitting on camel-word boundaries and capitalizing each segment’s
first letter (so "arrowup" → "ArrowUp" and existing "ArrowUp" stays correct).
Ensure you change the two regex literals (/^Digit\d$/ and the fallback
capitalization logic) in Utils.ts where these transformations occur.

In @src/client/vite-env.d.ts:
- Around line 13-17: The module declarations for "*.md?url" and "*.txt?raw" are
unsupported unless those extensions are added to Vite's assetsInclude; either
remove the "*.md?url" and "*.txt?raw" declarations from vite-env.d.ts, or add
the file globs to the vite config by updating the assetsInclude setting (e.g.,
include "**/*.md" and "**/*.txt"); keep "*.svg?url" as-is since .svg is an asset
by default.

In @src/core/validations/username.ts:
- Line 83: The comment examples are inconsistent with the actual behavior of the
username restoration which always uppercases the clan tag and inserts a space
(see the return expression `[${clanTag.toUpperCase()}]
${censoredNameWithoutClan}` in src/core/validations/username.ts); update the
example on line 61 to show the space after the bracket so it reads `"[CLAN]
GoodName" -> "[CLAN] GoodName"` (so examples match the logic that uppercases
clan tags and adds a space).

In @src/server/Worker.ts:
- Around line 275-277: Validate the route parameter gameID at the entry of
app.get("/game/:id") by checking req.params.id against GAME_ID_REGEX; if it
fails validation, respond immediately with a 400 (Bad Request) and an
explanatory message and do not call gm.game or log the invalid value; only
proceed to call gm.game(gameID) and any logging after the regex check passes.
- Around line 176-182: The app.use(express.static(path.join(__dirname,
"../../static"))) call references a non-existent static directory and will fail
to serve files; either create the repository root "static" folder and add the
required assets, or change the static middleware to point to an existing
directory (for example replace "../../static" with an existing path like
"../../resources" or another build output) so app.use(express.static(...)) in
Worker.ts and its surrounding static-serving setup uses a real folder; keep the
existing "/maps" route unchanged as it correctly points to
"../../resources/maps".
🧹 Nitpick comments (40)
src/client/graphics/layers/PlayerInfoOverlay.ts (1)

492-509: Consider aligning unit info opacity with the rest of the PR.

You removed opacity styling from the health line, but unit.type() still uses opacity-80 (Line 498). If the intent is “no opacity in this overlay”, consider removing that too for consistency.

src/client/ClientGameRunner.ts (1)

124-143: Good addition of error handling for game creation failures.

The catch block appropriately logs errors, hides the loading modal, and displays error details to the user. The direct DOM manipulation to hide the modal follows the codebase pattern.

However, consider typing the error parameter to ensure message and stack properties exist:

♻️ Type the error parameter
       )
-        .then((r) => r.start())
-        .catch((e) => {
-          console.error("error creating client game", e);
+        .then((r) => r.start())
+        .catch((e: unknown) => {
+          const error = e instanceof Error ? e : new Error(String(e));
+          console.error("error creating client game", error);
           const startingModal = document.querySelector(
             "game-starting-modal",
           ) as HTMLElement;
           if (startingModal) {
             startingModal.classList.add("hidden");
           }
           showErrorModal(
-            e.message,
-            e.stack,
+            error.message,
+            error.stack,
             lobbyConfig.gameID,
             lobbyConfig.clientID,
             true,
             false,
             "error_modal.connection_error",
           );
         });
src/client/utilities/RenderUnitTypeOptions.ts (1)

23-46: Consider clarifying the toggleUnit API for better readability.

The function works correctly, but the toggleUnit interface could be more intuitive. Currently, it's named "toggle" but requires passing the current state (isEnabled), meaning the handler must flip the boolean internally. This indirection adds cognitive load.

Two clearer alternatives:

  1. State-less toggle: toggleUnit(type: UnitType) => void — handler reads current state internally
  2. Explicit setter: Rename to setUnitEnabled(type: UnitType, enabled: boolean) => void — makes the set operation clear

Either approach would make the intent more obvious at the call site.

♻️ Example refactor (state-less toggle)

Update the interface:

 export interface UnitTypeRenderContext {
   disabledUnits: UnitType[];
-  toggleUnit: (unit: UnitType, checked: boolean) => void;
+  toggleUnit: (unit: UnitType) => void;
 }

Update the call site:

-        @click=${() => toggleUnit(type, isEnabled)}
+        @click=${() => toggleUnit(type)}

The handler would then check disabledUnits.includes(type) internally to determine the new state.

src/client/components/baseComponents/stats/DiscordUserHeader.ts (1)

43-56: Consider using Tailwind's spacing scale instead of arbitrary padding.

Line 46 uses p-[3px], which is outside Tailwind's standard spacing scale (0.5rem increments). Unless 3px is a strict design requirement, use p-0.5 (2px) or p-1 (4px) for consistency with the design system.

♻️ Proposed refactor using spacing scale
-              <div class="p-[3px] rounded-full bg-gray-500">
+              <div class="p-1 rounded-full bg-gray-500">
src/client/graphics/ui/NavalTarget.ts (2)

108-131: Optional: Consider composition over inheritance.

NavalTarget extends Target in a classical inheritance pattern. While this works, composition with typed unions can offer clearer contracts and easier testing. For example, a union type like { kind: 'static', ... } | { kind: 'naval', unit: UnitView, ... } with a shared render function would avoid the base-class pattern.

This is existing architecture and outside the scope of the current change—just noting for future refactors.


21-21: Remove dead lifeTime field.

The lifeTime field is incremented on line 35 but never used. The fade animation uses animationElapsedTime instead. This field can be safely removed from the Target class to clean up the code.

src/client/styles/components/setting.css (1)

220-240: Firefox slider track heights should match WebKit for visual consistency.

The --fill CSS variable is properly updated by SettingSlider.ts on input changes (line 34: slider.style.setProperty("--fill", "${clamped}%")), so the gradient works correctly in WebKit. However, the Firefox pseudo-elements have different heights than the WebKit track, creating a visual mismatch.

Consistency fix for Firefox track heights
 .setting-input.slider::-moz-range-track {
   background-color: #444;
-  height: 10px;
-  border-radius: 5px;
+  height: 12px;
+  border-radius: 6px;
 }
 
 .setting-input.slider::-moz-range-progress {
   background-color: #2196f3;
-  height: 10px;
-  border-radius: 5px;
+  height: 12px;
+  border-radius: 6px;
 }

Also applies to: 253-263

src/client/graphics/layers/HeadsUpMessage.ts (3)

19-20: Consider importing TemplateResult at the top.

The inline import("lit").TemplateResult type reference works but is inconsistent with other imports. For cleaner code, import TemplateResult alongside other lit imports on line 1.

📦 Proposed refactor
-import { LitElement, html } from "lit";
+import { LitElement, html, type TemplateResult } from "lit";

Then update line 20:

-  private toastMessage: string | import("lit").TemplateResult | null = null;
+  private toastMessage: string | TemplateResult | null = null;

48-68: Consider defining an interface for the event detail.

The event handling relies on runtime type checks for message and loosely typed event.detail. Defining a typed interface would improve maintainability and catch errors earlier.

💡 Example interface
interface ShowMessageEventDetail {
  message: string | TemplateResult;
  duration?: number;
  color?: "green" | "red";
}

// Then in the handler:
private handleShowMessage = (event: CustomEvent<ShowMessageEventDetail>) => {
  const { message, duration, color } = event.detail ?? {};
  // Type-safe access to properties
  this.toastMessage = message;
  this.toastColor = color === "red" ? "red" : "green";
  // ...
};

107-117: Extract repeated color logic to reduce duplication.

The color conditionals for background, border, and box-shadow are repeated 5 times in the inline styles. Extracting this to a computed property or helper function would make the template cleaner and easier to maintain.

♻️ Example refactor

Add a method:

private getToastStyles() {
  const isRed = this.toastColor === "red";
  const colorRgb = isRed ? "239,68,68" : "34,197,94";
  
  return {
    background: `rgba(${colorRgb},0.1)`,
    border: `1px solid rgba(${colorRgb},0.5)`,
    boxShadow: `0 0 30px 0 rgba(${colorRgb},0.3)`,
  };
}

Then in the template:

style="max-width: 90vw; min-width: 200px; text-align: center;
  background: ${styles.background};
  border: ${styles.border};
  color: white;
  box-shadow: ${styles.boxShadow};
  backdrop-filter: blur(12px);"
src/client/styles/components/modal.css (1)

1-4: Clear deprecation notice, but consider adding a removal timeline.

The comment clearly explains the architectural shift to component-scoped styles. However, it would help future maintainers to include a target date or milestone for removing this file entirely.

📝 Optional: Add removal timeline
 /* Deprecated global modal styles.
    The component-scoped styles in src/client/components/baseComponents/Modal.ts
    are the single source of truth now. Removing global overrides so the
-   component can control layout and internal scrolling behavior. */
+   component can control layout and internal scrolling behavior.
+   TODO: Remove this entire file after all legacy modals are migrated (target: vX.Y.Z). */
src/core/execution/TransportShipExecution.ts (3)

151-171: Spawn-to-water move is correct, but the dstShore !== null branch is dead code.
At this point init would have returned if dstShore was null; keeping the branch adds noise and can hide future regressions.

Proposed tidy-up
-    if (this.dstShore !== null) {
-      this.boat.setTargetTile(this.dstShore);
-    } else {
-      this.boat.setTargetTile(undefined);
-    }
+    this.boat.setTargetTile(this.dstShore);

218-255: Retreat: storing dstShore = src is a clean invariant.
Also good: you recompute dst as adjacent water to keep the water-pathfinder consistent. One small safety win: avoid this.dstShore! in setTargetTile() since you’ve just assigned it.


325-336: adjacentWater() is simple and readable; good helper extraction.
Minor thought: if mg.neighbors() order is not stable, you may get non-deterministic “first water neighbor” selection (only matters if multiple water neighbors behave differently).

src/client/GameStartingModal.ts (2)

15-15: Remove redundant local variable.

The const isVisible = this.isVisible; declaration is unnecessary. You can reference this.isVisible directly in the template since Lit's html tagged template handles property access correctly.

♻️ Proposed refactor
 render() {
-  const isVisible = this.isVisible;
   return html`
     <div
-      class="fixed inset-0 bg-black/30 backdrop-blur-[4px] z-[9998] transition-all duration-300 ${isVisible
+      class="fixed inset-0 bg-black/30 backdrop-blur-[4px] z-[9998] transition-all duration-300 ${this.isVisible
         ? "opacity-100 visible"
         : "opacity-0 invisible"}"
     ></div>
     <div
-      class="fixed top-1/2 left-1/2 bg-zinc-800/70 p-6 rounded-xl z-[9999] shadow-[0_0_20px_rgba(0,0,0,0.5)] backdrop-blur-[5px] text-white w-[300px] text-center transition-all duration-300 -translate-x-1/2 ${isVisible
+      class="fixed top-1/2 left-1/2 bg-zinc-800/70 p-6 rounded-xl z-[9999] shadow-[0_0_20px_rgba(0,0,0,0.5)] backdrop-blur-[5px] text-white w-[300px] text-center transition-all duration-300 -translate-x-1/2 ${this.isVisible
         ? "opacity-100 visible -translate-y-1/2"
         : "opacity-0 invisible -translate-y-[48%]"}"
     >

47-55: Manual requestUpdate() calls may be unnecessary.

Since isVisible is decorated with @state(), LitElement should automatically trigger a re-render when it changes. The manual requestUpdate() calls in show() and hide() are likely redundant unless there's a specific timing requirement.

You can simplify to:

♻️ Simplified show/hide methods
 show() {
   this.isVisible = true;
-  this.requestUpdate();
 }

 hide() {
   this.isVisible = false;
-  this.requestUpdate();
 }

If manual updates are required for timing reasons specific to game-start transitions, consider adding a comment explaining why.

src/server/Worker.ts (3)

52-57: Consider dev environment handling in protocol fallback

The hardcoded "https" fallback may cause issues in local development where http is typically used. Consider checking config.env() to provide appropriate defaults for dev vs production.

💡 Optional enhancement for dev environment support
 const requestOrigin = (req: Request): string => {
   const protoHeader = (req.headers["x-forwarded-proto"] as string) ?? "";
-  const proto = protoHeader.split(",")[0]?.trim() || req.protocol || "https";
+  const proto = protoHeader.split(",")[0]?.trim() || req.protocol || (config.env() === GameEnv.Dev ? "http" : "https");
   const host = req.get("host") ?? `${config.subdomain()}.${config.domain()}`;
   return `${proto}://${host}`;
 };

299-311: Synchronous file operations may block under load

fs.existsSync() and fs.readFileSync() are synchronous and will block the event loop on each HTML request. For high-traffic scenarios, consider:

  1. Reading the HTML template once at startup and storing in memory
  2. Using async fs.promises.readFile()
  3. Caching the parsed HTML structure

However, since meta tags differ per game, you'd need to cache the template and inject tags dynamically.

💡 Example: Cache HTML template at startup

At the top of startWorker():

// Read HTML template once at startup
let htmlTemplate: string | null = null;
const staticHtml = path.join(__dirname, "../../static/index.html");
const rootHtml = path.join(__dirname, "../../index.html");

try {
  if (fs.existsSync(staticHtml)) {
    htmlTemplate = fs.readFileSync(staticHtml, "utf-8");
  } else if (fs.existsSync(rootHtml)) {
    htmlTemplate = fs.readFileSync(rootHtml, "utf-8");
  }
} catch (error) {
  log.error("Failed to load HTML template", error);
}

Then in the route:

-        if (filePath) {
-          const html = fs.readFileSync(filePath, "utf-8");
+        if (htmlTemplate) {
+          const html = htmlTemplate;
           const root = parse(html);

290-292: Consider 404 response instead of redirect for missing games

Redirecting to / when no game info is found (line 291) loses the game ID context. A 404 status with a user-friendly message might provide better UX and allow proper error tracking.

🎯 Alternative: Return 404 with message
       if (!lobby && !publicInfo) {
-        return res.redirect(302, "/");
+        return res.status(404).send("Game not found");
       }
src/client/components/baseComponents/Button.ts (1)

28-31: Simplify responsive class logic and verify fill+block interaction.

The condition on lines 29-30 could be simplified using a single negative check. Also verify that fill and block won't both be true simultaneously, as they apply conflicting width/height classes (h-full w-full vs w-full block).

♻️ Simplified logic
           "h-full w-full flex items-center justify-center": this.fill,
-          "lg:w-auto lg:inline-block":
-            !this.block && !this.blockDesktop && !this.fill,
+          "lg:w-auto lg:inline-block": !this.block && !this.blockDesktop && !this.fill,
           "lg:w-1/2 lg:mx-auto lg:block": this.blockDesktop,

Consider documenting that fill should not be used with block or blockDesktop, or add runtime validation if they're mutually exclusive.

src/client/styles.css (1)

42-45: Fallback pattern looks good, but consider adding a comment for clarity.

The duplicate height property (line 43 as fallback, line 44 with 100dvh) is a valid progressive enhancement pattern for browsers without dvh support. Static analysis flags this as a duplicate, but it is intentional.

Consider adding a brief comment to make the intent clear and silence future warnings:

 html {
-  height: 100%; /* Fallback */
+  height: 100%; /* Fallback for browsers without dvh support */
   height: 100dvh;
 }
src/client/UsernameInput.ts (1)

134-162: Edge case: empty baseUsername check uses trim() but the stored value uses untrimmed input.

At line 136, you check !this.baseUsername.trim(), but this.baseUsername itself is assigned from val at line 130 which is not trimmed. The final stored username at line 157 uses trimmedFull. Consider whether leading/trailing spaces in baseUsername should be allowed or stripped on input.

This is a minor UX consideration - users might accidentally add spaces that remain in the input field but get trimmed when stored.

src/server/MapPlaylist.ts (1)

24-66: Sanity-check GameMapType key iteration + consider avoiding Partial here.

Adding DidierFrance: 2 is fine, but the current Partial<Record<GameMapName, number>> + Object.keys(GameMapType) pattern can hide mistakes (missing keys become 0, and enum key iteration can be tricky if GameMapType is a numeric enum). Consider tightening this so missing/invalid map keys fail loudly (even if you keep a separate “default 0” fallback).

tests/client/components/FluentSlider.test.ts (1)

281-294: Edge-case coverage is good; consider making the “0%” style assertion less brittle.

The “no NaN” assertion is great. The "0%" check may vary if the style string format changes; if this becomes flaky, consider matching the specific CSS var/property you set (regex) rather than any "0%" substring.

src/client/Layout.ts (1)

7-7: Extract magic number 768 to a named constant.

The breakpoint 768 appears three times. Extract it to a constant like MOBILE_BREAKPOINT for maintainability.

♻️ Extract breakpoint constant
+const MOBILE_BREAKPOINT = 768;
+
 export function initLayout() {
   const hb = document.getElementById("hamburger-btn");
   const sidebar = document.getElementById("sidebar-menu");
   const backdrop = document.getElementById("mobile-menu-backdrop");
 
   // Force sidebar visibility style to ensure it's not hidden by other CSS
-  if (sidebar && window.innerWidth < 768) {
+  if (sidebar && window.innerWidth < MOBILE_BREAKPOINT) {
     sidebar.style.display = "flex";
   }

And apply the same constant on lines 60 and 76:

-    if (window.innerWidth >= 768) return;
+    if (window.innerWidth >= MOBILE_BREAKPOINT) return;

Also applies to: 60-60, 76-76

src/client/Navigation.ts (1)

14-20: Redundant direct manipulation of page-play?

Line 19 directly adds the hidden class to page-play after already iterating over all .page-content elements (lines 15-18). If page-play has the .page-content class, this is redundant.

Verify whether page-play is already included in .page-content and remove line 19 if redundant:

#!/bin/bash
# Check if page-play element has page-content class in HTML templates
rg -i 'id="page-play"' -A2 -B2
src/client/components/baseComponents/setting/SettingNumber.ts (1)

32-34: Long inline class string hurts readability.

The rainbow gradient class is 255+ characters. Extract it to a constant at the module level for easier maintenance.

♻️ Extract to constant
+const RAINBOW_GRADIENT_CLASS = 
+  "bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)] " +
+  "bg-[length:1400%_1400%] animate-rainbow-bg text-white " +
+  "hover:bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)]";
+
 @customElement("setting-number")
 export class SettingNumber extends LitElement {
   // ...
   
   render() {
-    const rainbowClass = this.easter
-      ? "bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)] bg-[length:1400%_1400%] animate-rainbow-bg text-white hover:bg-[linear-gradient(270deg,#990033,#996600,#336600,#008080,#1c3f99,#5e0099,#990033)]"
-      : "";
+    const rainbowClass = this.easter ? RAINBOW_GRADIENT_CLASS : "";
src/client/components/baseComponents/stats/PlayerStatsTree.ts (1)

130-141: Long inline class strings reduce readability.

The type button classes span 4 lines with ternary logic embedded. Extract active and inactive class sets to constants.

♻️ Extract button class constants
+  private readonly activeTypeClass = 
+    "text-xs px-3 py-1.5 rounded-md border font-bold uppercase tracking-wider " +
+    "transition-all duration-200 bg-blue-600 border-blue-500 text-white " +
+    "shadow-lg shadow-blue-900/40";
+  
+  private readonly inactiveTypeClass = 
+    "text-xs px-3 py-1.5 rounded-md border font-bold uppercase tracking-wider " +
+    "transition-all duration-200 bg-white/5 border-white/10 text-gray-400 " +
+    "hover:bg-white/10 hover:text-white";
+
   render() {
     // ...
     ${types.map(
       (t) => html`
         <button
-          class="text-xs px-3 py-1.5 rounded-md border font-bold uppercase tracking-wider transition-all duration-200 ${this
-            .selectedType === t
-            ? "bg-blue-600 border-blue-500 text-white shadow-lg shadow-blue-900/40"
-            : "bg-white/5 border-white/10 text-gray-400 hover:bg-white/10 hover:text-white"}"
+          class="${this.selectedType === t ? this.activeTypeClass : this.inactiveTypeClass}"
           @click=${() => this.setGameType(t)}
         >
src/client/components/Difficulties.ts (1)

61-65: Long class constants could be split for readability.

The activeClass constant is 240+ characters. While functional, consider splitting into semantic parts like activeColorClass and activeAnimationClass for easier maintenance.

♻️ Split class constants
-  const activeClass =
-    "opacity-100 text-[#ff3838] drop-shadow-[0_0_4px_rgba(255,56,56,0.4)] transform group-hover:drop-shadow-[0_0_6px_rgba(255,56,56,0.6)] group-hover:-translate-y-[2px] -translate-y-[1px] transition-all duration-200";
+  const baseActiveClass = "opacity-100 text-[#ff3838] transition-all duration-200";
+  const activeShadowClass = "drop-shadow-[0_0_4px_rgba(255,56,56,0.4)] group-hover:drop-shadow-[0_0_6px_rgba(255,56,56,0.6)]";
+  const activeTransformClass = "transform -translate-y-[1px] group-hover:-translate-y-[2px]";
+  const activeClass = `${baseActiveClass} ${activeShadowClass} ${activeTransformClass}`;
+
   const inactiveClass = "opacity-30 w-4 h-4 transition-all duration-200";
src/client/PublicLobby.ts (1)

376-393: Consider adding a typed interface for username-input.

The as any cast works but loses type safety. If the username-input component exports its class, import and use it for better type checking.

// Example improvement:
import { UsernameInput } from "./UsernameInput"; // if exported
const usernameInput = document.querySelector("username-input") as UsernameInput | null;
src/client/components/BaseModal.ts (2)

115-119: Hardcoded fallback page "page-play" reduces flexibility.

When closing an inline modal, the code always navigates to "page-play". This may not be correct if the modal was opened from a different page. Consider passing the return target as a parameter or storing the previous page.


14-17: Prefer composition over inheritance for modal behavior.

This abstract class requires subclasses to extend it. A composition-based approach (e.g., a helper object or mixin that modals can use) would be more flexible and easier to test. That said, the current approach works and aligns with other modals in this PR.

src/client/FlagInputModal.ts (2)

9-9: Unused modalRef query.

The modalRef is declared but never used in the component. Consider removing it to reduce confusion.

Proposed fix
 @customElement("flag-input-modal")
 export class FlagInputModal extends BaseModal {
-  @query("#flag-input-modal") private modalRef!: HTMLElement;
-
   @state() private search = "";

14-16: Empty updated() override can be removed.

This override only calls super.updated() with no additional logic. It can be safely deleted.

src/client/KeybindsModal.ts (1)

35-38: Consider using a typed union instead of string | string[].

The value field accepts both string and string[], but arrays are normalized to their first element everywhere. If arrays are not truly needed, simplify to just string. If they are needed, document why.

Proposed simplification if arrays are not needed
   @state() private keybinds: Record<
     string,
-    { value: string | string[]; key: string }
+    { value: string; key: string }
   > = {};
src/client/Main.ts (1)

237-256: The 100ms timeout is fragile for component readiness.

Using setTimeout(() => {...}, 100) to wait for custom element rendering is brittle. Different devices or load conditions may need more or less time. A better approach is to use a mutation observer or check for element existence in a loop with requestAnimationFrame.

Suggested improvement using polling
-    // Wait for the flag-input component to be fully ready
-    customElements.whenDefined("flag-input").then(() => {
-      // Use a small delay to ensure the component has rendered
-      setTimeout(() => {
-        const flagButton = document.querySelector(
-          "#flag-input-component #flag-input_",
-        );
-        if (!flagButton) {
-          console.warn("Flag button not found inside component");
-          return;
-        }
-        flagButton.addEventListener("click", (e) => {
-          e.preventDefault();
-          e.stopPropagation();
-          if (flagInputModal && flagInputModal instanceof FlagInputModal) {
-            flagInputModal.open();
-          }
-        });
-      }, 100);
-    });
+    customElements.whenDefined("flag-input").then(() => {
+      const tryAttachListener = () => {
+        const flagButton = document.querySelector(
+          "#flag-input-component #flag-input_",
+        );
+        if (!flagButton) {
+          requestAnimationFrame(tryAttachListener);
+          return;
+        }
+        flagButton.addEventListener("click", (e) => {
+          e.preventDefault();
+          e.stopPropagation();
+          if (flagInputModal && flagInputModal instanceof FlagInputModal) {
+            flagInputModal.open();
+          }
+        });
+      };
+      tryAttachListener();
+    });
src/client/TerritoryPatternsModal.ts (1)

312-329: Options handling could use a type definition.

The open() method accepts string | { affiliateCode?: string; showOnlyOwned?: boolean }. Defining a named type makes the API clearer and helps with future maintenance.

Define options type
type OpenOptions = {
  affiliateCode?: string;
  showOnlyOwned?: boolean;
};

public async open(options?: string | OpenOptions) {
  // ...
}
src/client/components/baseComponents/Modal.ts (1)

75-78: Backdrop click handler should check for null.

When inline is true, you assign null to @click, but Lit handles this correctly. However, the ternary makes the intent less clear.

Use conditional binding for clarity
-            @click=${this.inline ? null : (e: Event) => this.close()}
+            @click=${this.inline ? undefined : () => this.close()}
src/client/JoinPrivateLobbyModal.ts (1)

311-325: Extract hardcoded unit translation keys to a shared constant.

The unitKeys mapping duplicates translations that exist in src/client/utilities/RenderUnitTypeOptions.ts. This mapping appears in multiple places (JoinPrivateLobbyModal, BuildMenu, tests) and will become brittle if translation keys change. Consider creating a shared constant for this mapping to maintain a single source of truth.

Location

src/client/JoinPrivateLobbyModal.ts, lines 311-325

src/client/Matchmaking.ts (1)

176-179: WebSocket onclose handler is minimal.

The handler only logs a message. This is fine since cleanup happens in onClose(), but if the server closes the connection unexpectedly while the modal is still open, the user sees no feedback.

Consider updating the UI state or showing a message when the connection closes unexpectedly:

💡 Optional: Add user feedback on unexpected disconnect
     this.socket.onclose = () => {
       console.log("Matchmaking server closed connection");
+      if (this.isModalOpen && this.connected && this.gameID === null) {
+        window.dispatchEvent(
+          new CustomEvent("show-message", {
+            detail: {
+              message: translateText("matchmaking_modal.disconnected"),
+              color: "yellow",
+              duration: 3000,
+            },
+          }),
+        );
+      }
     };

Comment thread src/client/components/BaseModal.ts
Comment thread src/client/components/ModalOverlay.ts
Comment thread src/client/FlagInput.ts
Comment thread src/client/GameStartingModal.ts
Comment thread src/client/graphics/layers/HeadsUpMessage.ts
Comment thread src/client/vite-env.d.ts
Comment thread src/core/execution/TransportShipExecution.ts Outdated
Comment thread src/core/validations/username.ts
Comment thread src/server/Worker.ts Outdated
Comment thread src/server/Worker.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/JoinPrivateLobbyModal.ts (1)

389-395: Bug: Copy URL uses old hash-based format instead of new path-based format.

The copyToClipboard method still generates /#join= URLs, but this PR migrates to path-based /game/{id} URLs. This creates an inconsistency where users share broken links.

Proposed fix
 private async copyToClipboard() {
   await copyToClipboard(
-    `${location.origin}/#join=${this.currentLobbyId}`,
+    `${location.origin}/game/${this.currentLobbyId}`,
     () => (this.copySuccess = true),
     () => (this.copySuccess = false),
   );
 }
🤖 Fix all issues with AI agents
In @src/client/Main.ts:
- Around line 639-647: The path-based lobby detection in Main.ts only handles
/game/{id} and returns early, breaking legacy hash links; add a fallback hash
parse before returning by matching window.location.hash for `#join=...`,
validate with GAME_ID_REGEX and set lobbyId accordingly if present; ensure this
fallback runs when the path check yields no lobbyId so old shared links still
open the join modal; also update JoinPrivateLobbyModal (the URL generation
logic) to emit new `/game/{id}` links instead of `#join=` so future links use
the canonical format.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b31bd4e and 8737951.

📒 Files selected for processing (4)
  • src/client/HostLobbyModal.ts
  • src/client/JoinPrivateLobbyModal.ts
  • src/client/Main.ts
  • src/server/GamePreviewBuilder.ts
🧰 Additional context used
🧠 Learnings (23)
📓 Common learnings
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/server/GamePreviewBuilder.ts
  • src/client/JoinPrivateLobbyModal.ts
  • src/client/Main.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.

Applied to files:

  • src/server/GamePreviewBuilder.ts
  • src/client/HostLobbyModal.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-27T09:47:26.395Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:770-795
Timestamp: 2025-10-27T09:47:26.395Z
Learning: In src/core/execution/FakeHumanExecution.ts, the selectSteamrollStopTarget() method intentionally allows MIRV targeting when secondHighest city count is 0 (e.g., nuclear endgame scenarios where structures are destroyed). This is valid game design—"if you can afford it, you're good to go"—and should not be flagged as requiring a guard condition.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-20T20:15:28.858Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:51-51
Timestamp: 2025-10-20T20:15:28.858Z
Learning: In src/core/execution/FakeHumanExecution.ts, game balance constants like MIRV_COOLDOWN_TICKS, MIRV_HESITATION_ODDS, VICTORY_DENIAL_TEAM_THRESHOLD, VICTORY_DENIAL_INDIVIDUAL_THRESHOLD, and STEAMROLL_CITY_GAP_MULTIPLIER are experimental tuning parameters subject to frequent change during balance testing. Do not flag changes to these values as issues or compare them against previous values.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/server/GamePreviewBuilder.ts
  • src/client/JoinPrivateLobbyModal.ts
  • src/client/HostLobbyModal.ts
  • src/client/Main.ts
📚 Learning: 2025-10-26T15:37:07.732Z
Learnt from: GlacialDrift
Repo: openfrontio/OpenFrontIO PR: 2298
File: src/client/graphics/layers/TerritoryLayer.ts:200-210
Timestamp: 2025-10-26T15:37:07.732Z
Learning: In GameImpl.ts lines 124-139, team assignment logic varies by number of teams: when numPlayerTeams < 8, teams are assigned ColoredTeams values (Red, Blue, Yellow, Green, Purple, Orange, Teal); when numPlayerTeams >= 8, teams are assigned generic string identifiers like "Team 1", "Team 2", etc., which are not members of ColoredTeams.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-20T11:02:16.969Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:57-57
Timestamp: 2025-10-20T11:02:16.969Z
Learning: In src/core/execution/FakeHumanExecution.ts, the correct MIRV victory denial thresholds are VICTORY_DENIAL_TEAM_THRESHOLD = 0.8 (80% for team games) and VICTORY_DENIAL_INDIVIDUAL_THRESHOLD = 0.65 (65% for individual players), not 0.85 and 0.7 as might be mentioned in some documentation.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.

Applied to files:

  • src/server/GamePreviewBuilder.ts
  • src/client/HostLobbyModal.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as a type inferred from a Zod schema that is marked as optional, which means PlayerStats already includes undefined as a possible type (PlayerStats | undefined).

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as `z.infer<typeof PlayerStatsSchema>` where PlayerStatsSchema has `.optional()` applied at the object level, making PlayerStats a union type that already includes undefined (PlayerStats | undefined).

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-19T06:00:38.007Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:125-134
Timestamp: 2025-05-19T06:00:38.007Z
Learning: In StatsImpl.ts, unused parameters in boat/stats-related methods are intentionally kept for future use and shouldn't be removed.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-21T04:10:59.706Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:44-53
Timestamp: 2025-05-21T04:10:59.706Z
Learning: The PlayerStats type from ArchiveSchemas already includes undefined in its definition, making explicit union types with undefined (PlayerStats | undefined) redundant.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, when the browser back button is pressed, the `popstate` event fires before the `hashchange` event. The `preventHashUpdate` flag is used to prevent the `hashchange` listener (`onHashUpdate`) from executing after a navigation rollback in the `popstate` listener (`onPopState`), specifically when the user cancels leaving an active game.

Applied to files:

  • src/client/JoinPrivateLobbyModal.ts
  • src/client/HostLobbyModal.ts
📚 Learning: 2026-01-02T18:11:06.832Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:06.832Z
Learning: In src/client/HostLobbyModal.ts, the ?s=xxxxx URL suffix used in lobby URLs is purely for cache-busting platform previews (e.g., Discord, WhatsApp, x.com) and is not used by the join logic. The join flow ignores the suffix value, so regenerating it via updateUrlWithSuffix() on configuration changes will not break existing shared URLs; it only prompts platforms to refresh preview metadata. Treat the suffix as non-functional for join behavior and ensure any related changes preserve that invariant.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-08-17T20:48:49.411Z
Learnt from: woodydrn
Repo: openfrontio/OpenFrontIO PR: 1836
File: src/client/Main.ts:482-482
Timestamp: 2025-08-17T20:48:49.411Z
Learning: In PR #1836, user woodydrn prefers to keep changes minimal and focused on the specific issue (clientID persistence) rather than refactoring redundant code in JoinLobbyEvent dispatchers. They want to avoid scope creep in focused bug fix PRs.

Applied to files:

  • src/client/HostLobbyModal.ts
  • src/client/Main.ts
📚 Learning: 2025-07-30T19:46:23.797Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1436
File: src/server/Worker.ts:93-93
Timestamp: 2025-07-30T19:46:23.797Z
Learning: Input validation at API boundaries is critical for security. All client-provided data, especially parameters used for authorization decisions like creatorClientID in the kick player functionality, must be validated for type, format, length, and business logic constraints before processing.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-06-20T20:11:00.965Z
Learnt from: devalnor
Repo: openfrontio/OpenFrontIO PR: 1195
File: src/client/graphics/layers/AlertFrame.ts:18-18
Timestamp: 2025-06-20T20:11:00.965Z
Learning: In the OpenFrontIO codebase, UserSettings instances are created directly with `new UserSettings()` in each component that needs them. This pattern is used consistently across at least 12+ files including OptionsMenu, EventsDisplay, DarkModeButton, Main, UserSettingModal, UsernameInput, NameLayer, AlertFrame, UILayer, InputHandler, ClientGameRunner, and GameView. This is the established architectural pattern and should be followed for consistency.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, ensure the browser back/forward navigation ordering is correctly handled: when popstate fires before hashchange during a navigation rollback (e.g., user cancels leaving an active game), use a preventHashUpdate-like flag to suppress the hashchange listener (onHashUpdate) from running in that rollback scenario. This avoids applying an unintended hash update after a rollback and keeps the UI/game state consistent. Document the flag’s usage and the exact conditions under which it is set/reset to prevent subtle regressions.

Applied to files:

  • src/client/Main.ts
🧬 Code graph analysis (4)
src/server/GamePreviewBuilder.ts (4)
src/core/game/GameView.ts (1)
  • gameID (910-912)
src/core/Schemas.ts (1)
  • GameInfo (134-140)
src/core/game/GameImpl.ts (1)
  • map (206-208)
src/core/configuration/DefaultConfig.ts (1)
  • playerTeams (317-319)
src/client/JoinPrivateLobbyModal.ts (1)
src/core/Schemas.ts (1)
  • GAME_ID_REGEX (222-222)
src/client/HostLobbyModal.ts (1)
src/core/Schemas.ts (1)
  • isValidGameID (224-225)
src/client/Main.ts (1)
src/core/Schemas.ts (1)
  • GAME_ID_REGEX (222-222)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (14)
src/client/JoinPrivateLobbyModal.ts (2)

413-429: Clean URL parsing with proper fallback.

Good use of URL constructor with try-catch, optional chaining on regex match, and validation against the centralized GAME_ID_REGEX. The fallback to return the original input on failure is a safe approach.


372-374: Good cleanup: URL reset on modal close.

Resetting the URL to base path on close prevents stale lobby URLs from persisting after the user leaves. This is consistent with the HostLobbyModal behavior.

src/client/HostLobbyModal.ts (3)

78-97: Clean URL helpers for cache-busting embed previews.

Good separation of concerns:

  • getRandomString() for suffix generation
  • buildLobbyUrl() for consistent URL construction
  • constructUrl() combines generation with building
  • updateHistory() for browser state

Based on learnings, the ?s=xxxxx suffix is purely for cache-busting platform previews (Discord, WhatsApp, etc.) and is ignored by join logic.


849-858: Good defensive validation after lobby creation.

Validating the lobby ID with isValidGameID before exposing it in URLs is a good safety check. If the server returns an unexpected format, the error is thrown early rather than propagating a malformed URL.


927-968: Thorough cleanup on modal close.

Comprehensive state reset including URL, timers, and all form fields ensures a clean slate for the next lobby creation. Good defensive practice.

src/server/GamePreviewBuilder.ts (5)

143-150: Good HTML escaping utility.

Covers the essential HTML entities: &, <, >, ", '. This is used by the Worker to safely inject preview metadata into HTML.


65-74: Clean duration formatting with edge case handling.

Good defensive check for non-finite and negative values. The hierarchical formatting (hours → minutes → seconds) is readable.


102-128: Robust winner parsing with format detection.

Good handling of both team and individual winner formats. The fallback to return undefined for unknown formats avoids displaying confusing output in previews.


4-32: Flexible schema for external game info.

Good use of optional fields and z.unknown() for the stats field, which can vary in shape. The z.union([z.number(), z.string()]) for playerTeams handles both numeric team counts and named team configurations (e.g., "Quads").


220-293: User-generated content in preview metadata is properly escaped.

All output from buildPreview is correctly escaped using escapeHtml in Worker.ts (lines 319–326) before injecting into HTML meta tags. This includes winner names, host username, and disabled units that appear in the title and description fields. No XSS vulnerability exists.

src/client/Main.ts (4)

769-776: Good URL sync helper.

Simple check to avoid redundant replaceState calls. This keeps the browser URL in sync with the current lobby for sharing without polluting browser history.


537-537: Good event-driven join trigger.

Adding join-changed listener allows other components (like AccountModal) to trigger the join flow without direct coupling. Reuses the existing onHashUpdate handler for consistency.


761-765: Live game URL with state marker.

Using ?live query param as a state indicator for embed previews is consistent with the PR's cache-busting strategy. The history.pushState creates a proper history entry for back-button navigation.


704-731: Extended modal close list.

Adding game-top-bar, account-button, and stats-button to the batch close ensures all UI elements are properly hidden during game transitions. Based on learnings, this follows the codebase pattern of direct DOM manipulation for UI hiding.

Comment thread src/client/Main.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @src/server/GamePreviewBuilder.ts:
- Around line 130-141: In countActivePlayers, the predicate wrongly returns true
for non-object stats (boolean/number/string), causing invalid stats to be
counted; update the predicate inside the filter in function countActivePlayers
to return false for any stats that are not an object with at least one property
(i.e., replace the final fallback return true with return false so only objects
with Object.keys(...).length > 0 are counted).
🧹 Nitpick comments (4)
src/server/GamePreviewBuilder.ts (4)

4-56: Derive ExternalGameInfo from schema to prevent type drift.

The ExternalGameInfo type (lines 40-56) manually mirrors ExternalGameInfoSchema (lines 4-32). Use z.infer<typeof ExternalGameInfoSchema> instead to eliminate duplication and ensure the type stays synchronized with the schema.

♻️ Refactor to use inferred type

Replace the manual type definition with:

-export type ExternalGameInfo = {
-  info?: {
-    config?: {
-      gameMap?: string;
-      gameMode?: string;
-      gameType?: string;
-      maxPlayers?: number;
-      playerTeams?: number | string;
-    };
-    players?: PlayerInfo[];
-    winner?: string[];
-    duration?: number;
-    start?: number;
-    end?: number;
-    lobbyCreatedAt?: number;
-  };
-};
+export type ExternalGameInfo = z.infer<typeof ExternalGameInfoSchema>;

Note: You'll need to keep the PlayerInfo type separate since it's referenced in the schema, or inline it within the schema definition.


76-78: Document the timestamp threshold.

The magic number 1e12 distinguishes seconds from milliseconds. Add a brief comment explaining this threshold to improve readability.

📝 Add clarifying comment
 function normalizeTimestamp(timestamp: number): number {
+  // Timestamps < 1 trillion are assumed to be in seconds (Unix epoch), otherwise milliseconds
   return timestamp < 1e12 ? timestamp * 1000 : timestamp;
 }

167-174: Player count fallback chain is complex but functional.

The logic correctly handles different data sources (finished vs active games), but the nested fallback on lines 171-173 makes it harder to trace. Consider extracting this to a named helper if similar logic appears elsewhere.


152-286: Consider extracting smaller helpers from buildPreview.

The function handles multiple responsibilities (config extraction, player counting, title/description generation) in 134 lines. While it's currently readable, extracting helpers like buildFinishedGameDescription, buildPrivateLobbyDescription, and buildTitle would improve testability and make future changes easier.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8737951 and 062838e.

📒 Files selected for processing (2)
  • src/client/HostLobbyModal.ts
  • src/server/GamePreviewBuilder.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/HostLobbyModal.ts
🧰 Additional context used
🧠 Learnings (17)
📓 Common learnings
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.
Learnt from: woodydrn
Repo: openfrontio/OpenFrontIO PR: 1836
File: src/client/Main.ts:482-482
Timestamp: 2025-08-17T20:48:49.411Z
Learning: In PR #1836, user woodydrn prefers to keep changes minimal and focused on the specific issue (clientID persistence) rather than refactoring redundant code in JoinLobbyEvent dispatchers. They want to avoid scope creep in focused bug fix PRs.
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-27T09:47:26.395Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:770-795
Timestamp: 2025-10-27T09:47:26.395Z
Learning: In src/core/execution/FakeHumanExecution.ts, the selectSteamrollStopTarget() method intentionally allows MIRV targeting when secondHighest city count is 0 (e.g., nuclear endgame scenarios where structures are destroyed). This is valid game design—"if you can afford it, you're good to go"—and should not be flagged as requiring a guard condition.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-20T20:15:28.858Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:51-51
Timestamp: 2025-10-20T20:15:28.858Z
Learning: In src/core/execution/FakeHumanExecution.ts, game balance constants like MIRV_COOLDOWN_TICKS, MIRV_HESITATION_ODDS, VICTORY_DENIAL_TEAM_THRESHOLD, VICTORY_DENIAL_INDIVIDUAL_THRESHOLD, and STEAMROLL_CITY_GAP_MULTIPLIER are experimental tuning parameters subject to frequent change during balance testing. Do not flag changes to these values as issues or compare them against previous values.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-26T15:37:07.732Z
Learnt from: GlacialDrift
Repo: openfrontio/OpenFrontIO PR: 2298
File: src/client/graphics/layers/TerritoryLayer.ts:200-210
Timestamp: 2025-10-26T15:37:07.732Z
Learning: In GameImpl.ts lines 124-139, team assignment logic varies by number of teams: when numPlayerTeams < 8, teams are assigned ColoredTeams values (Red, Blue, Yellow, Green, Purple, Orange, Teal); when numPlayerTeams >= 8, teams are assigned generic string identifiers like "Team 1", "Team 2", etc., which are not members of ColoredTeams.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-20T11:02:16.969Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:57-57
Timestamp: 2025-10-20T11:02:16.969Z
Learning: In src/core/execution/FakeHumanExecution.ts, the correct MIRV victory denial thresholds are VICTORY_DENIAL_TEAM_THRESHOLD = 0.8 (80% for team games) and VICTORY_DENIAL_INDIVIDUAL_THRESHOLD = 0.65 (65% for individual players), not 0.85 and 0.7 as might be mentioned in some documentation.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as a type inferred from a Zod schema that is marked as optional, which means PlayerStats already includes undefined as a possible type (PlayerStats | undefined).

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as `z.infer<typeof PlayerStatsSchema>` where PlayerStatsSchema has `.optional()` applied at the object level, making PlayerStats a union type that already includes undefined (PlayerStats | undefined).

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-19T06:00:38.007Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:125-134
Timestamp: 2025-05-19T06:00:38.007Z
Learning: In StatsImpl.ts, unused parameters in boat/stats-related methods are intentionally kept for future use and shouldn't be removed.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-21T04:10:59.706Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:44-53
Timestamp: 2025-05-21T04:10:59.706Z
Learning: The PlayerStats type from ArchiveSchemas already includes undefined in its definition, making explicit union types with undefined (PlayerStats | undefined) redundant.

Applied to files:

  • src/server/GamePreviewBuilder.ts
🧬 Code graph analysis (1)
src/server/GamePreviewBuilder.ts (4)
src/core/game/GameView.ts (1)
  • gameID (910-912)
src/core/Schemas.ts (1)
  • GameInfo (134-140)
src/core/game/GameImpl.ts (1)
  • map (206-208)
src/core/configuration/DefaultConfig.ts (1)
  • playerTeams (317-319)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (7)
src/server/GamePreviewBuilder.ts (7)

65-74: LGTM!

The duration formatting logic correctly handles edge cases and produces clean output.


80-98: LGTM!

Clean use of Intl.DateTimeFormat with explicit UTC timezone handling.


102-128: LGTM!

Winner parsing logic correctly handles team and individual formats with appropriate fallbacks.


143-150: LGTM!

Standard HTML entity escaping correctly covers the necessary characters.


184-191: LGTM!

Team formatting logic correctly handles finished vs lobby states, and the Math.max(1, ...) guard prevents division edge cases.


202-217: LGTM!

Image and title generation logic is clear and handles all expected states.


219-283: LGTM!

Description generation correctly handles all game states (finished, private lobby, public lobby) with appropriate detail levels. Username inclusion is safe since HTML escaping occurs at injection time in Worker.ts.

Comment thread src/server/GamePreviewBuilder.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/JoinPrivateLobbyModal.ts (1)

396-402: Update clipboard copy to use path-based URLs

The copy function still uses the old hash-based format /#join=, which is inconsistent with the PR's migration to path-based URLs (/game/{code}).

🔧 Suggested fix
  private async copyToClipboard() {
    await copyToClipboard(
-      `${location.origin}/#join=${this.currentLobbyId}`,
+      `${location.origin}/game/${this.currentLobbyId}?lobby`,
      () => (this.copySuccess = true),
      () => (this.copySuccess = false),
    );
  }
🧹 Nitpick comments (1)
src/client/JoinPrivateLobbyModal.ts (1)

386-389: Consider clarifying the operation sequence

The order of operations is correct but could benefit from a comment explaining why leaveLobby() is called before clearing currentLobbyId (since leaveLobby() checks this value at line 364).

💡 Optional clarity improvement
    // Reset URL to base when modal closes
    history.replaceState(null, "", window.location.origin + "/");
+   // Leave lobby before clearing ID (leaveLobby checks currentLobbyId)
    this.leaveLobby();
    this.currentLobbyId = "";
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67c5d28 and 8555892.

📒 Files selected for processing (3)
  • src/client/HostLobbyModal.ts
  • src/client/JoinPrivateLobbyModal.ts
  • src/client/components/baseComponents/Modal.ts
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.
📚 Learning: 2026-01-02T18:11:06.832Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:06.832Z
Learning: In src/client/HostLobbyModal.ts, the ?s=xxxxx URL suffix used in lobby URLs is purely for cache-busting platform previews (e.g., Discord, WhatsApp, x.com) and is not used by the join logic. The join flow ignores the suffix value, so regenerating it via updateUrlWithSuffix() on configuration changes will not break existing shared URLs; it only prompts platforms to refresh preview metadata. Treat the suffix as non-functional for join behavior and ensure any related changes preserve that invariant.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/HostLobbyModal.ts
  • src/client/JoinPrivateLobbyModal.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-08-17T20:48:49.411Z
Learnt from: woodydrn
Repo: openfrontio/OpenFrontIO PR: 1836
File: src/client/Main.ts:482-482
Timestamp: 2025-08-17T20:48:49.411Z
Learning: In PR #1836, user woodydrn prefers to keep changes minimal and focused on the specific issue (clientID persistence) rather than refactoring redundant code in JoinLobbyEvent dispatchers. They want to avoid scope creep in focused bug fix PRs.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-07-30T19:46:23.797Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1436
File: src/server/Worker.ts:93-93
Timestamp: 2025-07-30T19:46:23.797Z
Learning: Input validation at API boundaries is critical for security. All client-provided data, especially parameters used for authorization decisions like creatorClientID in the kick player functionality, must be validated for type, format, length, and business logic constraints before processing.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/client/JoinPrivateLobbyModal.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, when the browser back button is pressed, the `popstate` event fires before the `hashchange` event. The `preventHashUpdate` flag is used to prevent the `hashchange` listener (`onHashUpdate`) from executing after a navigation rollback in the `popstate` listener (`onPopState`), specifically when the user cancels leaving an active game.

Applied to files:

  • src/client/JoinPrivateLobbyModal.ts
🧬 Code graph analysis (2)
src/client/HostLobbyModal.ts (1)
src/core/Schemas.ts (1)
  • isValidGameID (224-225)
src/client/JoinPrivateLobbyModal.ts (2)
src/core/Schemas.ts (1)
  • GAME_ID_REGEX (222-222)
src/core/game/FetchGameMapLoader.ts (1)
  • url (41-51)
🔇 Additional comments (10)
src/client/components/baseComponents/Modal.ts (1)

76-76: LGTM - Explicit arrow functions for event handlers

Wrapping this.close() in arrow functions ensures explicit this binding in the event handlers. While LitElement typically preserves context, this defensive approach prevents potential issues.

Also applies to: 86-86

src/client/JoinPrivateLobbyModal.ts (3)

6-6: LGTM - Consistent validation using GAME_ID_REGEX

The import and usage of GAME_ID_REGEX provides centralized validation logic, ensuring consistency across the codebase.

Also applies to: 405-405


363-376: LGTM - Clean separation of leave logic

The new leaveLobby() method properly encapsulates the lobby exit flow with appropriate guards and event dispatching.


420-435: LGTM - Path-based URL extraction

The rewritten extractLobbyIdFromUrl correctly extracts lobby IDs from the new /game/{id} URL pattern using the URL API, with proper validation via GAME_ID_REGEX.

src/client/HostLobbyModal.ts (6)

24-24: LGTM - Defensive validation of lobby IDs

The import and usage of isValidGameID adds a defensive check ensuring the generated lobby ID matches the expected format before proceeding. This prevents downstream issues with malformed IDs.

Also applies to: 852-854


104-123: LGTM - Clean URL construction helpers

The helper methods are well-structured with clear separation of concerns:

  • getRandomString() generates cache-busting suffixes
  • buildLobbyUrl() constructs the full URL with current state
  • constructUrl() generates a new suffix and builds the URL
  • updateHistory() provides a single point for history manipulation

Based on learnings, the ?s=xxxxx suffix is purely for cache-busting embed previews and is ignored by join logic.


856-857: LGTM - URL management on lobby creation

The URL construction and history update after lobby creation ensures the browser URL reflects the current lobby state, enabling proper link sharing and embed preview generation.


1111-1112: LGTM - Cache-busting on config changes

Regenerating the URL suffix on each config change forces external platforms to re-fetch updated preview metadata while preserving the lobby ID, as intended by the PR design.

Based on learnings, this doesn't break existing shared URLs since the suffix is ignored by join logic.


929-931: LGTM - Proper cleanup sequence

The cleanup sequence correctly leaves the lobby before resetting the URL and clearing other state, ensuring proper event dispatching while the lobby ID is still available.


1185-1191: LGTM - Path-based URL for clipboard

Using buildLobbyUrl() ensures the copied URL uses the new path-based format (/game/{id}?lobby&s=...) with proper cache-busting parameters.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/server/Worker.ts (1)

47-47: Duplicate schema definition.

The gameIDSchema duplicates the ID schema already imported from Schemas.ts. Consider reusing ID directly.

♻️ Suggested simplification
-const gameIDSchema = z.string().regex(GAME_ID_REGEX);
+// Use the imported ID schema directly in validation calls
+// e.g., ID.safeParse(gameID).success instead of gameIDSchema.safeParse(gameID).success
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8555892 and 286ebf4.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • src/core/Schemas.ts
  • src/server/GamePreviewBuilder.ts
  • src/server/Worker.ts
🧰 Additional context used
🧠 Learnings (20)
📓 Common learnings
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/server/GamePreviewBuilder.ts
  • src/server/Worker.ts
  • src/core/Schemas.ts
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/server/GamePreviewBuilder.ts
  • src/server/Worker.ts
📚 Learning: 2025-10-27T09:47:26.395Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:770-795
Timestamp: 2025-10-27T09:47:26.395Z
Learning: In src/core/execution/FakeHumanExecution.ts, the selectSteamrollStopTarget() method intentionally allows MIRV targeting when secondHighest city count is 0 (e.g., nuclear endgame scenarios where structures are destroyed). This is valid game design—"if you can afford it, you're good to go"—and should not be flagged as requiring a guard condition.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/server/GamePreviewBuilder.ts
  • src/server/Worker.ts
📚 Learning: 2025-10-26T15:37:07.732Z
Learnt from: GlacialDrift
Repo: openfrontio/OpenFrontIO PR: 2298
File: src/client/graphics/layers/TerritoryLayer.ts:200-210
Timestamp: 2025-10-26T15:37:07.732Z
Learning: In GameImpl.ts lines 124-139, team assignment logic varies by number of teams: when numPlayerTeams < 8, teams are assigned ColoredTeams values (Red, Blue, Yellow, Green, Purple, Orange, Teal); when numPlayerTeams >= 8, teams are assigned generic string identifiers like "Team 1", "Team 2", etc., which are not members of ColoredTeams.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-20T20:15:28.858Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:51-51
Timestamp: 2025-10-20T20:15:28.858Z
Learning: In src/core/execution/FakeHumanExecution.ts, game balance constants like MIRV_COOLDOWN_TICKS, MIRV_HESITATION_ODDS, VICTORY_DENIAL_TEAM_THRESHOLD, VICTORY_DENIAL_INDIVIDUAL_THRESHOLD, and STEAMROLL_CITY_GAP_MULTIPLIER are experimental tuning parameters subject to frequent change during balance testing. Do not flag changes to these values as issues or compare them against previous values.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-20T11:02:16.969Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:57-57
Timestamp: 2025-10-20T11:02:16.969Z
Learning: In src/core/execution/FakeHumanExecution.ts, the correct MIRV victory denial thresholds are VICTORY_DENIAL_TEAM_THRESHOLD = 0.8 (80% for team games) and VICTORY_DENIAL_INDIVIDUAL_THRESHOLD = 0.65 (65% for individual players), not 0.85 and 0.7 as might be mentioned in some documentation.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as a type inferred from a Zod schema that is marked as optional, which means PlayerStats already includes undefined as a possible type (PlayerStats | undefined).

Applied to files:

  • src/server/GamePreviewBuilder.ts
  • src/core/Schemas.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as `z.infer<typeof PlayerStatsSchema>` where PlayerStatsSchema has `.optional()` applied at the object level, making PlayerStats a union type that already includes undefined (PlayerStats | undefined).

Applied to files:

  • src/server/GamePreviewBuilder.ts
  • src/core/Schemas.ts
📚 Learning: 2025-05-19T06:00:38.007Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:125-134
Timestamp: 2025-05-19T06:00:38.007Z
Learning: In StatsImpl.ts, unused parameters in boat/stats-related methods are intentionally kept for future use and shouldn't be removed.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-21T04:10:59.706Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:44-53
Timestamp: 2025-05-21T04:10:59.706Z
Learning: The PlayerStats type from ArchiveSchemas already includes undefined in its definition, making explicit union types with undefined (PlayerStats | undefined) redundant.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-07-30T19:46:23.797Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1436
File: src/server/Worker.ts:93-93
Timestamp: 2025-07-30T19:46:23.797Z
Learning: Input validation at API boundaries is critical for security. All client-provided data, especially parameters used for authorization decisions like creatorClientID in the kick player functionality, must be validated for type, format, length, and business logic constraints before processing.

Applied to files:

  • src/server/Worker.ts
📚 Learning: 2025-06-13T20:57:49.599Z
Learnt from: El-Magico777
Repo: openfrontio/OpenFrontIO PR: 1166
File: src/core/Schemas.ts:50-56
Timestamp: 2025-06-13T20:57:49.599Z
Learning: In the OpenFrontIO codebase, player identification primarily uses numeric smallIDs throughout the system (e.g., playerBySmallID). When adding new intents or schemas, maintain consistency with this existing pattern rather than introducing mixed ID types, even if string IDs might be more standard elsewhere.

Applied to files:

  • src/server/Worker.ts
📚 Learning: 2025-05-26T09:52:52.465Z
Learnt from: tnhnblgl
Repo: openfrontio/OpenFrontIO PR: 875
File: src/client/index.html:396-402
Timestamp: 2025-05-26T09:52:52.465Z
Learning: In the OpenFrontIO project, the correct path for icons from src/client/index.html is ../../resources/icons/, not /static/icons/ as the build process handles the path mapping differently.

Applied to files:

  • src/server/Worker.ts
🧬 Code graph analysis (1)
src/server/GamePreviewBuilder.ts (4)
src/core/game/GameView.ts (1)
  • gameID (910-912)
src/core/Schemas.ts (1)
  • GameInfo (147-153)
src/core/game/GameImpl.ts (1)
  • map (206-208)
src/core/configuration/DefaultConfig.ts (1)
  • playerTeams (317-319)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (17)
src/server/GamePreviewBuilder.ts (8)

1-56: Clean schema and type definitions.

The Zod schema ExternalGameInfoSchema and the corresponding ExternalGameInfo type are well-structured with appropriate optional fields for external data. Good use of .optional() throughout for handling incomplete data.


65-98: Time formatting helpers look solid.

Good defensive check for non-finite/negative values in formatDuration. The timestamp normalization threshold of 1e12 correctly distinguishes seconds from milliseconds. Using Intl.DateTimeFormat with explicit UTC timezone is a clean approach.


100-128: Winner parsing handles known formats gracefully.

The function correctly handles both ["team", teamName, ...playerIds] and ["player", clientId] formats while returning undefined for unknown structures. This defensive approach avoids confusing output.


130-141: Player counting logic is reasonable.

Filtering by clientID presence to exclude NPCs and checking for non-empty stats object is a sensible heuristic for counting active human players.


143-150: Standard HTML escaping - good for XSS prevention.

The escape order is correct (ampersand first). This covers the essential characters for safe HTML attribute injection. Good that this is exported for reuse in Worker.ts.


152-175: Preview builder entry point is well-structured.

Clean state detection with isFinished and isPrivate flags. The fallback chain for activePlayers handles both finished games and active lobbies appropriately.


176-200: Team mode formatting handles multiple input shapes.

Good handling of playerTeams as either number or string. The team breakdown label logic correctly avoids division by zero since numericTeamCount is only set when > 0.


241-283: Private lobby preview includes detailed game settings.

For private lobbies, the description shows host, game options, and disabled units. This is helpful for embed previews. The public lobby case returns an empty description, which is fine for minimal previews.

One note: Consider whether showing the host username in embeds could be a privacy concern, though usernames are already visible in-game.

src/core/Schemas.ts (2)

134-145: Schema definitions match the interface.

ClientInfoSchema and GameInfoSchema correctly mirror the ClientInfo and GameInfo interfaces. Using z.lazy(() => GameConfigSchema) for the recursive reference is the right approach.


235-240: Centralized game ID validation is a good pattern.

Exporting GAME_ID_REGEX and isValidGameID allows consistent validation across client and server. The 8-character alphanumeric pattern is clear and simple.

src/server/Worker.ts (7)

53-58: Origin helper is straightforward.

Correctly handles x-forwarded-proto header (takes first value if comma-separated) and falls back to config-based host. Good for generating absolute URLs in previews.


60-90: Fetch helper with timeout and validation is well-implemented.

Good use of AbortController for timeout, encodeURIComponent for URL safety, and schema validation before returning. Returning null on failure is appropriate since this data is optional for preview generation.


92-126: Remote lobby fetch correctly avoids self-calls.

The early return when targetWorkerIndex === workerId prevents unnecessary loopback requests. Same solid timeout and validation patterns as the public info fetcher.


187-192: Long-term cache for map assets is appropriate.

Static map thumbnails with 1-year maxAge is reasonable since map images are unlikely to change frequently.


285-308: Game route entry with proper validation and fallback.

Good validation order: check ID format first, then try local game, remote lobby, and public info. The 302 redirect when no data is found is a clean fallback to prevent dead ends.


314-356: HTML injection for social meta tags looks correct.

The approach of removing existing og: and twitter: meta tags before injecting new ones prevents duplicates. Using escapeHtml for all injected content protects against XSS.

One minor point: the file existence check using fsPromises.access followed by fsPromises.readFile has a small TOCTOU window, but this is acceptable for static files that don't change at runtime.


358-365: JSON fallback and error handling are appropriate.

The JSON response uses the replacer utility for consistent serialization. The 500 response on errors is correct, though the error message is user-facing and generic (good).

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @build-deploy.sh:
- Line 60: Update the usage/help text in build.sh to reflect the actual five
positional parameters: DEPLOY_ENV, VERSION_TAG, VERSION_TXT, CHANGELOG_MD, and
METADATA_FILE (in that order) instead of the outdated "[prod|staging]
[version_tag] [metadata_file]" wording; locate the usage strings printed in
build.sh (the existing usage lines around the current help/usage output) and
replace them with a single clear usage line and brief descriptions that list
those five parameter names so callers understand the correct parameter order.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 286ebf4 and 4eb4d93.

📒 Files selected for processing (2)
  • build-deploy.sh
  • resources/version.txt
✅ Files skipped from review due to trivial changes (1)
  • resources/version.txt
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev

Comment thread build-deploy.sh Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
nginx.conf (1)

64-107: Upstream block looks good for load balancing game routes.

The least_conn algorithm is a sensible choice for distributing requests across workers with varying loads. The 41 servers match the existing worker routing configuration.

For production resilience, consider adding explicit failure handling:

upstream workers {
    least_conn;
    server 127.0.0.1:3001 max_fails=3 fail_timeout=10s;
    # ... same for other servers
}

This makes the passive health check behavior explicit and gives you control over failure thresholds.

src/client/Main.ts (1)

750-750: Consider removing redundant modal close call.

The joinModal is already closed in the forEach loop above (lines 705-732) which closes all modals including "join-private-lobby-modal". This explicit call is defensive but redundant.

If you prefer the explicit close for clarity, consider adding a comment explaining why it's separate from the batch close.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4eb4d93 and 7df933e.

📒 Files selected for processing (2)
  • nginx.conf
  • src/client/Main.ts
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, ensure the browser back/forward navigation ordering is correctly handled: when popstate fires before hashchange during a navigation rollback (e.g., user cancels leaving an active game), use a preventHashUpdate-like flag to suppress the hashchange listener (onHashUpdate) from running in that rollback scenario. This avoids applying an unintended hash update after a rollback and keeps the UI/game state consistent. Document the flag’s usage and the exact conditions under which it is set/reset to prevent subtle regressions.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2025-08-17T20:48:49.411Z
Learnt from: woodydrn
Repo: openfrontio/OpenFrontIO PR: 1836
File: src/client/Main.ts:482-482
Timestamp: 2025-08-17T20:48:49.411Z
Learning: In PR #1836, user woodydrn prefers to keep changes minimal and focused on the specific issue (clientID persistence) rather than refactoring redundant code in JoinLobbyEvent dispatchers. They want to avoid scope creep in focused bug fix PRs.

Applied to files:

  • src/client/Main.ts
🧬 Code graph analysis (1)
src/client/Main.ts (1)
src/core/Schemas.ts (1)
  • GAME_ID_REGEX (235-235)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (11)
nginx.conf (2)

269-282: Static maps location configured correctly for embed previews.

The setup serves map files with appropriate long-term caching (versioned via query params as noted) and CORS headers enabling Discord/WhatsApp/X to fetch preview images.

The Access-Control-Allow-Headers "Range" is good for supporting partial content requests on large map files. If you later need additional headers for authenticated requests, you would need to expand this list.


284-297: Game route load balancing looks correct.

The regex ^/(game/|api/game/) correctly matches both /game/<code> and /api/game/<code> paths. WebSocket headers are included for connection upgrades, and caching is properly disabled for dynamic game state.

This enables the path-based routing needed for embedded previews while distributing load across workers.

src/client/Main.ts (9)

4-4: LGTM: Clean import of GAME_ID_REGEX.

The import is straightforward and supports the validation logic used throughout this file.


90-90: LGTM: Event type properly declared.

The "join-changed" event is correctly added to the DocumentEventMap for type safety.


563-563: LGTM: Clean validation with GAME_ID_REGEX.

The replacement of ID.safeParse with GAME_ID_REGEX.test is straightforward and consistent with the PR's migration to regex-based validation.


639-647: LGTM: Path-based URL parsing implemented correctly.

The new logic properly extracts and validates lobby IDs from /game/{id} URLs, aligning with the PR's goal to support embed previews. The query parameters are correctly ignored during ID extraction.


663-663: Verify URL update timing and query parameter handling.

updateJoinUrlForShare is called early in the join flow and uses replaceState to update the URL to /game/{lobbyId} (without query params). Later at line 766, pushState adds a history entry with ?live.

This creates two URL updates:

  1. Line 663: replaceState to /game/{lobbyId} (clean shareable URL)
  2. Line 766: pushState to /game/{lobbyId}?live (game started state)

If a user navigates to /game/{lobbyId}?lobby, the first update would strip the query param. Confirm this is intentional—that the shareable URL should always be clean without query parameters.


710-710: LGTM: UI element cleanup expanded appropriately.

Adding "game-top-bar", "account-button", and "stats-button" to the close list improves the game transition experience by hiding additional UI elements during gameplay.

Also applies to: 717-718


766-766: LGTM: Path-based URL with history management.

The pushState correctly creates a history entry with the path-based URL and ?live cache-busting parameter when the game starts. This works well with the earlier replaceState at line 663 to maintain both a shareable URL and proper browser history.


771-778: LGTM: Clean shareable URL logic.

The method correctly updates the URL to a clean /game/{lobbyId} format using replaceState (no history entry). The pathname-only comparison means query parameters are stripped, which aligns with creating shareable URLs.

Per the learnings, query parameters like ?s=xxxxx are for cache-busting embed previews and are ignored by join logic, so stripping them here maintains a clean shareable URL.

Based on learnings, the query suffix is purely for cache-busting and doesn't affect join logic.


537-537: No changes needed—the event handler is correct.

The "join-changed" event is dispatched from AccountModal.viewGame() when a user clicks a game from the recent games list. At that point:

  1. The URL changes to /game/{gameId}
  2. The event fires to signal this URL change
  3. onHashUpdate() runs to handle the change

This behavior is intentional: onHashUpdate() cleans up existing state (closes modal, leaves current game if needed) and then calls handleUrl() to parse the new URL and open the join modal. The user has not joined yet—they have only selected a game to view. The flow works correctly.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
src/server/Worker.ts (3)

32-32: Duplicate import of GAME_ID_REGEX.

GAME_ID_REGEX is already imported from ../core/Schemas on line 14-22 (via the destructured import block). The separate import on line 32 is redundant.

🔧 Suggested fix
-import { GAME_ID_REGEX } from "../core/Schemas";
-

And add GAME_ID_REGEX to the existing import on lines 14-22:

 import {
   ClientMessageSchema,
   GameID,
   GameInfo,
   GameInfoSchema,
+  GAME_ID_REGEX,
   ID,
   PartialGameRecordSchema,
   ServerErrorMessage,
 } from "../core/Schemas";

186-201: Minor redundancy in webp MIME handling.

Line 188 defines the webp MIME type globally via express.static.mime.define(), but lines 195-198 also set the header explicitly for webp files. This redundancy is harmless but could be simplified.

🔧 Optional cleanup
   app.use(
     "/maps",
     express.static(path.join(__dirname, "../../resources/maps"), {
       maxAge: "1y",
-      setHeaders: (res, filePath) => {
-        if (filePath.endsWith(".webp")) {
-          res.setHeader("Content-Type", "image/webp");
-        }
-      },
     }),
   );

302-317: Consider parallel fetching for better latency.

Currently, fetchRemoteLobbyInfo (line 308) runs before fetchPublicGameInfo (line 312) sequentially. Since both have 1.5s timeouts, worst case is 3s delay. For embed preview generation where latency matters, consider running these in parallel:

⚡ Parallel fetch suggestion
     const game = gm.game(gameID);
 
     let lobby: GameInfo | null = null;
+    let publicInfo: ExternalGameInfo | null = null;
+
     if (game) {
       lobby = game.gameInfo();
     } else {
-      lobby = await fetchRemoteLobbyInfo(gameID);
+      // Fetch remote lobby and public info in parallel
+      [lobby, publicInfo] = await Promise.all([
+        fetchRemoteLobbyInfo(gameID),
+        fetchPublicGameInfo(gameID),
+      ]);
     }
 
-    try {
-      const publicInfo = await fetchPublicGameInfo(gameID);
+    // If we have local game, still fetch public info for additional metadata
+    if (game && !publicInfo) {
+      publicInfo = await fetchPublicGameInfo(gameID);
+    }
nginx.conf (1)

290-303: Routing logic is correct; simplify WebSocket headers if not needed.

The regex ~ ^/(game/|api/game/) routes correctly. nginx evaluates exact matches, then prefix matches with ^~, then regex locations in order. No earlier location blocks (= /api/public_lobbies, = /api/env, ^~ /maps/) would intercept /game/ requests, and this regex comes before the default location / fallback, so routing precedence is sound.

The Upgrade and Connection headers (lines 294–295) enable WebSocket support. If the /game/ endpoint handles only HTTP requests (for embed previews), remove these headers to simplify the configuration. If the game service uses WebSocket for real-time updates, keep them.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7df933e and 217d909.

📒 Files selected for processing (2)
  • nginx.conf
  • src/server/Worker.ts
🧰 Additional context used
🧠 Learnings (7)
📓 Common learnings
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/server/Worker.ts
📚 Learning: 2025-07-30T19:46:23.797Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1436
File: src/server/Worker.ts:93-93
Timestamp: 2025-07-30T19:46:23.797Z
Learning: Input validation at API boundaries is critical for security. All client-provided data, especially parameters used for authorization decisions like creatorClientID in the kick player functionality, must be validated for type, format, length, and business logic constraints before processing.

Applied to files:

  • src/server/Worker.ts
📚 Learning: 2025-06-13T20:57:49.599Z
Learnt from: El-Magico777
Repo: openfrontio/OpenFrontIO PR: 1166
File: src/core/Schemas.ts:50-56
Timestamp: 2025-06-13T20:57:49.599Z
Learning: In the OpenFrontIO codebase, player identification primarily uses numeric smallIDs throughout the system (e.g., playerBySmallID). When adding new intents or schemas, maintain consistency with this existing pattern rather than introducing mixed ID types, even if string IDs might be more standard elsewhere.

Applied to files:

  • src/server/Worker.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/server/Worker.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/server/Worker.ts
📚 Learning: 2025-05-26T09:52:52.465Z
Learnt from: tnhnblgl
Repo: openfrontio/OpenFrontIO PR: 875
File: src/client/index.html:396-402
Timestamp: 2025-05-26T09:52:52.465Z
Learning: In the OpenFrontIO project, the correct path for icons from src/client/index.html is ../../resources/icons/, not /static/icons/ as the build process handles the path mapping differently.

Applied to files:

  • src/server/Worker.ts
🧬 Code graph analysis (1)
src/server/Worker.ts (2)
src/core/Schemas.ts (3)
  • GAME_ID_REGEX (235-235)
  • GameInfo (147-153)
  • GameInfoSchema (139-145)
src/server/GamePreviewBuilder.ts (4)
  • ExternalGameInfo (40-56)
  • ExternalGameInfoSchema (4-32)
  • buildPreview (152-286)
  • escapeHtml (143-150)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (8)
nginx.conf (2)

64-107: LGTM! Upstream block for worker load balancing.

The least_conn strategy is a good choice for distributing game requests across workers evenly. The port range (3001-3041) matches the existing worker configuration pattern.


269-288: LGTM! Static map serving with aggressive caching.

The ^~ prefix ensures this location takes priority over regex-based locations, which is correct for static assets. The 1-year cache with immutable is appropriate for versioned or unchanging map files.

src/server/Worker.ts (6)

53-58: LGTM! Origin detection handles proxy chains correctly.

The comma-split on x-forwarded-proto properly handles multiple proxy hops where headers may be comma-separated.


60-90: Well-structured fetch with proper timeout and validation.

Good patterns here:

  • Input validation before network call
  • AbortController for timeout (1.5s is reasonable for embed previews)
  • Schema validation on response
  • Proper cleanup in finally block

92-126: LGTM! Worker-to-worker communication is well isolated.

The early return on line 98 when targetWorkerIndex === workerId prevents unnecessary self-calls. Using 127.0.0.1 for internal worker communication avoids external network hops.


340-364: LGTM! HTML injection is secure.

Good security practices:

  • escapeHtml() is applied to all user-controlled content in meta tags (title, description, URL, image)
  • Existing og:/twitter: meta tags are removed before injection to prevent duplicates
  • The node-html-parser approach is cleaner than regex-based HTML manipulation

367-374: Consider more specific error messages.

The catch block logs the full error but returns a generic message. For debugging purposes, you might want to include a request ID or correlation ID in the response. However, hiding internal errors from clients is correct for security.


323-323: Content negotiation behavior note.

When req.accepts(["json", "html"]) returns a type other than "html" (including "json" or false), the code falls through to the JSON response. This is reasonable default behavior for API consumers.

Comment thread src/server/Worker.ts Fixed
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @src/server/Worker.ts:
- Around line 53-61: The domain check in requestOrigin uses host.includes(...)
which allows suffix-bypass like evil.openfront.io.attacker.com; change the
forceHttps logic in requestOrigin to perform proper canonical suffix matching:
strip the optional port from req.get("host"), then require either exact match or
endsWith('.openfront.io') / endsWith('.openfront.dev') (or use a safe regex that
enforces a dot before the domain) rather than includes; update the forceHttps
assignment to use this stricter check so only true openfront domains force
https.
🧹 Nitpick comments (2)
src/server/GamePreviewBuilder.ts (1)

40-56: Use z.infer to keep type and schema in sync.

Manual type definition can drift from the schema over time. Using z.infer ensures they stay aligned.

♻️ Suggested change
-export type ExternalGameInfo = {
-  info?: {
-    config?: {
-      gameMap?: string;
-      gameMode?: string;
-      gameType?: string;
-      maxPlayers?: number;
-      playerTeams?: number | string;
-    };
-    players?: PlayerInfo[];
-    winner?: string[];
-    duration?: number;
-    start?: number;
-    end?: number;
-    lobbyCreatedAt?: number;
-  };
-};
+export type ExternalGameInfo = z.infer<typeof ExternalGameInfoSchema>;
src/server/Worker.ts (1)

326-340: Simplify file reading with try-read pattern.

Instead of check-then-read, try reading directly and catch ENOENT. This avoids the TOCTOU pattern and is cleaner.

♻️ Suggested simplification
-      const staticHtml = path.join(__dirname, "../../static/index.html");
-      const rootHtml = path.join(__dirname, "../../index.html");
-      let filePath: string | null = null;
-
-      try {
-        await fsPromises.access(staticHtml);
-        filePath = staticHtml;
-      } catch {
-        try {
-          await fsPromises.access(rootHtml);
-          filePath = rootHtml;
-        } catch {
-          // Neither file exists
-        }
-      }
-
-      if (filePath) {
-        const html = await fsPromises.readFile(filePath, "utf-8");
+      const staticHtml = path.join(__dirname, "../../static/index.html");
+      const rootHtml = path.join(__dirname, "../../index.html");
+      let html: string | null = null;
+
+      for (const filePath of [staticHtml, rootHtml]) {
+        try {
+          html = await fsPromises.readFile(filePath, "utf-8");
+          break;
+        } catch {
+          // Try next path
+        }
+      }
+
+      if (html) {
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 217d909 and dfdf0ca.

📒 Files selected for processing (2)
  • src/server/GamePreviewBuilder.ts
  • src/server/Worker.ts
🧰 Additional context used
🧠 Learnings (20)
📓 Common learnings
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, when the browser back button is pressed, the `popstate` event fires before the `hashchange` event. The `preventHashUpdate` flag is used to prevent the `hashchange` listener (`onHashUpdate`) from executing after a navigation rollback in the `popstate` listener (`onPopState`), specifically when the user cancels leaving an active game.
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/server/GamePreviewBuilder.ts
  • src/server/Worker.ts
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/server/GamePreviewBuilder.ts
  • src/server/Worker.ts
📚 Learning: 2025-10-27T09:47:26.395Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:770-795
Timestamp: 2025-10-27T09:47:26.395Z
Learning: In src/core/execution/FakeHumanExecution.ts, the selectSteamrollStopTarget() method intentionally allows MIRV targeting when secondHighest city count is 0 (e.g., nuclear endgame scenarios where structures are destroyed). This is valid game design—"if you can afford it, you're good to go"—and should not be flagged as requiring a guard condition.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-20T20:15:28.858Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:51-51
Timestamp: 2025-10-20T20:15:28.858Z
Learning: In src/core/execution/FakeHumanExecution.ts, game balance constants like MIRV_COOLDOWN_TICKS, MIRV_HESITATION_ODDS, VICTORY_DENIAL_TEAM_THRESHOLD, VICTORY_DENIAL_INDIVIDUAL_THRESHOLD, and STEAMROLL_CITY_GAP_MULTIPLIER are experimental tuning parameters subject to frequent change during balance testing. Do not flag changes to these values as issues or compare them against previous values.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/server/GamePreviewBuilder.ts
  • src/server/Worker.ts
📚 Learning: 2025-10-26T15:37:07.732Z
Learnt from: GlacialDrift
Repo: openfrontio/OpenFrontIO PR: 2298
File: src/client/graphics/layers/TerritoryLayer.ts:200-210
Timestamp: 2025-10-26T15:37:07.732Z
Learning: In GameImpl.ts lines 124-139, team assignment logic varies by number of teams: when numPlayerTeams < 8, teams are assigned ColoredTeams values (Red, Blue, Yellow, Green, Purple, Orange, Teal); when numPlayerTeams >= 8, teams are assigned generic string identifiers like "Team 1", "Team 2", etc., which are not members of ColoredTeams.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-20T11:02:16.969Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:57-57
Timestamp: 2025-10-20T11:02:16.969Z
Learning: In src/core/execution/FakeHumanExecution.ts, the correct MIRV victory denial thresholds are VICTORY_DENIAL_TEAM_THRESHOLD = 0.8 (80% for team games) and VICTORY_DENIAL_INDIVIDUAL_THRESHOLD = 0.65 (65% for individual players), not 0.85 and 0.7 as might be mentioned in some documentation.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as a type inferred from a Zod schema that is marked as optional, which means PlayerStats already includes undefined as a possible type (PlayerStats | undefined).

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as `z.infer<typeof PlayerStatsSchema>` where PlayerStatsSchema has `.optional()` applied at the object level, making PlayerStats a union type that already includes undefined (PlayerStats | undefined).

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-19T06:00:38.007Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:125-134
Timestamp: 2025-05-19T06:00:38.007Z
Learning: In StatsImpl.ts, unused parameters in boat/stats-related methods are intentionally kept for future use and shouldn't be removed.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-21T04:10:59.706Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:44-53
Timestamp: 2025-05-21T04:10:59.706Z
Learning: The PlayerStats type from ArchiveSchemas already includes undefined in its definition, making explicit union types with undefined (PlayerStats | undefined) redundant.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-07-30T19:46:23.797Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1436
File: src/server/Worker.ts:93-93
Timestamp: 2025-07-30T19:46:23.797Z
Learning: Input validation at API boundaries is critical for security. All client-provided data, especially parameters used for authorization decisions like creatorClientID in the kick player functionality, must be validated for type, format, length, and business logic constraints before processing.

Applied to files:

  • src/server/Worker.ts
📚 Learning: 2025-06-13T20:57:49.599Z
Learnt from: El-Magico777
Repo: openfrontio/OpenFrontIO PR: 1166
File: src/core/Schemas.ts:50-56
Timestamp: 2025-06-13T20:57:49.599Z
Learning: In the OpenFrontIO codebase, player identification primarily uses numeric smallIDs throughout the system (e.g., playerBySmallID). When adding new intents or schemas, maintain consistency with this existing pattern rather than introducing mixed ID types, even if string IDs might be more standard elsewhere.

Applied to files:

  • src/server/Worker.ts
📚 Learning: 2025-05-26T09:52:52.465Z
Learnt from: tnhnblgl
Repo: openfrontio/OpenFrontIO PR: 875
File: src/client/index.html:396-402
Timestamp: 2025-05-26T09:52:52.465Z
Learning: In the OpenFrontIO project, the correct path for icons from src/client/index.html is ../../resources/icons/, not /static/icons/ as the build process handles the path mapping differently.

Applied to files:

  • src/server/Worker.ts
🧬 Code graph analysis (2)
src/server/GamePreviewBuilder.ts (4)
src/core/game/GameView.ts (1)
  • gameID (910-912)
src/core/Schemas.ts (1)
  • GameInfo (147-153)
src/core/game/GameImpl.ts (1)
  • map (206-208)
src/core/configuration/DefaultConfig.ts (1)
  • playerTeams (317-319)
src/server/Worker.ts (2)
src/core/Schemas.ts (3)
  • GAME_ID_REGEX (235-235)
  • GameInfo (147-153)
  • GameInfoSchema (139-145)
src/server/GamePreviewBuilder.ts (4)
  • ExternalGameInfo (40-56)
  • ExternalGameInfoSchema (4-32)
  • buildPreview (152-289)
  • escapeHtml (143-150)
🪛 GitHub Check: CodeQL
src/server/Worker.ts

[failure] 59-59: Incomplete URL substring sanitization
'openfront.io' can be anywhere in the URL, and arbitrary hosts may come before or after it.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (10)
src/server/GamePreviewBuilder.ts (5)

1-32: LGTM!

Schema is well-structured for validating external API responses. The use of .optional() throughout handles partial data gracefully.


65-98: LGTM!

Helper functions are clean, handle edge cases well, and use proper i18n formatting with Intl.DateTimeFormat.


100-141: LGTM!

Winner parsing handles both team and individual formats correctly. Player counting properly excludes bots by checking for clientID.


143-150: LGTM!

Standard HTML escaping with correct order (ampersand first). Covers all necessary special characters.


152-288: LGTM!

The function handles all game states (finished, private, public) with appropriate fallbacks. Data prioritization (live over archived) is correct, and the preview metadata is comprehensive for embed platforms.

src/server/Worker.ts (5)

4-43: LGTM!

Imports are well-organized and all are used. Good separation between schema validation (Schemas) and preview building (GamePreviewBuilder).


63-93: LGTM!

Good defensive implementation: validates input before request, uses timeout with AbortController, validates response with schema, and logs issues with context. Timeout cleanup in finally block is correct.


95-129: LGTM!

Smart optimization to skip self-fetch when this worker hosts the game. Uses loopback for secure inter-worker calls. Same solid timeout and validation pattern as fetchPublicGameInfo.


189-204: LGTM!

Good cache configuration for map assets (1 year). Explicit webp Content-Type handling ensures proper serving across different environments.


297-375: Clean implementation of preview route.

The fallback chain (local game → remote worker → archived data → redirect) handles all cases well. Meta tag injection is correctly escaped. One small note: the JSON fallback on line 370 might confuse crawlers that expect HTML, but this is an edge case when the HTML template is missing entirely.

Comment thread src/server/Worker.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
src/server/GamePreviewBuilder.ts (3)

4-56: Schema and type are defined separately - consider using z.infer.

The ExternalGameInfo type (lines 40-56) duplicates the structure of ExternalGameInfoSchema. You could derive the type from the schema to keep them in sync:

export type ExternalGameInfo = z.infer<typeof ExternalGameInfoSchema>;

This is a minor readability preference since the explicit type is also valid and can serve as documentation.


65-74: Float seconds may produce odd output.

If seconds is a float (e.g., 65.7), secs will be 5.7 and output will be "1m 5.7s". Consider flooring the seconds value for cleaner display:

Suggested fix
 function formatDuration(seconds: number): string {
   if (!Number.isFinite(seconds) || seconds < 0) return "Unknown";
+  seconds = Math.floor(seconds);
   const mins = Math.floor(seconds / 60);
   const secs = seconds % 60;

184-191: Team breakdown label edge case when activePlayers is 0.

When activePlayers is 0 and numericTeamCount is set, the label becomes "X teams of 1" due to Math.max(1, ...). This might be misleading for an empty lobby. Consider showing just "X teams" without the "of Y" part when activePlayers is 0:

Optional refinement
   const teamBreakdownLabel = numericTeamCount
     ? isFinished
-      ? `${numericTeamCount} teams of ${Math.max(
-          1,
-          Math.ceil(activePlayers / numericTeamCount),
-        )}`
+      ? activePlayers > 0
+        ? `${numericTeamCount} teams of ${Math.ceil(activePlayers / numericTeamCount)}`
+        : `${numericTeamCount} teams`
       : `${numericTeamCount} teams`
     : undefined;
src/server/Worker.ts (2)

14-32: GAME_ID_REGEX import could be consolidated.

GAME_ID_REGEX is imported separately on line 32 from the same module imported on lines 14-22. Consider consolidating:

 import {
   ClientMessageSchema,
   GameID,
   GameInfo,
   GameInfoSchema,
+  GAME_ID_REGEX,
   ID,
   PartialGameRecordSchema,
   ServerErrorMessage,
 } from "../core/Schemas";
-
-import { GAME_ID_REGEX } from "../core/Schemas";

330-344: File existence check could be simplified.

The nested try-catch for checking two file paths is verbose. Consider a helper:

Suggested simplification
+const findFirstExisting = async (...paths: string[]): Promise<string | null> => {
+  for (const p of paths) {
+    try {
+      await fsPromises.access(p);
+      return p;
+    } catch {
+      // continue to next path
+    }
+  }
+  return null;
+};

// Then in the route handler:
-      const staticHtml = path.join(__dirname, "../../static/index.html");
-      const rootHtml = path.join(__dirname, "../../index.html");
-      let filePath: string | null = null;
-
-      try {
-        await fsPromises.access(staticHtml);
-        filePath = staticHtml;
-      } catch {
-        try {
-          await fsPromises.access(rootHtml);
-          filePath = rootHtml;
-        } catch {
-          // Neither file exists
-        }
-      }
+      const filePath = await findFirstExisting(
+        path.join(__dirname, "../../static/index.html"),
+        path.join(__dirname, "../../index.html"),
+      );
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dfdf0ca and f3313c6.

📒 Files selected for processing (2)
  • src/server/GamePreviewBuilder.ts
  • src/server/Worker.ts
🧰 Additional context used
🧠 Learnings (21)
📓 Common learnings
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/server/Worker.ts
  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-07-30T19:46:23.797Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1436
File: src/server/Worker.ts:93-93
Timestamp: 2025-07-30T19:46:23.797Z
Learning: Input validation at API boundaries is critical for security. All client-provided data, especially parameters used for authorization decisions like creatorClientID in the kick player functionality, must be validated for type, format, length, and business logic constraints before processing.

Applied to files:

  • src/server/Worker.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/server/Worker.ts
  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-06-13T20:57:49.599Z
Learnt from: El-Magico777
Repo: openfrontio/OpenFrontIO PR: 1166
File: src/core/Schemas.ts:50-56
Timestamp: 2025-06-13T20:57:49.599Z
Learning: In the OpenFrontIO codebase, player identification primarily uses numeric smallIDs throughout the system (e.g., playerBySmallID). When adding new intents or schemas, maintain consistency with this existing pattern rather than introducing mixed ID types, even if string IDs might be more standard elsewhere.

Applied to files:

  • src/server/Worker.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/server/Worker.ts
  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-26T09:52:52.465Z
Learnt from: tnhnblgl
Repo: openfrontio/OpenFrontIO PR: 875
File: src/client/index.html:396-402
Timestamp: 2025-05-26T09:52:52.465Z
Learning: In the OpenFrontIO project, the correct path for icons from src/client/index.html is ../../resources/icons/, not /static/icons/ as the build process handles the path mapping differently.

Applied to files:

  • src/server/Worker.ts
📚 Learning: 2025-07-19T18:12:24.553Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1417
File: src/client/index.html:335-350
Timestamp: 2025-07-19T18:12:24.553Z
Learning: For the OpenFrontIO project: rel="noopener" is not required for internal links (like /privacy-policy.html and /terms-of-service.html) since the project controls the destination pages. The security concern only applies to external, untrusted links.

Applied to files:

  • src/server/Worker.ts
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-27T09:47:26.395Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:770-795
Timestamp: 2025-10-27T09:47:26.395Z
Learning: In src/core/execution/FakeHumanExecution.ts, the selectSteamrollStopTarget() method intentionally allows MIRV targeting when secondHighest city count is 0 (e.g., nuclear endgame scenarios where structures are destroyed). This is valid game design—"if you can afford it, you're good to go"—and should not be flagged as requiring a guard condition.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-20T20:15:28.858Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:51-51
Timestamp: 2025-10-20T20:15:28.858Z
Learning: In src/core/execution/FakeHumanExecution.ts, game balance constants like MIRV_COOLDOWN_TICKS, MIRV_HESITATION_ODDS, VICTORY_DENIAL_TEAM_THRESHOLD, VICTORY_DENIAL_INDIVIDUAL_THRESHOLD, and STEAMROLL_CITY_GAP_MULTIPLIER are experimental tuning parameters subject to frequent change during balance testing. Do not flag changes to these values as issues or compare them against previous values.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-26T15:37:07.732Z
Learnt from: GlacialDrift
Repo: openfrontio/OpenFrontIO PR: 2298
File: src/client/graphics/layers/TerritoryLayer.ts:200-210
Timestamp: 2025-10-26T15:37:07.732Z
Learning: In GameImpl.ts lines 124-139, team assignment logic varies by number of teams: when numPlayerTeams < 8, teams are assigned ColoredTeams values (Red, Blue, Yellow, Green, Purple, Orange, Teal); when numPlayerTeams >= 8, teams are assigned generic string identifiers like "Team 1", "Team 2", etc., which are not members of ColoredTeams.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-20T11:02:16.969Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:57-57
Timestamp: 2025-10-20T11:02:16.969Z
Learning: In src/core/execution/FakeHumanExecution.ts, the correct MIRV victory denial thresholds are VICTORY_DENIAL_TEAM_THRESHOLD = 0.8 (80% for team games) and VICTORY_DENIAL_INDIVIDUAL_THRESHOLD = 0.65 (65% for individual players), not 0.85 and 0.7 as might be mentioned in some documentation.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as a type inferred from a Zod schema that is marked as optional, which means PlayerStats already includes undefined as a possible type (PlayerStats | undefined).

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as `z.infer<typeof PlayerStatsSchema>` where PlayerStatsSchema has `.optional()` applied at the object level, making PlayerStats a union type that already includes undefined (PlayerStats | undefined).

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-19T06:00:38.007Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:125-134
Timestamp: 2025-05-19T06:00:38.007Z
Learning: In StatsImpl.ts, unused parameters in boat/stats-related methods are intentionally kept for future use and shouldn't be removed.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-21T04:10:59.706Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:44-53
Timestamp: 2025-05-21T04:10:59.706Z
Learning: The PlayerStats type from ArchiveSchemas already includes undefined in its definition, making explicit union types with undefined (PlayerStats | undefined) redundant.

Applied to files:

  • src/server/GamePreviewBuilder.ts
🧬 Code graph analysis (2)
src/server/Worker.ts (2)
src/core/Schemas.ts (3)
  • GAME_ID_REGEX (235-235)
  • GameInfo (147-153)
  • GameInfoSchema (139-145)
src/server/GamePreviewBuilder.ts (4)
  • ExternalGameInfo (40-56)
  • ExternalGameInfoSchema (4-32)
  • buildPreview (152-289)
  • escapeHtml (143-150)
src/server/GamePreviewBuilder.ts (4)
src/core/game/GameView.ts (1)
  • gameID (910-912)
src/core/Schemas.ts (1)
  • GameInfo (147-153)
src/core/game/GameImpl.ts (1)
  • map (206-208)
src/core/configuration/DefaultConfig.ts (1)
  • playerTeams (317-319)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (6)
src/server/GamePreviewBuilder.ts (1)

152-289: Clean preview builder with good fallback handling.

The function handles multiple game states (finished, private lobby, public lobby) with sensible defaults. The use of optional chaining and nullish coalescing keeps the code readable. The thumbnail URL normalization and escaping are appropriate.

src/server/Worker.ts (5)

53-65: Good origin detection with HTTPS forcing.

The helper correctly handles proxy headers, extracts the first value from comma-separated x-forwarded-proto, and forces HTTPS on production domains. This is solid defensive coding for running behind load balancers.


67-97: Clean fetch helper with proper timeout and validation.

Good practices here:

  • Validates input before making the request
  • Uses AbortController for timeout management
  • Validates response against schema
  • Logs warnings with context on failures
  • Cleans up timeout in finally block

99-133: LGTM - consistent fetch pattern for inter-worker communication.

The helper mirrors fetchPublicGameInfo structure with appropriate differences:

  • Skips fetch if targeting same worker
  • Uses localhost for inter-worker calls
  • Validates against GameInfoSchema for live lobby data

193-208: Good static asset configuration with long caching.

The 1-year cache for map thumbnails is appropriate for static assets. Explicitly setting Content-Type for .webp files ensures correct MIME type regardless of server configuration.


301-379: Well-structured route with proper validation and escaping.

The /game/:id route correctly:

  • Validates the gameID format before processing
  • Fetches from local game manager first, then remote workers, then public API
  • Redirects to home if no data is available (good UX)
  • Escapes all user-derived content before injecting into HTML
  • Falls back to JSON if HTML templating fails

The meta tag injection enables proper embed previews on Discord/WhatsApp/X as intended by the PR.

Comment thread src/server/Worker.ts Outdated
Comment thread src/server/Worker.ts Outdated
Comment thread src/server/Worker.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/client/HostLobbyModal.ts (2)

19-25: Reset lobbyUrlSuffix on close (state leak across modal opens).

You reset most transient state in onClose(), but not lobbyUrlSuffix, so the next open can briefly reuse an old suffix until constructUrl() runs. Add this.lobbyUrlSuffix = ""; in the reset block. (Also matches the “suffix is non-functional / cache-busting only” invariant from learnings.)

Also applies to: 64-65


850-871: Add .catch() to createLobby(...).then(...) chain (avoid unhandled rejection + stuck modal).

You throw on invalid ID format; without a .catch(), this becomes an unhandled promise rejection and the modal may remain open in a broken state.

Proposed fix (minimal catch)
 createLobby(this.lobbyCreatorClientID)
   .then(async (lobby) => {
     this.lobbyId = lobby.gameID;
     if (!isValidGameID(this.lobbyId)) {
       throw new Error(`Invalid lobby ID format: ${this.lobbyId}`);
     }
     crazyGamesSDK.showInviteButton(this.lobbyId);
     const url = await this.constructUrl();
     this.updateHistory(url);
   })
   .then(() => {
     this.dispatchEvent(
       new CustomEvent("join-lobby", {
         detail: {
           gameID: this.lobbyId,
           clientID: this.lobbyCreatorClientID,
         } as JoinLobbyEvent,
         bubbles: true,
         composed: true,
       }),
     );
-  });
+  })
+  .catch((err) => {
+    console.error("Failed to create lobby:", err);
+    this.close();
+  });
🤖 Fix all issues with AI agents
In @src/client/HostLobbyModal.ts:
- Around line 928-933: In HostLobbyModal.onClose replace the hardcoded
updateHistory("/") call with updateHistory(window.location.origin + "/") to
preserve sub-path deployments (matching JoinPrivateLobbyModal behavior), and
reset modal state by setting this.lobbyUrlSuffix = "" before returning; keep the
existing leaveLobby() and crazyGamesSDK.hideInviteButton() calls intact to
ensure cleanup.
🧹 Nitpick comments (6)
src/client/AccountModal.ts (1)

355-365: Consider adding error handling for the config fetch.

If getServerConfigFromClient() fails (e.g., network issues), the error will bubble up silently after the modal has already closed. The user sees the modal close but nothing happens.

Proposed fix
 private async viewGame(gameId: string): Promise<void> {
   this.close();
+  try {
     const config = await getServerConfigFromClient();
     const encodedGameId = encodeURIComponent(gameId);
     const newUrl = `/${config.workerPath(gameId)}/game/${encodedGameId}`;

     history.pushState({ join: gameId }, "", newUrl);
     window.dispatchEvent(
       new CustomEvent("join-changed", { detail: { gameId: encodedGameId } }),
     );
+  } catch (err) {
+    console.error("Failed to navigate to game:", err);
+    // Optionally re-open modal or show error message
+  }
 }
src/client/Main.ts (1)

777-787: LGTM!

Method correctly uses replaceState to update the URL for sharing without adding a history entry. The comparison before update avoids unnecessary state changes.

Minor note: The type Awaited<ReturnType<typeof getServerConfigFromClient>> is verbose. If this pattern is used elsewhere, a type alias like ServerConfig from the import could simplify it.

src/client/HostLobbyModal.ts (2)

117-124: Async race: putGameConfig() can update history out of order.

putGameConfig() awaits constructUrl() (which mutates this.lobbyUrlSuffix) and then calls replaceState. If multiple config updates happen close together, earlier calls can resolve later and “win” the URL.

Low-risk functionally (suffix is cache-busting), but it can make the URL feel unstable and can reduce the “latest config got a fresh URL” guarantee. Consider a “latest-call-wins” guard:

  • increment a counter/token per putGameConfig() call
  • only updateHistory() if token still matches

Also applies to: 1108-1114


1186-1193: Copy action should no-op (or show error) until lobbyId is valid.

Right now copyToClipboard() will attempt to build/copy a URL even if lobbyId is still empty/invalid (fast click). Add a small guard (or disable the button until lobby created).

src/server/GamePreviewBuilder.ts (2)

130-141: Consider checking for arrays in stats validation.

The current logic counts any object with properties, but if stats is an empty array, Object.keys([]).length returns 0 (correct). However, if stats is a non-empty array like [null], it would be counted as an active player. Consider adding Array.isArray(p.stats) check if array stats should be excluded.

♻️ Potential enhancement if arrays should be excluded
-    if (typeof p.stats === "object") {
+    if (typeof p.stats === "object" && !Array.isArray(p.stats)) {
       return Object.keys(p.stats as Record<string, unknown>).length > 0;
     }

245-290: LGTM with optional enhancement suggestion.

Lobby description generation is comprehensive with proper handling of private and public lobbies.

Line 276 uses String() to convert disabled units, which may display internal enum values rather than user-friendly names. If disabled units should show labels like "MIRV Missile" instead of enum values, consider adding a unit name mapping function.

♻️ Optional enhancement for disabled units display

Add a helper to map unit IDs to display names:

// At the top of the file or imported from a shared module
const UNIT_DISPLAY_NAMES: Record<string, string> = {
  // Add your unit mappings here
  // Example: 'UNIT_MIRV': 'MIRV Missile'
};

function formatUnitName(unit: unknown): string {
  const unitStr = String(unit);
  return UNIT_DISPLAY_NAMES[unitStr] ?? unitStr;
}

Then use it in line 276:

-          `Disabled Units: ${gc.disabledUnits.map(String).join(" | ")}`,
+          `Disabled Units: ${gc.disabledUnits.map(formatUnitName).join(" | ")}`,
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3313c6 and d72e81d.

📒 Files selected for processing (7)
  • nginx.conf
  • src/client/AccountModal.ts
  • src/client/HostLobbyModal.ts
  • src/client/Main.ts
  • src/server/GamePreviewBuilder.ts
  • src/server/GamePreviewRoute.ts
  • src/server/Worker.ts
🧰 Additional context used
🧠 Learnings (25)
📓 Common learnings
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/AccountModal.ts
  • src/server/GamePreviewBuilder.ts
  • src/client/HostLobbyModal.ts
  • src/server/Worker.ts
  • src/client/Main.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/client/AccountModal.ts
  • src/server/GamePreviewBuilder.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, when the browser back button is pressed, the `popstate` event fires before the `hashchange` event. The `preventHashUpdate` flag is used to prevent the `hashchange` listener (`onHashUpdate`) from executing after a navigation rollback in the `popstate` listener (`onPopState`), specifically when the user cancels leaving an active game.

Applied to files:

  • src/client/AccountModal.ts
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/client/AccountModal.ts
  • src/server/GamePreviewBuilder.ts
  • src/client/Main.ts
📚 Learning: 2025-08-17T20:48:49.411Z
Learnt from: woodydrn
Repo: openfrontio/OpenFrontIO PR: 1836
File: src/client/Main.ts:482-482
Timestamp: 2025-08-17T20:48:49.411Z
Learning: In PR #1836, user woodydrn prefers to keep changes minimal and focused on the specific issue (clientID persistence) rather than refactoring redundant code in JoinLobbyEvent dispatchers. They want to avoid scope creep in focused bug fix PRs.

Applied to files:

  • src/client/AccountModal.ts
  • src/client/HostLobbyModal.ts
  • src/client/Main.ts
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/server/GamePreviewBuilder.ts
  • src/server/Worker.ts
  • src/client/Main.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.

Applied to files:

  • src/server/GamePreviewBuilder.ts
  • src/client/HostLobbyModal.ts
📚 Learning: 2025-12-13T14:58:29.645Z
Learnt from: scamiv
Repo: openfrontio/OpenFrontIO PR: 2607
File: src/core/execution/PlayerExecution.ts:271-295
Timestamp: 2025-12-13T14:58:29.645Z
Learning: In src/core/execution/PlayerExecution.ts surroundedBySamePlayer(), the `as Player` cast on `mg.playerBySmallID(scan.enemyId)` is intentional. Since scan.enemyId comes from ownerID() on an owned tile and playerBySmallID() only returns Player or undefined, the cast expresses a known invariant. The maintainers prefer loud failures (runtime errors) over silent masking (early returns with guards) for corrupted game state scenarios at trusted call sites.

Applied to files:

  • src/server/GamePreviewBuilder.ts
  • src/server/Worker.ts
📚 Learning: 2025-10-27T09:47:26.395Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:770-795
Timestamp: 2025-10-27T09:47:26.395Z
Learning: In src/core/execution/FakeHumanExecution.ts, the selectSteamrollStopTarget() method intentionally allows MIRV targeting when secondHighest city count is 0 (e.g., nuclear endgame scenarios where structures are destroyed). This is valid game design—"if you can afford it, you're good to go"—and should not be flagged as requiring a guard condition.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-20T20:15:28.858Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:51-51
Timestamp: 2025-10-20T20:15:28.858Z
Learning: In src/core/execution/FakeHumanExecution.ts, game balance constants like MIRV_COOLDOWN_TICKS, MIRV_HESITATION_ODDS, VICTORY_DENIAL_TEAM_THRESHOLD, VICTORY_DENIAL_INDIVIDUAL_THRESHOLD, and STEAMROLL_CITY_GAP_MULTIPLIER are experimental tuning parameters subject to frequent change during balance testing. Do not flag changes to these values as issues or compare them against previous values.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-26T15:37:07.732Z
Learnt from: GlacialDrift
Repo: openfrontio/OpenFrontIO PR: 2298
File: src/client/graphics/layers/TerritoryLayer.ts:200-210
Timestamp: 2025-10-26T15:37:07.732Z
Learning: In GameImpl.ts lines 124-139, team assignment logic varies by number of teams: when numPlayerTeams < 8, teams are assigned ColoredTeams values (Red, Blue, Yellow, Green, Purple, Orange, Teal); when numPlayerTeams >= 8, teams are assigned generic string identifiers like "Team 1", "Team 2", etc., which are not members of ColoredTeams.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-20T11:02:16.969Z
Learnt from: sambokai
Repo: openfrontio/OpenFrontIO PR: 2225
File: src/core/execution/FakeHumanExecution.ts:57-57
Timestamp: 2025-10-20T11:02:16.969Z
Learning: In src/core/execution/FakeHumanExecution.ts, the correct MIRV victory denial thresholds are VICTORY_DENIAL_TEAM_THRESHOLD = 0.8 (80% for team games) and VICTORY_DENIAL_INDIVIDUAL_THRESHOLD = 0.65 (65% for individual players), not 0.85 and 0.7 as might be mentioned in some documentation.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.

Applied to files:

  • src/server/GamePreviewBuilder.ts
  • src/client/HostLobbyModal.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as a type inferred from a Zod schema that is marked as optional, which means PlayerStats already includes undefined as a possible type (PlayerStats | undefined).

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-21T04:10:33.435Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:34-38
Timestamp: 2025-05-21T04:10:33.435Z
Learning: In the codebase, PlayerStats is defined as `z.infer<typeof PlayerStatsSchema>` where PlayerStatsSchema has `.optional()` applied at the object level, making PlayerStats a union type that already includes undefined (PlayerStats | undefined).

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-19T06:00:38.007Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:125-134
Timestamp: 2025-05-19T06:00:38.007Z
Learning: In StatsImpl.ts, unused parameters in boat/stats-related methods are intentionally kept for future use and shouldn't be removed.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-06-16T03:03:59.778Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1192
File: src/client/graphics/layers/RadialMenuElements.ts:312-314
Timestamp: 2025-06-16T03:03:59.778Z
Learning: In RadialMenuElements.ts, when fixing undefined params errors in subMenu functions, use explicit checks like `if (params === undefined || params.selected === null)` rather than optional chaining, as it makes the intent clearer and matches the specific error scenarios where params can be undefined during spawn phase operations.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2025-05-21T04:10:59.706Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 784
File: src/core/game/StatsImpl.ts:44-53
Timestamp: 2025-05-21T04:10:59.706Z
Learning: The PlayerStats type from ArchiveSchemas already includes undefined in its definition, making explicit union types with undefined (PlayerStats | undefined) redundant.

Applied to files:

  • src/server/GamePreviewBuilder.ts
📚 Learning: 2026-01-02T18:11:06.832Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:06.832Z
Learning: In src/client/HostLobbyModal.ts, the ?s=xxxxx URL suffix used in lobby URLs is purely for cache-busting platform previews (e.g., Discord, WhatsApp, x.com) and is not used by the join logic. The join flow ignores the suffix value, so regenerating it via updateUrlWithSuffix() on configuration changes will not break existing shared URLs; it only prompts platforms to refresh preview metadata. Treat the suffix as non-functional for join behavior and ensure any related changes preserve that invariant.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-07-30T19:46:23.797Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1436
File: src/server/Worker.ts:93-93
Timestamp: 2025-07-30T19:46:23.797Z
Learning: Input validation at API boundaries is critical for security. All client-provided data, especially parameters used for authorization decisions like creatorClientID in the kick player functionality, must be validated for type, format, length, and business logic constraints before processing.

Applied to files:

  • src/client/HostLobbyModal.ts
  • src/server/Worker.ts
📚 Learning: 2025-06-13T20:57:49.599Z
Learnt from: El-Magico777
Repo: openfrontio/OpenFrontIO PR: 1166
File: src/core/Schemas.ts:50-56
Timestamp: 2025-06-13T20:57:49.599Z
Learning: In the OpenFrontIO codebase, player identification primarily uses numeric smallIDs throughout the system (e.g., playerBySmallID). When adding new intents or schemas, maintain consistency with this existing pattern rather than introducing mixed ID types, even if string IDs might be more standard elsewhere.

Applied to files:

  • src/server/Worker.ts
📚 Learning: 2025-05-26T09:52:52.465Z
Learnt from: tnhnblgl
Repo: openfrontio/OpenFrontIO PR: 875
File: src/client/index.html:396-402
Timestamp: 2025-05-26T09:52:52.465Z
Learning: In the OpenFrontIO project, the correct path for icons from src/client/index.html is ../../resources/icons/, not /static/icons/ as the build process handles the path mapping differently.

Applied to files:

  • src/server/Worker.ts
📚 Learning: 2025-07-19T18:12:24.553Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1417
File: src/client/index.html:335-350
Timestamp: 2025-07-19T18:12:24.553Z
Learning: For the OpenFrontIO project: rel="noopener" is not required for internal links (like /privacy-policy.html and /terms-of-service.html) since the project controls the destination pages. The security concern only applies to external, untrusted links.

Applied to files:

  • src/server/Worker.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, ensure the browser back/forward navigation ordering is correctly handled: when popstate fires before hashchange during a navigation rollback (e.g., user cancels leaving an active game), use a preventHashUpdate-like flag to suppress the hashchange listener (onHashUpdate) from running in that rollback scenario. This avoids applying an unintended hash update after a rollback and keeps the UI/game state consistent. Document the flag’s usage and the exact conditions under which it is set/reset to prevent subtle regressions.

Applied to files:

  • src/client/Main.ts
🧬 Code graph analysis (6)
src/client/AccountModal.ts (1)
src/core/configuration/ConfigLoader.ts (1)
  • getServerConfigFromClient (29-46)
src/server/GamePreviewRoute.ts (4)
src/core/configuration/Config.ts (1)
  • ServerConfig (29-63)
src/core/Schemas.ts (3)
  • GAME_ID_REGEX (235-235)
  • GameInfo (147-153)
  • GameInfoSchema (139-145)
src/server/GamePreviewBuilder.ts (4)
  • ExternalGameInfo (40-56)
  • ExternalGameInfoSchema (4-32)
  • buildPreview (152-290)
  • escapeHtml (143-150)
src/core/Util.ts (1)
  • replacer (329-331)
src/server/GamePreviewBuilder.ts (4)
src/core/game/GameView.ts (1)
  • gameID (910-912)
src/core/Schemas.ts (1)
  • GameInfo (147-153)
src/core/game/GameImpl.ts (1)
  • map (206-208)
src/core/configuration/DefaultConfig.ts (1)
  • playerTeams (317-319)
src/client/HostLobbyModal.ts (6)
src/core/game/GameImpl.ts (1)
  • config (345-347)
src/core/game/GameView.ts (1)
  • config (783-785)
src/core/configuration/ConfigLoader.ts (1)
  • getServerConfigFromClient (29-46)
src/core/Schemas.ts (1)
  • isValidGameID (237-238)
src/client/Utils.ts (1)
  • copyToClipboard (19-36)
src/client/JoinPrivateLobbyModal.ts (1)
  • copyToClipboard (396-402)
src/server/Worker.ts (1)
src/server/GamePreviewRoute.ts (1)
  • registerGamePreviewRoute (33-200)
src/client/Main.ts (2)
src/core/Schemas.ts (1)
  • GAME_ID_REGEX (235-235)
src/core/configuration/ConfigLoader.ts (1)
  • getServerConfigFromClient (29-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (30)
src/client/AccountModal.ts (2)

8-8: LGTM!

Import aligns with the new async config fetching pattern used in viewGame.


231-231: LGTM!

Using void to discard the Promise return is the correct pattern for async handlers in Lit templates where the result is not needed.

src/client/Main.ts (9)

4-4: LGTM!

Import of GAME_ID_REGEX from core Schemas centralizes ID validation logic.


90-90: LGTM!

Good addition to the global event map for type safety.


537-537: LGTM!

Event listener correctly hooks the new join-changed event into the existing URL handling flow.


563-563: LGTM!

Using GAME_ID_REGEX.test() is simpler and consistent with the centralized validation pattern.


639-649: LGTM!

Path-based URL matching correctly handles both direct /game/{id} and worker-prefixed /w{n}/game/{id} formats. The two-step validation (regex capture + GAME_ID_REGEX.test) ensures only valid game IDs proceed.


671-671: LGTM!

Early URL update ensures the shareable link is correct before game setup proceeds.


707-724: LGTM!

Adding game-top-bar, account-button, and stats-button to the close list ensures these UI elements are properly hidden during active gameplay.


751-753: LGTM!

Explicitly closing joinModal on game start ensures clean UI transition.


768-772: LGTM!

Path-based URL with ?live query marker enables proper embed previews on platforms like Discord and WhatsApp. Based on learnings, the query suffix is for cache-busting and doesn't affect join logic.

src/client/HostLobbyModal.ts (1)

104-125: Remove the proposed slash normalization and duplicate validation — workerPath() never has leading/trailing slashes.

The workerPath() method in DefaultConfig returns w${this.workerIndex(gameID)} (e.g., "w0", "w1") with no slashes, so the double-slash risk does not exist. Similarly, isValidGameID() is already validated in onOpen() immediately after the lobby is created, before the copy button becomes usable. The proposed fix adds unnecessary code.

Likely an incorrect or invalid review comment.

nginx.conf (2)

9-11: LGTM!

The API cache configuration is well-suited for frequently-requested endpoints with appropriate timeout and size limits.


173-192: Remove the duplication concern—the /maps/ location block appears only once in the file.

The original review incorrectly claimed this block appears twice. A search of nginx.conf shows only one /maps/ location block at line 175. No duplication issue exists.

The CORS configuration is worth noting: Access-Control-Allow-Origin: * allows any domain to request map files. This is typically acceptable for public static assets, but confirm this aligns with your security requirements.

Likely an incorrect or invalid review comment.

src/server/Worker.ts (3)

24-24: LGTM!

Import statement is clean and correct.


98-113: Verify dual /maps handling between nginx and Express.

Both nginx (lines 173-192 in nginx.conf) and Express (lines 103-113) serve the /maps/ route with similar caching headers. While nginx caching in front of Express is common, ensure this layering is intentional and doesn't create cache invalidation issues.


206-213: LGTM!

Route registration is properly structured with all required parameters. The /api/game/:id and /game/:id routes serve different purposes (JSON API vs. HTML preview) and don't conflict.

src/server/GamePreviewRoute.ts (5)

18-31: LGTM!

The requestOrigin helper properly handles protocol detection with X-Forwarded-Proto, and wisely forces HTTPS only for the configured public domain while allowing HTTP for localhost and custom hosts.


45-75: LGTM with minor observation.

The fetch implementation is secure with proper input validation, timeout handling, and error logging. The 1500ms timeout should be sufficient for most cases, but monitor in production to ensure it doesn't cause false negatives during network congestion.


77-111: LGTM!

The remote lobby fetch is well-structured with proper validation and timeout handling. The localhost-only fetch with config-derived ports is safe for inter-worker communication.


113-145: LGTM!

Input validation and data fetching logic is sound. The fallback to redirect when no data is available provides good UX.


164-189: LGTM!

HTML rendering with meta tag injection is properly implemented. The use of escapeHtml on all user-controllable content (title, description, joinUrl, image) prevents XSS attacks. The fallback to JSON is a good safety net.

src/server/GamePreviewBuilder.ts (8)

4-63: LGTM!

Schema definitions and types are well-structured with appropriate optional fields. The union type for playerTeams correctly handles both numeric and string representations.


65-74: LGTM!

Duration formatting is clean with proper input validation and appropriate fallback for invalid values.


76-98: LGTM!

Timestamp normalization using the 1e12 threshold cleverly handles both second and millisecond timestamps. UTC formatting is appropriate for international game previews.


100-128: LGTM!

Winner parsing correctly handles both team and individual winner formats with appropriate fallbacks for unknown formats.


143-150: LGTM!

HTML escaping is correctly implemented with proper character ordering to prevent double-escaping.


152-175: LGTM!

Initial setup properly distinguishes between finished games and active lobbies with appropriate fallbacks for player counts.


176-212: LGTM!

Metadata extraction and image URL construction are well-implemented with proper URL encoding and fallbacks.


214-244: LGTM!

Title and finished game description formatting creates clear, informative previews with proper fallbacks.

Comment thread src/client/HostLobbyModal.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/HostLobbyModal.ts (1)

852-873: Add error handling to the promise chain.

The validation at lines 855-857 throws an error, but the promise chain lacks a .catch() handler. If isValidGameID() fails or any async operation rejects, the error won't be handled, potentially causing unhandled promise rejection warnings or silent failures.

🔧 Add error handler
     createLobby(this.lobbyCreatorClientID)
       .then(async (lobby) => {
         this.lobbyId = lobby.gameID;
         if (!isValidGameID(this.lobbyId)) {
           throw new Error(`Invalid lobby ID format: ${this.lobbyId}`);
         }
         crazyGamesSDK.showInviteButton(this.lobbyId);
         const url = await this.constructUrl();
         this.updateHistory(url);
       })
       .then(() => {
         this.dispatchEvent(
           new CustomEvent("join-lobby", {
             detail: {
               gameID: this.lobbyId,
               clientID: this.lobbyCreatorClientID,
             } as JoinLobbyEvent,
             bubbles: true,
             composed: true,
           }),
         );
-      });
+      })
+      .catch((error) => {
+        console.error("Failed to create or join lobby:", error);
+        // Consider showing user-facing error message
+        this.close();
+      });
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d72e81d and 37747c9.

📒 Files selected for processing (1)
  • src/client/HostLobbyModal.ts
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.
📚 Learning: 2026-01-02T18:11:06.832Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:06.832Z
Learning: In src/client/HostLobbyModal.ts, the ?s=xxxxx URL suffix used in lobby URLs is purely for cache-busting platform previews (e.g., Discord, WhatsApp, x.com) and is not used by the join logic. The join flow ignores the suffix value, so regenerating it via updateUrlWithSuffix() on configuration changes will not break existing shared URLs; it only prompts platforms to refresh preview metadata. Treat the suffix as non-functional for join behavior and ensure any related changes preserve that invariant.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-10-21T20:06:04.823Z
Learnt from: Saphereye
Repo: openfrontio/OpenFrontIO PR: 2233
File: src/client/HostLobbyModal.ts:891-891
Timestamp: 2025-10-21T20:06:04.823Z
Learning: For the HumansVsNations game mode in `src/client/HostLobbyModal.ts` and related files, the implementation strategy is to generate all nations and adjust their strength for balancing, rather than limiting lobby size based on the number of available nations on the map.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, when the browser back button is pressed, the `popstate` event fires before the `hashchange` event. The `preventHashUpdate` flag is used to prevent the `hashchange` listener (`onHashUpdate`) from executing after a navigation rollback in the `popstate` listener (`onPopState`), specifically when the user cancels leaving an active game.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-08-17T20:48:49.411Z
Learnt from: woodydrn
Repo: openfrontio/OpenFrontIO PR: 1836
File: src/client/Main.ts:482-482
Timestamp: 2025-08-17T20:48:49.411Z
Learning: In PR #1836, user woodydrn prefers to keep changes minimal and focused on the specific issue (clientID persistence) rather than refactoring redundant code in JoinLobbyEvent dispatchers. They want to avoid scope creep in focused bug fix PRs.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-07-30T19:46:23.797Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1436
File: src/server/Worker.ts:93-93
Timestamp: 2025-07-30T19:46:23.797Z
Learning: Input validation at API boundaries is critical for security. All client-provided data, especially parameters used for authorization decisions like creatorClientID in the kick player functionality, must be validated for type, format, length, and business logic constraints before processing.

Applied to files:

  • src/client/HostLobbyModal.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.

Applied to files:

  • src/client/HostLobbyModal.ts
🧬 Code graph analysis (1)
src/client/HostLobbyModal.ts (3)
src/core/configuration/ConfigLoader.ts (1)
  • getServerConfigFromClient (29-46)
src/core/Schemas.ts (1)
  • isValidGameID (237-238)
src/client/Utils.ts (1)
  • copyToClipboard (19-36)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (6)
src/client/HostLobbyModal.ts (6)

106-112: LGTM: Clean random string generator.

Simple, readable implementation for the cache-busting suffix. Five characters from lowercase alphanumeric set provides sufficient uniqueness for the use case.


114-126: LGTM: Well-structured URL management.

The separation is clear:

  • buildLobbyUrl() reuses the current suffix (for copying)
  • constructUrl() generates a fresh suffix (for cache-busting)
  • updateHistory() centralizes history updates

This matches the learned behavior that suffix regeneration forces embed preview refresh without breaking existing shared URLs.


932-936: LGTM: Proper conditional cleanup.

The leaveLobbyOnClose flag correctly controls whether the lobby is left on modal close. Resetting the URL to / when leaving is appropriate. The flag reset at line 975 ensures clean state for next use.


1118-1119: LGTM: Cache-busting on config changes.

Regenerating the URL suffix on every config update forces external platforms to refresh their embed previews, which is the intended behavior per the PR objectives and learnings.


1179-1197: LGTM: Proper flag management on game start.

Setting leaveLobbyOnClose = false before starting prevents the modal from leaving the lobby when it closes (since the game is starting). Reverting to true on failure ensures proper cleanup if start fails. This pattern is clean and correct.


1200-1207: LGTM: Reuses current URL suffix for copying.

Using buildLobbyUrl() (which reuses the current suffix) instead of constructUrl() (which generates a new one) is correct. Copying the invite link shouldn't trigger cache-busting—it should share the exact current lobby URL.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/JoinPrivateLobbyModal.ts (1)

403-409: Fix: clipboard still copies the old hash URL (/#join=), which blocks embeds and new /game/<id> routing.

Proposed fix (copy the new path-based URL, with cache-bust marker)
   private async copyToClipboard() {
     await copyToClipboard(
-      `${location.origin}/#join=${this.currentLobbyId}`,
+      `${location.origin}/game/${this.currentLobbyId}?lobby`,
       () => (this.copySuccess = true),
       () => (this.copySuccess = false),
     );
   }
🤖 Fix all issues with AI agents
In @src/client/JoinPrivateLobbyModal.ts:
- Around line 427-443: In extractLobbyIdFromUrl, extend URL parsing to also
detect a lobby id in the URL hash: after checking
url.pathname.match(/game\/([^/]+)/) also inspect url.hash for either a
hash-anchored /game/<id> (e.g., url.hash.match(/game\/([^/]+)/)) and the legacy
#join=<id> pattern (e.g., url.hash.match(/#?join=([^&]+)/)); validate any
candidate with GAME_ID_REGEX and return it if valid, otherwise fall back to
returning the original input and keep the existing try/catch/logging behavior.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 37747c9 and a3a2e48.

📒 Files selected for processing (1)
  • src/client/JoinPrivateLobbyModal.ts
🧰 Additional context used
🧠 Learnings (4)
📓 Common learnings
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.
Learnt from: woodydrn
Repo: openfrontio/OpenFrontIO PR: 1836
File: src/client/Main.ts:482-482
Timestamp: 2025-08-17T20:48:49.411Z
Learning: In PR #1836, user woodydrn prefers to keep changes minimal and focused on the specific issue (clientID persistence) rather than refactoring redundant code in JoinLobbyEvent dispatchers. They want to avoid scope creep in focused bug fix PRs.
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/client/JoinPrivateLobbyModal.ts
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/JoinPrivateLobbyModal.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, when the browser back button is pressed, the `popstate` event fires before the `hashchange` event. The `preventHashUpdate` flag is used to prevent the `hashchange` listener (`onHashUpdate`) from executing after a navigation rollback in the `popstate` listener (`onPopState`), specifically when the user cancels leaving an active game.

Applied to files:

  • src/client/JoinPrivateLobbyModal.ts
🧬 Code graph analysis (1)
src/client/JoinPrivateLobbyModal.ts (1)
src/core/Schemas.ts (1)
  • GAME_ID_REGEX (235-235)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (3)
src/client/JoinPrivateLobbyModal.ts (3)

4-10: Good: shared lobby-id validation via GAME_ID_REGEX. This removes duplicated regex logic and keeps client/server rules consistent.

Also applies to: 411-413


398-401: Good: leaveLobbyOnClose prevents “leave” during join-triggered closes, and the close button still forces leave.

Also applies to: 530-532


365-378: The leave-lobby event contract is consistent across the codebase. All four emitters—JoinPrivateLobbyModal, HostLobbyModal, PublicLobby, and ClientGameRunner—use the same field name lobby for the lobby identifier. The listener (handleLeaveLobby in Main.ts) does not read the event detail, so field naming has no practical impact. No contract mismatch exists.

Likely an incorrect or invalid review comment.

Comment thread src/client/JoinPrivateLobbyModal.ts
Comment thread src/client/JoinPrivateLobbyModal.ts
Comment thread nginx.conf Outdated
Comment thread nginx.conf Outdated
Comment thread src/server/GamePreviewRoute.ts Outdated
Comment thread src/server/GamePreviewRoute.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/JoinPrivateLobbyModal.ts (1)

404-410: Critical: Update to path-based URL format.

Line 406 still uses the old hash-based format /#join=${this.currentLobbyId}, which is inconsistent with the PR's objective to migrate to path-based URLs (/game/<code>). This will break embed previews on Discord, WhatsApp, and X, which is the main goal of this PR.

🔧 Proposed fix
  private async copyToClipboard() {
    await copyToClipboard(
-     `${location.origin}/#join=${this.currentLobbyId}`,
+     `${location.origin}/game/${this.currentLobbyId}`,
      () => (this.copySuccess = true),
      () => (this.copySuccess = false),
    );
  }
🧹 Nitpick comments (6)
nginx.conf (2)

7-12: Ensure the new API cache path is deploy-safe (directory + permissions) and won’t cache auth’d responses.

  • proxy_cache_path /var/cache/nginx/api ... keys_zone=API_CACHE... needs /var/cache/nginx/api present and writable by the Nginx worker user; otherwise caching (or even startup, depending on setup) can break.
  • If any “public” endpoint ever starts varying by auth, add an explicit bypass so you don’t accidentally cache user-scoped responses.
Possible hardening (optional)
 proxy_cache_path /var/cache/nginx/api levels=1:2 keys_zone=API_CACHE:10m inactive=60m max_size=100m;
+
+# Optional (apply inside cached API locations):
+# proxy_cache_bypass $http_authorization;
+# proxy_no_cache    $http_authorization;

174-199: Avoid conflicting cache headers on / (hide upstream cache headers or ensure a single policy).

add_header Cache-Control/Pragma/Expires can end up alongside upstream headers (depending on what the upstream returns), which can lead to confusing client behavior. If you want “no browser cache” to be guaranteed, hide upstream cache headers in this location before adding your own.

Safer header override for the root route
 location = / {
     proxy_pass http://127.0.0.1:3000;
+
+    # Prevent upstream from sending conflicting cache headers
+    proxy_hide_header Cache-Control;
+    proxy_hide_header Pragma;
+    proxy_hide_header Expires;

     # Tell browsers not to cache
-    add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate";
-    add_header Pragma "no-cache";
-    add_header Expires "0";
+    add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate" always;
+    add_header Pragma "no-cache" always;
+    add_header Expires "0" always;
src/server/GamePreviewRoute.ts (4)

43-43: Consider standardizing validation approach.

The route uses GAME_ID_REGEX.test() at line 81, while fetchPublicGameInfo uses gameIDSchema.safeParse() at line 48. Since the file already leverages Zod for ExternalGameInfoSchema validation, consider using the schema consistently:

♻️ Proposed refactor for consistent validation

Replace the regex test at line 81-83:

-    if (!GAME_ID_REGEX.test(gameID)) {
+    if (!gameIDSchema.safeParse(gameID).success) {
       return res.status(400).json({ error: "Invalid game ID format" });
     }

This makes validation consistent throughout the file and aligns with the Zod-based validation pattern used elsewhere.

Also applies to: 81-83


45-75: LGTM with optional note.

The function has robust error handling, appropriate timeout (1500ms), and proper logging. The validation at line 48 is redundant given the route handler validates at line 81 before calling this function, but it adds defensive programming if this function might be called from other contexts.

If fetchPublicGameInfo will only be called after validation in the route handler, you could remove the redundant check at line 48 for simplicity.


85-87: Consider a more descriptive variable name.

The variable lobby might be clearer as liveGameInfo or currentGame, since game.gameInfo() returns the current game state (which could be in-progress or finished), not necessarily just lobby data.

♻️ Proposed rename for clarity
-    const lobby: GameInfo | null = game ? game.gameInfo() : null;
+    const liveGameInfo: GameInfo | null = game ? game.gameInfo() : null;

Then update references throughout the function (lines 93, 102, 153).


107-121: Consider simplifying file lookup logic.

The nested try-catch blocks work correctly but are verbose. Consider a cleaner approach:

♻️ Proposed refactor for readability
-      const staticHtml = path.join(baseDir, "../../static/index.html");
-      const rootHtml = path.join(baseDir, "../../index.html");
-      let filePath: string | null = null;
-
-      try {
-        await fsPromises.access(staticHtml);
-        filePath = staticHtml;
-      } catch {
-        try {
-          await fsPromises.access(rootHtml);
-          filePath = rootHtml;
-        } catch {
-          // Neither file exists
-        }
-      }
+      const candidatePaths = [
+        path.join(baseDir, "../../static/index.html"),
+        path.join(baseDir, "../../index.html"),
+      ];
+      
+      let filePath: string | null = null;
+      for (const candidate of candidatePaths) {
+        try {
+          await fsPromises.access(candidate);
+          filePath = candidate;
+          break;
+        } catch {
+          // Try next candidate
+        }
+      }

This approach is easier to extend if more paths are added later.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3a2e48 and 5d1a195.

📒 Files selected for processing (3)
  • nginx.conf
  • src/client/JoinPrivateLobbyModal.ts
  • src/server/GamePreviewRoute.ts
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/JoinPrivateLobbyModal.ts
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/client/JoinPrivateLobbyModal.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, when the browser back button is pressed, the `popstate` event fires before the `hashchange` event. The `preventHashUpdate` flag is used to prevent the `hashchange` listener (`onHashUpdate`) from executing after a navigation rollback in the `popstate` listener (`onPopState`), specifically when the user cancels leaving an active game.

Applied to files:

  • src/client/JoinPrivateLobbyModal.ts
📚 Learning: 2025-08-17T20:48:49.411Z
Learnt from: woodydrn
Repo: openfrontio/OpenFrontIO PR: 1836
File: src/client/Main.ts:482-482
Timestamp: 2025-08-17T20:48:49.411Z
Learning: In PR #1836, user woodydrn prefers to keep changes minimal and focused on the specific issue (clientID persistence) rather than refactoring redundant code in JoinLobbyEvent dispatchers. They want to avoid scope creep in focused bug fix PRs.

Applied to files:

  • src/client/JoinPrivateLobbyModal.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.

Applied to files:

  • src/client/JoinPrivateLobbyModal.ts
🧬 Code graph analysis (1)
src/client/JoinPrivateLobbyModal.ts (1)
src/core/Schemas.ts (1)
  • GAME_ID_REGEX (235-235)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (13)
src/server/GamePreviewRoute.ts (5)

1-16: LGTM!

Imports are clean, necessary, and properly typed. Good use of type-only imports where appropriate.


18-31: LGTM!

The origin extraction logic correctly handles proxied requests and appropriately forces HTTPS only for the configured public domain, avoiding issues with localhost or custom hosts.


97-104: LGTM!

Preview building logic is clean and passes all necessary data to the builder function.


123-148: LGTM!

HTML meta tag injection is implemented correctly with proper:

  • XSS prevention via escapeHtml() on all dynamic values
  • Removal of existing tags to prevent duplicates
  • Safe DOM manipulation using node-html-parser
  • Fallback to meta.title when meta.description is missing

151-157: LGTM!

Fallback to JSON when HTML is unavailable is reasonable, and the error handling logs details before returning a 500 status. The broad try-catch is acceptable here since the function logs the error and provides a meaningful error response.

src/client/JoinPrivateLobbyModal.ts (8)

6-6: Good use of shared constant for validation.

Using GAME_ID_REGEX from Schemas eliminates duplication and ensures consistent validation across the codebase.


35-35: Clear intent with the auto-leave control flag.

The flag correctly defaults to true for normal close behavior and is reset in onClose() to maintain proper state for subsequent modal openings.


365-376: Clean guard logic for leave event.

The guards (!currentLobbyId || !hasJoined) correctly prevent dispatching the leave event for archived games and edge cases. Implementation is straightforward.


378-397: Well-structured close logic with correct state reset ordering.

The conditional leave-and-reset (lines 386-390) correctly preserves currentLobbyId and hasJoined for leaveLobby(), then resets state after (lines 392-394). When leaveLobbyOnClose is false (game starting), the URL is preserved. Resetting the flag to true at line 396 ensures proper default behavior for subsequent modal openings.


399-402: LGTM!

Setting leaveLobbyOnClose = true before closing ensures onClose() will trigger the leave logic. Clear and correct.


412-414: LGTM!

Using the shared GAME_ID_REGEX ensures consistent validation across the codebase.


428-444: Solid URL parsing with proper validation.

The rewrite correctly handles the new /game/{id} format using the URL API. Extracting from url.pathname (line 435) properly ignores query parameters like ?s=xxxxx (per learnings, this is purely for cache-busting embed previews). The fallback to input on parse failure and the GAME_ID_REGEX validation provide good safety.


531-532: Correct flag setting prevents unwanted leave event.

Setting leaveLobbyOnClose = false here correctly prevents the modal from dispatching a leave event when it closes as part of the game starting. The comment clearly explains the intent. This works well with closeAndLeave() which sets the flag back to true for manual closes.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
nginx.conf (1)

22-66: Silent fallback for out-of-range worker numbers.

If a request hits /w41/... or any worker number beyond 40, $worker_port stays at the default 3001. This routes the request to worker 0 without any error, which could hide misconfiguration.

Two options to consider:

  1. Return an error for unknown workers – Add a check after the if-chain to return 404 or 502 when the port was not explicitly set.
  2. Use a map directive – Cleaner and easier to maintain. The map can define a default that returns empty, then check for empty before proxying.
♻️ Optional: Use a map directive instead of if-chain

Add this outside the server block:

map $worker_number $worker_port_map {
    default     "";
    "0"         3001;
    "1"         3002;
    # ... continue for all workers ...
    "40"        3041;
}

Then in the location:

 location ~* ^/w(\d+)(/.*)?$ {
     set $worker_number $1;
-    set $worker_port 3001;
-    
-    if ($worker_number = "0") { set $worker_port 3001; }
-    # ... all the if statements ...
+    set $worker_port $worker_port_map;
+    
+    if ($worker_port = "") {
+        return 502;
+    }
     
     proxy_pass http://127.0.0.1:$worker_port$2$is_args$args;
     # ... rest unchanged ...
 }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d1a195 and cb64da5.

📒 Files selected for processing (1)
  • nginx.conf
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.
🔇 Additional comments (2)
nginx.conf (2)

10-11: LGTM on the API cache config.

Separate cache zone for API endpoints with smaller size (100m) and shorter inactive timeout (60m) is appropriate. Keeps API responses from competing with static assets in the main cache.


68-76: LGTM on the proxy configuration.

Query string handling with $is_args$args is correct – it only appends ? when args exist. WebSocket upgrade headers are properly included for real-time connections.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/JoinPrivateLobbyModal.ts (1)

423-428: copyToClipboard still uses hash-based URL format.

The copy function constructs /#join=${this.currentLobbyId} but the PR migrates to path-based URLs (/game/{id}). This creates inconsistency where shared URLs will not work with the new routing.

Suggested fix using path-based URL
  private async copyToClipboard() {
+   const config = await getServerConfigFromClient();
    await copyToClipboard(
-     `${location.origin}/#join=${this.currentLobbyId}`,
+     `${location.origin}/${config.workerPath(this.currentLobbyId)}/game/${this.currentLobbyId}`,
      () => (this.copySuccess = true),
      () => (this.copySuccess = false),
    );
  }

Note: You may need to import getServerConfigFromClient if not already available, but it appears to already be imported at line 12.

🧹 Nitpick comments (2)
src/client/AccountModal.ts (2)

258-268: Mismatch between workerPath(gameId) and encoded URL segment.

The workerPath receives the raw gameId, but the URL path uses encodedGameId. If workerPath internally uses the game ID for routing logic, this could cause inconsistency.

Also, game IDs matching GAME_ID_REGEX (/^[A-Za-z0-9]{8}$/) contain only URL-safe characters, so encodeURIComponent does nothing here. Consider removing the encoding for clarity, or ensure the pattern matches Main.ts (which does not encode the ID in its URL construction at lines 840-844 and 853).

Suggested simplification
  private async viewGame(gameId: string): Promise<void> {
    this.close();
    const config = await getServerConfigFromClient();
-   const encodedGameId = encodeURIComponent(gameId);
-   const newUrl = `/${config.workerPath(gameId)}/game/${encodedGameId}`;
+   const newUrl = `/${config.workerPath(gameId)}/game/${gameId}`;

    history.pushState({ join: gameId }, "", newUrl);
    window.dispatchEvent(
-     new CustomEvent("join-changed", { detail: { gameId: encodedGameId } }),
+     new CustomEvent("join-changed", { detail: { gameId } }),
    );
  }

227-228: Consider adding error handling for the async viewGame call.

If getServerConfigFromClient() fails (network error, server down), the promise rejection goes unhandled. A simple .catch would prevent silent failures.

Example with error handling
-             .onViewGame=${(id: string) => void this.viewGame(id)}
+             .onViewGame=${(id: string) => {
+               this.viewGame(id).catch((err) => {
+                 console.error("Failed to view game:", err);
+               });
+             }}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb64da5 and cd47dc1.

📒 Files selected for processing (4)
  • src/client/AccountModal.ts
  • src/client/HostLobbyModal.ts
  • src/client/JoinPrivateLobbyModal.ts
  • src/client/Main.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/client/HostLobbyModal.ts
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.
📚 Learning: 2025-06-09T02:20:43.637Z
Learnt from: VariableVince
Repo: openfrontio/OpenFrontIO PR: 1110
File: src/client/Main.ts:293-295
Timestamp: 2025-06-09T02:20:43.637Z
Learning: In src/client/Main.ts, during game start in the handleJoinLobby callback, UI elements are hidden using direct DOM manipulation with classList.add("hidden") for consistency. This includes modals, buttons, and error divs. The codebase follows this pattern rather than using component APIs for hiding elements during game transitions.

Applied to files:

  • src/client/AccountModal.ts
  • src/client/JoinPrivateLobbyModal.ts
  • src/client/Main.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, when the browser back button is pressed, the `popstate` event fires before the `hashchange` event. The `preventHashUpdate` flag is used to prevent the `hashchange` listener (`onHashUpdate`) from executing after a navigation rollback in the `popstate` listener (`onPopState`), specifically when the user cancels leaving an active game.

Applied to files:

  • src/client/AccountModal.ts
  • src/client/JoinPrivateLobbyModal.ts
📚 Learning: 2026-01-02T18:11:15.132Z
Learnt from: ryanbarlow97
Repo: openfrontio/OpenFrontIO PR: 2740
File: src/client/HostLobbyModal.ts:821-821
Timestamp: 2026-01-02T18:11:15.132Z
Learning: In src/client/HostLobbyModal.ts, the `?s=xxxxx` URL suffix in lobby URLs is purely for cache-busting embed previews on platforms like Discord, WhatsApp, and x.com. The suffix value is ignored by the join logic (any value works), so regenerating it on config changes via `updateUrlWithSuffix()` doesn't break existing shared URLs - it only forces platforms to re-fetch updated preview metadata.

Applied to files:

  • src/client/AccountModal.ts
  • src/client/JoinPrivateLobbyModal.ts
  • src/client/Main.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: In `ClientGameRunner.ts`, the `myPlayer` field is always set when `shouldPreventWindowClose()` is called, so the null check in that method is sufficient without needing to fetch it again from `gameView.playerByClientID()`.

Applied to files:

  • src/client/AccountModal.ts
📚 Learning: 2025-08-17T20:48:49.411Z
Learnt from: woodydrn
Repo: openfrontio/OpenFrontIO PR: 1836
File: src/client/Main.ts:482-482
Timestamp: 2025-08-17T20:48:49.411Z
Learning: In PR #1836, user woodydrn prefers to keep changes minimal and focused on the specific issue (clientID persistence) rather than refactoring redundant code in JoinLobbyEvent dispatchers. They want to avoid scope creep in focused bug fix PRs.

Applied to files:

  • src/client/AccountModal.ts
  • src/client/JoinPrivateLobbyModal.ts
  • src/client/Main.ts
📚 Learning: 2025-10-08T17:14:49.369Z
Learnt from: Foorack
Repo: openfrontio/OpenFrontIO PR: 2141
File: src/client/ClientGameRunner.ts:228-234
Timestamp: 2025-10-08T17:14:49.369Z
Learning: For the window close confirmation feature in `ClientGameRunner.ts`, the troop count requirement (>10,000 troops) from issue #2137 was intentionally removed because it was arbitrary and troop count can be reported as low despite having significant land. The confirmation now triggers for any alive player regardless of troop count.

Applied to files:

  • src/client/JoinPrivateLobbyModal.ts
📚 Learning: 2026-01-08T13:52:00.939Z
Learnt from: deshack
Repo: openfrontio/OpenFrontIO PR: 2777
File: src/client/Main.ts:374-378
Timestamp: 2026-01-08T13:52:00.939Z
Learning: In src/client/Main.ts, ensure the browser back/forward navigation ordering is correctly handled: when popstate fires before hashchange during a navigation rollback (e.g., user cancels leaving an active game), use a preventHashUpdate-like flag to suppress the hashchange listener (onHashUpdate) from running in that rollback scenario. This avoids applying an unintended hash update after a rollback and keeps the UI/game state consistent. Document the flag’s usage and the exact conditions under which it is set/reset to prevent subtle regressions.

Applied to files:

  • src/client/Main.ts
📚 Learning: 2025-08-12T00:31:50.144Z
Learnt from: scottanderson
Repo: openfrontio/OpenFrontIO PR: 1752
File: src/core/game/Game.ts:750-752
Timestamp: 2025-08-12T00:31:50.144Z
Learning: In the OpenFrontIO codebase, changes to the PlayerInteraction interface (like adding canDonateGold and canDonateTroops flags) do not require corresponding updates to src/core/Schemas.ts or server serialization code.

Applied to files:

  • src/client/Main.ts
🧬 Code graph analysis (3)
src/client/AccountModal.ts (1)
src/core/configuration/ConfigLoader.ts (1)
  • getServerConfigFromClient (29-46)
src/client/JoinPrivateLobbyModal.ts (1)
src/core/Schemas.ts (1)
  • GAME_ID_REGEX (235-235)
src/client/Main.ts (2)
src/core/Schemas.ts (1)
  • GAME_ID_REGEX (235-235)
src/core/configuration/ConfigLoader.ts (1)
  • getServerConfigFromClient (29-46)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy to openfront.dev
🔇 Additional comments (12)
src/client/AccountModal.ts (1)

8-8: Import looks good.

The getServerConfigFromClient import aligns with the async config fetching pattern used elsewhere in this PR.

src/client/Main.ts (6)

711-721: Path-based URL parsing looks solid.

The regex correctly handles optional worker prefix (w\d+), and the validation with GAME_ID_REGEX ensures only valid 8-character alphanumeric IDs proceed. Good defensive approach.


634-640: Good use of centralized GAME_ID_REGEX for validation.

Using the shared regex constant from Schemas.ts keeps validation consistent across the codebase.


849-859: Clean helper for URL updates.

The guard currentUrl !== targetUrl avoids redundant replaceState calls. Using replaceState (not pushState) is correct here since this is for sharing, not navigation history.


784-792: LGTM.

Adding game-top-bar, account-button, and stats-button to the close list follows the established pattern for hiding UI elements during game transitions.


840-844: Path-based URL with ?live cache-buster looks correct.

The URL structure matches the PR's goal of supporting embed previews. The ?live query param will help platforms distinguish between lobby and live game states.


609-609: Add guard to prevent unintended game stop when viewing replays from account modal.

The join-changed event listener (line 609) calls onHashUpdate, which unconditionally stops the current game if gameStop !== null (lines 595-597). When AccountModal.viewGame() dispatches join-changed to view a replay while in-game, this stops the active game without warning.

If viewing a replay while in-game should stop the current game, this behavior is correct but should be documented. If viewing a replay should not stop the active game, add a flag similar to preventHashUpdate to distinguish between application navigation (join-changed) and browser navigation (popstate/hashchange).

src/client/JoinPrivateLobbyModal.ts (5)

384-416: Well-structured leave logic with leaveLobbyOnClose flag.

The conditional leave behavior prevents the modal from dispatching leave-lobby when it closes as part of the game join flow. The flag reset at line 415 ensures clean state for subsequent modal opens.


447-462: Good URL parsing with fallback.

Using the URL API for safe parsing is cleaner than regex-only approaches. The fallback to raw input handles edge cases gracefully. The try-catch with logged warning helps debug issues without breaking the flow.


431-433: Good use of centralized GAME_ID_REGEX.

Single source of truth for validation keeps behavior consistent across the codebase.


550-551: Correctly prevents leave on successful join.

Setting leaveLobbyOnClose = false before dispatching join-lobby ensures that if the modal closes during the join flow, it won't accidentally trigger a leave event.


418-421: Clean delegation to onClose for leave logic.

Setting the flag before close() keeps the leave logic centralized in onClose. This avoids duplication and makes the flow easier to follow.

Comment thread src/server/GamePreviewBuilder.ts Outdated
Comment thread src/server/GamePreviewBuilder.ts Outdated
Comment thread src/server/GamePreviewBuilder.ts Outdated
evanpelle
evanpelle previously approved these changes Jan 13, 2026
Copy link
Copy Markdown
Collaborator

@evanpelle evanpelle left a comment

Choose a reason for hiding this comment

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

thanks!

Comment thread src/server/GamePreviewRoute.ts Outdated
Copy link
Copy Markdown
Collaborator

@evanpelle evanpelle left a comment

Choose a reason for hiding this comment

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

thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend Server-side features and systems - lobbies, matchmaking, accounts, APIs, etc. Bugfix Fixes a bug Feature UI/UX UI/UX changes including assets, menus, QoL, etc.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants