For Team

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:

  1. Run the console one-liner above to see which runtime Babel is using.
  2. Check the HEADER template in deploy.sh still has @babel/standalone@<pinned> and the react-classic preset.
  3. 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.