# Explorer frontend 404 fix — runbook **Date:** 2026-03-02 **Issue:** Root path (`/`) at https://explorer.d-bis.org returns 404 "Page not found". **Cause:** Nginx proxies `/` to Blockscout :4000; Blockscout is API-only and has no route for GET `/`. **Status:** ✅ **FIXED** (2026-03-02). Nginx now serves the custom frontend from `/var/www/html` for `/` and SPA paths; `/api/v1/` and `/api/config/*` preserved. All endpoints verified 200. --- ## 1. How the UI is supposed to be served This deployment uses a **custom frontend** (SolaceScanScout), not the built-in Blockscout web UI: - **Static frontend:** Built files under `/var/www/html/` on VMID 5000: - `index.html` (main SPA shell, contains "SolaceScanScout") - `explorer-spa.js`, `favicon.ico`, `apple-touch-icon.png`, `/snap/`, etc. - **Blockscout (port 4000):** API only. The Phoenix router has no route for `GET /`; it serves `/api/*` and returns 404 for `/`. - **Nginx:** Should serve the static frontend for `/` and SPA paths, and proxy only `/api/`, `/api/v1/`, `/api/config/*`, `/health` to the appropriate backends (Blockscout :4000, token-aggregation :3001, or static config files). **Relevant docs/config:** - `explorer-monorepo/scripts/fix-nginx-serve-custom-frontend.sh` — nginx config that serves `/var/www/html` for `/` and SPA paths. - `explorer-monorepo/scripts/fix-nginx-conflicts-vmid5000.sh` — current “conflicts” config: proxies `location /` to :4000 (no static root). - `explorer-monorepo/scripts/deploy-frontend-to-vmid5000.sh` — deploys frontend files and can apply the custom-frontend nginx config. - This runbook replaces ad-hoc 404 notes; use `explorer-monorepo/scripts/` above for nginx and deploy. - `explorer-monorepo/docs/BLOCKSCOUT_START_AND_BUILD.md` — Blockscout container/assets; UI in this setup is the custom frontend, not Blockscout’s own UI. --- ## 2. What we confirmed on VMID 5000 - **Custom frontend present:** `/var/www/html/index.html` exists (~60KB), contains "SolaceScanScout"; `explorer-spa.js`, favicon, `/snap/`, etc. are present. - **Blockscout logs:** For `GET /` to :4000, Blockscout logs: `Phoenix.Router.NoRouteError`, "no route found for GET / (BlockScoutWeb.Router)". So 404 for `/` is expected when nginx sends `/` to Blockscout. - **Live nginx:** HTTPS server block has `location / { proxy_pass http://127.0.0.1:4000; }` with **no** `root` / `try_files` for the frontend. So every request to `/` is proxied to Blockscout and returns 404. Conclusion: the frontend files are in place; the **nginx config** is wrong (proxy-only for `/` instead of serving static files). --- ## 3. Fix: make nginx serve the custom frontend for `/` Apply a config that, for the HTTPS (and optionally HTTP) server block: 1. Serves **`/`** from `/var/www/html` (e.g. `location = /` with `root /var/www/html` and `try_files /index.html =404`). 2. Serves **SPA paths** (e.g. `/address`, `/tx`, `/blocks`, …) from the same root with `try_files $uri $uri/ /index.html`. 3. Keeps **`/api/`**, **`/api/v1/`**, **`/api/config/*`**, **`/snap/`**, **`/health`** as they are (proxy or alias). **Option A — Apply the full custom-frontend script (recommended)** From the repo root, from a host that can SSH to the Proxmox node for VMID 5000 (e.g. r630-02): ```bash # Set Proxmox host (r630-02) export PROXMOX_R630_02=192.168.11.12 # or PROXMOX_HOST_R630_02 # Apply nginx config that serves / and SPA from /var/www/html cd /home/intlc/projects/proxmox/explorer-monorepo # Copy script into VM and run (requires pct exec) EXPLORER_VM_HOST=root@192.168.11.12 bash scripts/apply-nginx-explorer-fix.sh ``` Or run the fix script **inside** VMID 5000 (e.g. after copying it in): ```bash # From Proxmox host pct exec 5000 -- bash /path/to/fix-nginx-serve-custom-frontend.sh ``` **Option B — Manual nginx change (HTTPS server block only)** On VMID 5000, edit `/etc/nginx/sites-enabled/blockscout`. In the `server { listen 443 ... }` block, **replace** the single: ```nginx location / { proxy_pass http://127.0.0.1:4000; ... } ``` with something equivalent to: ```nginx # Serve custom frontend for root location = / { root /var/www/html; add_header Cache-Control "no-store, no-cache, must-revalidate"; try_files /index.html =404; } # SPA paths — serve index.html for client-side routing location ~ ^/(address|tx|block|token|tokens|blocks|transactions|bridge|weth|watchlist|nft|home|analytics|operator)(/|$) { root /var/www/html; try_files /index.html =404; add_header Cache-Control "no-store, no-cache, must-revalidate"; } # All other non-API paths — static files and SPA fallback location / { root /var/www/html; try_files $uri $uri/ /index.html; } ``` Keep all existing `location /api/`, `location /api/v1/`, `location /api/config/`, `location /snap/`, `location /health` blocks unchanged and **before** the catch-all `location /` (so API and config still proxy correctly). Then: ```bash nginx -t && systemctl reload nginx ``` --- ## 4. Verify - From LAN: `curl -sk -H "Host: explorer.d-bis.org" https://192.168.11.140:443/` should return **200** with HTML containing "SolaceScanScout" (or similar), not "Page not found". - Public: `https://explorer.d-bis.org/` should show the explorer UI. - API unchanged: `curl -s http://192.168.11.140:4000/api/v2/stats` and `https://explorer.d-bis.org/api/v2/stats` should still return JSON. --- ## 5. Summary | Item | Status | |------|--------| | How UI is served | Custom static frontend in `/var/www/html/` (index.html + SPA); Blockscout :4000 is API-only. | | Frontend files on VMID 5000 | Present; `index.html` contains SolaceScanScout. | | Blockscout logs for GET `/` | NoRouteError for GET `/` — expected when nginx proxies `/` to :4000. | | Nginx fix | Serve `/` and SPA paths from `root /var/www/html` and `try_files`; proxy only `/api/` (and specific locations) to :4000. | | Script to apply | `fix-nginx-serve-custom-frontend.sh` or `apply-nginx-explorer-fix.sh`; or apply the manual snippet above. | --- ## 6. Completion (2026-03-02) - **Applied:** `apply-nginx-explorer-fix.sh` (via `EXPLORER_VM_HOST=root@192.168.11.12`). - **Script updated:** `fix-nginx-serve-custom-frontend.sh` now includes `location /api/v1/` (token-aggregation :3001) and `location = /api/config/token-list` / `location = /api/config/networks` (static JSON) so config and token-aggregation are not lost on re-apply. - **Verification:** From LAN, all return 200: `/` (frontend HTML), `/api/config/token-list`, `/api/config/networks`, `/api/v2/stats`, `/api/v1/chains`.