# Incident: dwroller.alw.dk blank page (2026-07-04)

## Symptom
`https://dwroller.alw.dk/` loaded a valid HTML shell (correct `<title>`,
correct hashed JS/CSS asset links, all served 200) but the page body was
blank — `<div id="root">` never got populated by React.

## Root causes

Two independent problems, both needed fixing.

### 1. `/nas` autofs idle-unmount (infrastructure)

`/etc/fstab` mounted `/nas` via autofs with `x-systemd.idle-timeout=10min`.
The dwroller app's code, data files, and cwd all live under
`/nas/git/dwroller`, so every ~10 minutes of inactivity autofs silently
unmounted the share out from under the running Node process. PM2 detected
the process as unhealthy ("Process 0 in a stopped status") and force
stop/restarted it every 6–47 minutes (119+ restarts logged in
`pm2.log` / `deathwatch-server-out.log`). Any request during one of these
restart windows got nothing back.

**Fix:** changed the `/nas` line in `/etc/fstab` from an autofs mount with
an idle timeout to a plain persistent NFS mount (kept `_netdev,nofail` so
boot still won't hang if the NAS is down):

```diff
-192.168.1.113:/volume1/data   /nas        nfs  vers=4,noauto,x-systemd.automount,x-systemd.idle-timeout=10min,_netdev,nofail,retry=15,timeo=600,rsize=32768,wsize=32768  0  0
+192.168.1.113:/volume1/data   /nas        nfs  vers=4,_netdev,nofail,retry=15,timeo=600,rsize=32768,wsize=32768  0  0
```

Original backed up to `/etc/fstab.bak.<timestamp>`. Applied live with
`systemctl daemon-reload` + `umount`/`mount /nas` (no reboot needed).

### 2. Frontend JS crash on every page load (application bug)

`src/components/BattlemapTab.jsx` (new component, not yet committed at the
time) did:

```js
const { ... } = require('../shared/battlemapEngine');
```

`../shared/battlemapEngine.js` is plain CommonJS (`module.exports = {...}`),
required directly by the Express backend (`database/routes/*.js`) — that
usage is fine, since backend code runs as real, unbundled Node.

But `BattlemapTab.jsx` is itself an ES module (`import React from 'react'`,
`export default function BattlemapTab(...)`), so it's part of the frontend's
ESM dependency graph. Webpack 5's CommonJS↔ESM interop cannot safely
reconcile a **whole-object** `module.exports = {...}` reassignment with a
consumer inside an ES module graph. Depending on the exact
`require`/`import` style used, this surfaced as one of:

- `Uncaught Error: ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: <id>`
- `Uncaught ReferenceError: exports is not defined`
- a build-time failure: `Attempted import error: 'X' is not exported from '../shared/battlemapEngine'`

All are the same underlying conflict, just different corners of webpack's
static analysis / module concatenation / `hmd` (harmony module decorator)
guard tripping over each other. This is **not** a transient glitch or a
caching issue — every browser hitting the built bundle got a thrown error
before `ReactDOM.createRoot(...).render(...)` ever ran, leaving `#root`
permanently empty.

Confirmed by rendering the production bundle in headless Chromium
(`chromium --headless=old --dump-dom` + `--enable-logging=stderr --v=1` to
capture `console` output) — `<div id="root"></div>` stayed empty and the
uncaught error was visible in the captured console log.

**Fix:** `src/shared/battlemapEngine.js` was left untouched (still plain
CommonJS, still required directly by the backend and by
`src/tests/battlemapEngine.test.js` via Jest/Node). `BattlemapTab.jsx` no
longer requires/imports it at all. The ~10 small pure functions it actually
needed (`battlemapVisibleForPlayer`, `normalizeMovement`,
`normalizeTurnState`, `movementBudget`, `moveWithinRange`, `stepToward`,
`distance`, `enemyMoveTowardNearest`, `sceneBattlemapTheme`, and
`snapCoordinate`, which the component already had a duplicate of) are
defined locally inside the component, with a comment pointing at this
incident/reasoning. This sidesteps the webpack interop bug entirely rather
than fighting it further.

**Known trade-off:** those functions are now duplicated between
`BattlemapTab.jsx` and `src/shared/battlemapEngine.js` (still used by
`database/routes/missionRoutes.js` and `simulationRoutes.js`). If
battlemap movement/theme logic changes, it needs updating in both places —
nothing currently guards against drift between the two copies.

## Verification

- `CI=true npx react-scripts build` compiles cleanly.
- Headless Chromium render of the live production bundle shows `#root`
  populated with the full app shell (nav, login form, tab list including
  "Battlemap") and zero uncaught console errors.
- `node -e "require('./src/shared/battlemapEngine')"` still resolves all
  41 exports for the backend/tests, unaffected by the frontend-side fix.
- `curl https://dwroller.alw.dk/` and `curl http://localhost:5000/` both
  return 200 with the current build's asset hashes.
- PM2 restart count stable (no new restarts) after the `/nas` mount fix.

## Follow-ups (not done as part of this fix)

- Consider whether `src/shared/battlemapEngine.js` should be split into a
  true dual CJS/ESM package (e.g. via a build step) if more frontend
  components need to consume it, instead of duplicating functions again.
- Jest/RTL tests that spin up jsdom (`battlemapTab.test.js`,
  `battlemapEngine.test.js` when run together with DOM tests) hang/segfault
  on this ARM host — verify logic changes via plain `node -e` require checks
  or the built bundle instead of relying on `npm test` here.
