From 24775845a3cc61d9ab8ab3a6c908cd8115b48cfe Mon Sep 17 00:00:00 2001 From: atlas Date: Sun, 31 May 2026 15:01:37 +0200 Subject: [PATCH] nix/hive-gateway: static not-found + unreachable pages for /agent// (#755) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mara on #755: "e.g. /agent/name should show an error page stating that the agent could not be found if missing in json or that it is not reachable if we get a connection error. we dont want a fully generic fallback, only for routes already special cased in the nginx config." Adds two static HTML pages built at deploy time via `pkgs.runCommand "hyperhive-agent-error-pages"`: - **not-found.html** — served when `/agent//...` hits the bare `/agent/` catch-all. The catch-all `return 404`s, and `error_page 404 = /__hive_agent_not_found` rewrites to the static page. - **unreachable.html** — served when `/agent//...` proxy_pass to the harness returns 502 / 503 / 504. `proxy_intercept_errors on` + `error_page 502 503 504 = /__hive_agent_unreachable` on each per-agent location block rewrites to the static page. Mechanics: - `agentErrorPagesDir` (in the `let` block) is a `runCommand` that emits two HTML files using a `</` requests hit two failure modes; both get static +HTML pages instead of nginx's default error chrome (#755): + +- **Agent not found** (`/agent//...`) — name isn't in + `agentPortsTable`. nginx's prefix match falls back to the bare + `/agent/` catch-all, which `return 404`s and `error_page 404` rewrites + to `/__hive_agent_not_found` → serves `not-found.html` with a link + back to the dashboard. + +- **Agent unreachable** (`502 / 503 / 504` from `proxy_pass`) — the + per-agent harness isn't responding (container restarting, crash + recovery, etc.). `proxy_intercept_errors on` + `error_page 502 503 + 504 = /__hive_agent_unreachable` rewrites to `unreachable.html`. + +Both pages are built at deploy time via `pkgs.runCommand` (one nix +derivation `hyperhive-agent-error-pages` with `not-found.html` + +`unreachable.html` inside) and served via two `internal` nginx +locations with `alias` to the exact file. `internal` keeps the +files from being directly request-able by operators — only nginx's +own error-handling can reach them. + +Page styling: minimal inline CSS matching the dashboard's catppuccin +palette (`#1e1e2e` bg, `#cdd6f4` text, `#cba6f7` heading). No +dependencies on the frontend dist — these pages render even when +hive-c0re itself is down. + +Scope is intentionally narrow per mara on #755: "only for routes +already special cased in the nginx config". Other gateway routes +(forge / matrix / fluffychat) get nginx defaults — extending the +custom-error pattern there is a separate follow-up. diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix index fba7b60b..26d322a6 100644 --- a/nix/modules/hive-gateway.nix +++ b/nix/modules/hive-gateway.nix @@ -19,6 +19,56 @@ let { } else builtins.fromJSON (builtins.readFile cfg.agentPortsFile); + + # Static error pages for `/agent//` mishaps (#755). Mara's + # call: useful pages instead of nginx's default 404/502 for routes + # we've already special-cased. See `docs/gateway.md::Per-agent + # error pages` for the design rationale + page-vs-status semantics. + agentErrorPagesDir = pkgs.runCommand "hyperhive-agent-error-pages" { } '' + mkdir -p $out + cat > $out/not-found.html <<'EOF' + + + + + agent not found ◆ hyperhive + + + +

◆ agent not found

+

No agent matches the requested /agent/<name>/ path on this hive.

+

Operator: check the agent name in the dashboard — the gateway picks up new agents on the next nixos-rebuild switch.

+ + + EOF + cat > $out/unreachable.html <<'EOF' + + + + + agent unreachable ◆ hyperhive + + + +

◆ agent unreachable

+

The agent's harness web server isn't responding. Container restarting, or the agent crashed.

+

Operator: dashboard → check the container status / journal; the page will recover on retry once the harness is back up.

+ + + EOF + ''; in { # Single nginx in front of every hyperhive web surface — dashboard, @@ -268,8 +318,11 @@ in # per entry in `agentPortsTable`. Trailing-slash pair # strips the prefix; `X-Forwarded-Prefix` lets the # harness build absolute URLs when relative isn't - # enough. See `docs/gateway.md` for the vhost map - # + tuning rationale. + # enough. `proxy_intercept_errors` + `error_page` rewrite + # upstream 502/503/504 (container down / restarting) to + # the static `unreachable.html` instead of nginx's + # default Bad Gateway page (#755). See + # `docs/gateway.md` for the vhost map + tuning. lib.mapAttrs' (name: port: { name = "/agent/${name}/"; value = { @@ -279,9 +332,44 @@ in proxy_set_header X-Forwarded-Prefix /agent/${name}; proxy_buffering off; proxy_read_timeout 1d; + proxy_intercept_errors on; + error_page 502 503 504 = /__hive_agent_unreachable; ''; }; }) agentPortsTable + // + # `/agent/` catch-all (#755): hits when an operator + # requests `/agent//...` — a name not in + # `agentPortsTable`. Without this it falls through to + # `/` (c0re dashboard upstream) which returns 404 + # with no useful context. Custom 404 page instead. + { + "/agent/" = { + extraConfig = '' + error_page 404 = /__hive_agent_not_found; + return 404; + ''; + }; + # Internal static-file locations the error_page + # directives above point at. `internal` keeps + # operators from hitting the file directly (only + # nginx's error-handling can reach it); `alias` + # serves the exact file regardless of request URI. + "= /__hive_agent_not_found" = { + extraConfig = '' + internal; + alias ${agentErrorPagesDir}/not-found.html; + default_type text/html; + ''; + }; + "= /__hive_agent_unreachable" = { + extraConfig = '' + internal; + alias ${agentErrorPagesDir}/unreachable.html; + default_type text/html; + ''; + }; + } // { # Everything else proxies to hive-c0re. Upgrade # headers stay set so SSE (`/dashboard/stream`,