Skip to content

Production readiness: self-hosted assets, accessibility, tests and CI - #1

Merged
russofg merged 12 commits into
mainfrom
chore/production-readiness
Aug 16, 2026
Merged

Production readiness: self-hosted assets, accessibility, tests and CI#1
russofg merged 12 commits into
mainfrom
chore/production-readiness

Conversation

@russofg

@russofg russofg commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator

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:

  • All styling depended on a third-party host. index.html loaded
    cdn.tailwindcss.com, the JIT build meant for prototyping. Three more CDNs
    (two texture hosts, one avatar service) were reached at runtime.
  • The published bundle was 19 MB. Four crew portraits accounted for 9.6 MB
    and were rendered into 40x40 boxes.
  • @types/react was never installed. React 19 ships no types, so tsc
    resolved react to any and verified nothing across the .tsx files. The
    typecheck passed because it was not looking.
  • A blocked localStorage crashed the app. The read sat outside its try
    and there was no error boundary, so it surfaced as a blank page.
  • Zero aria and zero role attributes. The faders were divs with
    onPointerDown, so the core mechanic was unreachable without a pointer.
  • No CI, and npm run check did not run the tests.

Result

Before After
dist/ 19 MB 1.6 MB
Runtime third-party origins 4 1 (Google Fonts)
Accessibility attributes 0 93
Tests 117 151
npm advisories 5 (1 critical) 0
Largest source file 2719 lines 1334

Verification

  • npm run check (typecheck, tests, build, bundle budget) exits 0.
  • The full pipeline was also run on Node 20, the version CI and Netlify use,
    rather than only the local Node 25.
  • The useGameLogic split is proven to be a pure move: normalising both sides
    for imports and export keywords, the extracted logic diffs identically
    against the previous file.
  • New tests were mutation-tested — removing aria-label forwarding,
    role="progressbar", role="alert" or the arrow-key step each fails the
    suite, so they can actually detect a regression.
  • Menu, settings persistence across reload, and gameplay were smoke-tested in a
    real browser: no console errors, no broken images, fader keyboard control
    confirmed end to end.

Notes for the reviewer

  • Three components (Visualizer, Minigames, TutorialOverlay) carry both
    their accessibility changes and their CDN-to-local texture swap in the a11y
    commit, because the two touch the same files.
  • Commits are grouped by area and the branch tip is verified; individual
    intermediate commits were not each run through the pipeline.
  • tailwind.config.js carries a safelist. Three components build class names
    at runtime with String.replace, so the content scanner cannot see them —
    the comment there names the files to keep in sync.
  • Deliberately left out: strict in tsconfig, ESLint/Prettier, and a LICENSE.

russofg added 10 commits August 15, 2026 23:59
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.
Copilot AI lite review requested due to automatic review settings August 16, 2026 03:01

Copilot AI 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.

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 check to run typecheck, tests, build, and a bundle-budget gate.
  • Hardens runtime reliability (safe localStorage access + top-level ErrorBoundary) 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 thread utils/safeStorage.ts
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.
@russofg
russofg merged commit d442dbe into main Aug 16, 2026
5 checks passed
@russofg
russofg deleted the chore/production-readiness branch August 16, 2026 03:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants