Troubleshooting
Runbook for the intranet and Laurelin. Start here when something looks down. The fastest way to waste an afternoon is to guess at the layer; this page exists so you check the real one first.
"It's down" is usually not a server outage
The deploy widget on the site only proves the deployer ran and the box is up. It does not prove the app is serving. Check true liveness directly:
systemctl status valinor-intra --no-pager | grep Active # Node app, port 3000
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/ # expect 200
systemctl status cloudflared --no-pager | grep Active # the tunnel
The request path is: browser → Cloudflare → cloudflared tunnel → Node on
localhost:3000. Nginx is on the box but is NOT in that path (see
architecture.md). Before blaming a cache, confirm what
the origin actually serves:
curl -s http://localhost:3000/apps/<page>.html | head -40
If that output is correct, the origin, the tunnel, and Cloudflare are all fine. The problem is then in the browser or in how the page runs, and no redeploy or cache purge will change it. Spend your next move there, not on the server.
Pages are blank; console shows "Cannot use import statement outside a module"
This is the signature of the 2026-06-22 outage. Every in-browser-transpiled page (all the non-Laurelin apps) went blank with that error. Laurelin kept working.
Root cause: the wrapped pages transpile JSX in the browser with Babel standalone
loaded from a CDN. When the CDN serves a Babel build that defaults to the
automatic JSX runtime, Babel compiles <App /> into output that begins with
import { jsx } from "react/jsx-runtime" and injects it as a classic script. The
browser rejects the top-level import. The served HTML is clean. Babel adds the
import at transform time, which is why purging caches and redeploying did nothing.
Laurelin is immune because it is pre-built with esbuild, not transpiled in the
browser.
Confirm it in one line in the browser console on the broken page:
Babel.transform('<a/>', {presets:['react']}).code
// classic (good): React.createElement("a", null)
// automatic (broken): import { jsx as _jsx } from "react/jsx-runtime"; ...
The fix is already in deploy.sh. The page template pins the Babel standalone
version and registers a classic-runtime React preset that the wrapped pages use
via data-presets="react-classic". Classic runtime emits React.createElement
and injects no import. A guard in the wrap loop fails the deploy if any wrapped
page ships without that safeguard, so the protection cannot be dropped by a
careless template edit.
If it ever recurs:
- Run the console one-liner above to see which runtime Babel is using.
- Check the
HEADERtemplate indeploy.shstill has@babel/standalone@<pinned>and thereact-classicpreset. - If the pinned version itself regressed, bump the pin to a known-good Babel version. Do not revert to an unpinned CDN URL.
The durable removal of this whole class of bug is to build these pages with esbuild the way Laurelin is built, so nothing is transpiled in the browser. That is the recommended next step if these legacy pages keep causing trouble.
Deploys only run from main
deploy.sh refuses to run from any other branch:
[deploy] refusing to deploy from branch '<x>' — checkout main first
The box re-deploys main on a timer, so a manual git checkout <branch> && deploy.sh would be reverted on the next cycle anyway. To ship a fix, merge it to
main, then either wait for the timer or force it:
cd ~/valinor-intra && git checkout main && git pull origin main && bash deploy.sh --force
Cloudflare and DNS
Both hostnames route to the live tunnel laurelin (7a19dd55…) per
/etc/cloudflared/config.yml. A second tunnel valinor-intra (152319d5…)
exists but is dead. If a hostname returns Cloudflare errors while Node is healthy,
confirm its DNS CNAME points at the live tunnel:
cloudflared tunnel list # which tunnel has active connections
Always Online should stay off in the Cloudflare dashboard. With it on, Cloudflare
can serve an archived copy of a page when it thinks the origin is down, which is
indistinguishable from a stale cache during a real incident.