Production readiness: self-hosted assets, accessibility, tests and CI - #1
Merged
Conversation
React 19 ships without bundled types, so tsc silently resolved 'react' to any and verified nothing across the .tsx files. Installing @types/react and @types/react-dom makes the existing typecheck meaningful. Also adds tailwindcss/postcss/autoprefixer for a build-time stylesheet, and jsdom + testing-library for component tests. npm audit fix clears one critical and three high advisories, all in dev dependencies.
The published bundle shipped 19 MB of assets. The four crew portraits alone were 9.6 MB of 1024x1536 PNGs rendered into 40x40 boxes, and the scenario thumbnails sat under two dark gradients at 0.34 opacity. Every asset is now sized for how it is actually drawn: crew at 192px, scenario thumbnails at 640px, fx tiles at 512px to match their 360-480px background-size, full-screen backgrounds re-encoded at their existing resolution. favicon.png, icon.png and menu_background.png were JPEG data behind a .png extension while the manifest declared image/png. They are replaced by real PNG icons at 64/180/192/512 and a 1200x630 JPEG for social cards. public/ drops from 16 MB to 1.0 MB.
index.html pulled https://cdn.tailwindcss.com, the JIT build meant for prototyping. All styling therefore depended on a third-party host staying up: if it failed or was blocked, the game rendered unstyled. Tailwind now compiles at build time. The directives sit above the custom rules in index.css so project styles keep winning at equal specificity, which matters for .font-mono resolving to --aaa-font-mono. Three components assemble class names at runtime with String.replace, so the content scanner cannot see them. Those classes are listed in the config safelist, with a comment naming the files to keep in sync. Also drops the importmap: Vite bundles React, so it resolved nothing and only shipped dead bytes in the production HTML.
The codebase had zero aria attributes and zero role attributes. The faders are the core mechanic and were plain divs with onPointerDown, so the game could not be played without a pointer at all. Each fader is now a focusable role=slider following the WAI-ARIA pattern: arrows step by 2, PageUp/PageDown by 10, Home and End jump to the extremes. Its aria-valuetext announces the zone, not just the number, because staying inside the safe zone is the actual objective. Beyond that: progress bars expose their values, the seven overlays are labelled modal dialogs, the terminal and client messages are live regions, warnings are assertive, and decorative canvases and overlays are hidden from the tree. Button did not forward aria-label, which is why the two icon-only HUD controls had no accessible name; it now accepts ariaLabel and defaults to type=button. ClientPopup kept a dismissed message in the DOM at opacity 0, so screen readers still read it and its close button stayed focusable. It is now aria-hidden and inert while hidden. Also replaces the last two third-party texture CDNs and the dicebear avatar with local assets already present in the art pack.
useGameLogic read localStorage.getItem outside its try block and none of the writes were guarded. Safari private mode, blocked storage and quota errors throw on plain reads too, and with no error boundary anywhere in the tree that surfaced as a blank page. safeStorage probes with a write round-trip, because some browsers allow getItem and still throw on setItem, then degrades to an in-memory store for the session rather than crashing. ErrorBoundary catches what still gets through and offers a reload instead of a white screen.
The file was 2719 lines, but the first 1463 were pure rules with no state and no React: match pacing, boss moments, missions, event spawning, economy, career persistence and crew modifiers all sharing one scope. Those move verbatim into hooks/gameLogic/*, grouped by domain. The hook keeps the stateful half and re-exports the modules, so it stays the single public entry point and every existing test imports the same paths unchanged. A few types were declared wherever they happened to land rather than where they belong; BossMomentProfile and the economy profiles now live in their own module. Verified as a pure move: normalising both sides for imports and export keywords, the extracted logic diffs identically against the previous file.
App.tsx held 22 useState calls, five of which were player preferences with their own persistence effect and storage keys, plus two viewport listeners. useUserSettings owns the preferences and their persistence, keeping the legacy visual-quality key honoured. useViewportLayout owns the two viewport queries. The quality and audio cyclers used to call addLog inside a setState updater, which React invokes twice in StrictMode and therefore logged twice in dev. They now return the mode they picked and the screen does the announcing, so the hooks know nothing about the game log.
The suite covered pure logic only and ran on the node environment, so the whole render path and every aria attribute could break without a single failure. Component suites opt into jsdom per file with a docblock, leaving the existing logic tests on the fast node environment; the full run still finishes in about three seconds. Assertions use Vitest's own matchers rather than jest-dom, whose matcher types would need augmentation to keep tsc --noEmit clean. Coverage targets the fader keyboard pattern, the accessible-name contract for buttons and dialogs, live regions, and that the error boundary really catches a throw. Each was mutation-tested: removing the behaviour fails the suite.
Nothing ran the checks automatically and npm run check only covered typecheck and build, so a red test suite could reach main unnoticed. The workflow runs typecheck, tests, build and the budget on push and pull requests, pinned to Node 20 so CI and the Netlify deploy never disagree about the runtime. The budget guard exists because tests protect behaviour but nothing protected size: the project already shipped a 19 MB bundle once. Limits are deliberately loose, meant to catch that class of mistake rather than police kilobytes. netlify.toml adds the SPA redirect, cache headers and a CSP. With the CDNs gone, Google Fonts is the only third-party origin left to allow. npm run check now mirrors the pipeline exactly.
The repository moved to matecodedev/event_chaos; the badge still referenced the old owner, where Actions never runs.
There was a problem hiding this comment.
Pull request overview
Production-readiness pass that makes the project deployable with confidence by removing runtime CDN dependencies, adding CI + local verification, hardening persistence/error handling, and improving accessibility with accompanying tests.
Changes:
- Adds CI workflow and expands
npm run checkto run typecheck, tests, build, and a bundle-budget gate. - Hardens runtime reliability (safe
localStorageaccess + top-levelErrorBoundary) and introduces hooks for settings/viewport state. - Improves accessibility semantics across UI (roles/aria/keyboard support) and adds focused a11y + regression tests; migrates art assets to self-hosted/optimized formats.
Reviewed changes
Copilot reviewed 46 out of 97 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| vitest.config.ts | Enables React transform + .test.tsx discovery and shared test setup file. |
| utils/safeStorage.ts | Adds resilient storage wrapper with in-memory fallback and JSON helpers. |
| utils/artAssets.ts | Updates asset paths to optimized formats (webp) and centralized asset access. |
| tests/ui-accessibility-contract.test.tsx | Adds accessibility contract tests for key UI components. |
| tests/setup/testing-library.ts | Global test cleanup setup gated on DOM availability. |
| tests/safe-storage-regressions.test.ts | Regression tests for storage failures and JSON corruption. |
| tests/fader-panel-a11y.test.tsx | Keyboard + ARIA behavior tests for core fader mechanic. |
| tests/error-boundary.test.tsx | Validates ErrorBoundary UI behavior and logging. |
| tailwind.config.js | Adds Tailwind content config + safelist for runtime-built class names. |
| scripts/check-bundle-budget.mjs | Adds bundle-size budget script for CI/local gating. |
| README.md | Documents CI/check workflow, test layout, styling build, and deployment notes. |
| public/manifest.json | Improves PWA icon metadata and maskable icon entry. |
| postcss.config.js | Adds PostCSS pipeline for Tailwind + Autoprefixer. |
| package.json | Adds dependencies/devDependencies and expands scripts for checks and watch mode. |
| netlify.toml | Defines Netlify build/env, caching headers, SPA redirects, and CSP. |
| index.tsx | Wraps app in ErrorBoundary for crash resilience. |
| index.html | Removes Tailwind CDN/importmap usage and updates icons/social metadata. |
| index.css | Adds Tailwind directives and updates CSS asset URLs to webp. |
| hooks/useViewportLayout.ts | New hook to centralize mobile/compact viewport detection. |
| hooks/useUserSettings.ts | New hook owning persisted user settings using safeStorage. |
| hooks/useGameLogic.ts | Extracts pure game logic into hooks/gameLogic/* and uses safe storage for career save. |
| hooks/gameLogic/upgrades.ts | Extracted pure upgrade modifiers logic. |
| hooks/gameLogic/sessionRules.ts | Extracted session rule helpers and telemetry types. |
| hooks/gameLogic/missions.ts | Extracted mission selection/reward logic helpers. |
| hooks/gameLogic/math.ts | Shared clamp helper for extracted logic. |
| hooks/gameLogic/events.ts | Extracted event spawning/escalation/cascade helpers. |
| hooks/gameLogic/economy.ts | Extracted scoring/economy computations and profiles. |
| hooks/gameLogic/director.ts | Extracted director/adaptive difficulty logic. |
| hooks/gameLogic/career.ts | Extracted career normalization + known IDs/constants. |
| hooks/gameLogic/bossMoments.ts | Extracted boss-moment pacing logic. |
| components/Visualizer.tsx | Switches texture overlays to local assets and hides purely visual canvas from AT. |
| components/UpgradeShop.tsx | Adds dialog semantics, button labeling, and safer icon/close markup. |
| components/TutorialOverlay.tsx | Adds dialog semantics and swaps avatar CDN to local portrait asset + proper alt text. |
| components/TerminalLog.tsx | Adds role="log" + live region semantics and hides decorative boot lines. |
| components/ProgressBar.tsx | Adds role="progressbar" with ARIA value semantics and hides decorative segments. |
| components/Minigames.tsx | Adds dialog semantics and swaps texture CDNs to local assets. |
| components/GameSettingsPanel.tsx | Adds dialog semantics, grouped/toggle semantics for option pills, and button labels. |
| components/GameMenu.tsx | Adds dialog semantics for pause/loading overlay. |
| components/FXCanvas.tsx | Marks decorative canvas as aria-hidden. |
| components/FaderPanel.tsx | Adds keyboard slider interaction and ARIA slider semantics for accessibility. |
| components/ErrorBoundary.tsx | New error boundary component with alert UI and reload control. |
| components/EarlyWarningPanel.tsx | Adds live log semantics and hides decorative overlay/icon from AT. |
| components/ClientPopup.tsx | Adds status live region semantics and hides dismissed popup from AT. |
| components/Button.tsx | Adds default type="button" and supports accessible naming / toggle state. |
| components/AchievementPanel.tsx | Adds dialog semantics, close button labeling, and hides decorative icons. |
| App.tsx | Moves settings/layout logic into dedicated hooks and adds accessible labels to icon buttons. |
| .gitignore | Ignores local tooling cache directory. |
| .github/workflows/ci.yml | Adds GitHub Actions CI for typecheck, test, build, and bundle-budget verification. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+83
to
+89
| try { | ||
| const parsed = JSON.parse(raw) as T; | ||
| if (parsed === null || typeof parsed !== 'object') return fallback; | ||
| return parsed; | ||
| } catch { | ||
| return fallback; | ||
| } |
Comment on lines
+39
to
+44
| const gzipSizeOf = async (files, extension) => { | ||
| const match = files.find((file) => file.path.endsWith(extension)); | ||
| if (!match) return null; | ||
| const content = await readFile(path.join(DIST, match.path)); | ||
| return { path: match.path, size: gzipSync(content).length }; | ||
| }; |
Comment on lines
+29
to
+33
| <div | ||
| role="progressbar" | ||
| aria-label={label} | ||
| aria-valuemin={0} | ||
| aria-valuemax={max} |
Comment on lines
+81
to
+86
| <button | ||
| type="button" | ||
| onClick={() => setIsVisible(false)} | ||
| aria-label="Descartar mensaje del cliente" | ||
| className="text-white/50 hover:text-white" | ||
| > |
Feature branches were only covered through the pull_request trigger, so work pushed before a PR exists got no feedback at all.
…iness # Conflicts: # README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Production-readiness pass over the whole project, split into reviewable units.
Each commit stands on its own theme; the branch tip is verified green.
Why
The game worked, but it was not deployable with confidence:
index.htmlloadedcdn.tailwindcss.com, the JIT build meant for prototyping. Three more CDNs(two texture hosts, one avatar service) were reached at runtime.
and were rendered into 40x40 boxes.
@types/reactwas never installed. React 19 ships no types, sotscresolved
reacttoanyand verified nothing across the.tsxfiles. Thetypecheck passed because it was not looking.
localStoragecrashed the app. The read sat outside itstryand there was no error boundary, so it surfaced as a blank page.
ariaand zeroroleattributes. The faders weredivs withonPointerDown, so the core mechanic was unreachable without a pointer.npm run checkdid not run the tests.Result
dist/Verification
npm run check(typecheck, tests, build, bundle budget) exits 0.rather than only the local Node 25.
useGameLogicsplit is proven to be a pure move: normalising both sidesfor imports and
exportkeywords, the extracted logic diffs identicallyagainst the previous file.
aria-labelforwarding,role="progressbar",role="alert"or the arrow-key step each fails thesuite, so they can actually detect a regression.
real browser: no console errors, no broken images, fader keyboard control
confirmed end to end.
Notes for the reviewer
Visualizer,Minigames,TutorialOverlay) carry boththeir accessibility changes and their CDN-to-local texture swap in the a11y
commit, because the two touch the same files.
intermediate commits were not each run through the pipeline.
tailwind.config.jscarries asafelist. Three components build class namesat runtime with
String.replace, so the content scanner cannot see them —the comment there names the files to keep in sync.
strictintsconfig, ESLint/Prettier, and a LICENSE.