diff --git a/docs/gateway.md b/docs/gateway.md index e974ec2b..82eb6c59 100644 --- a/docs/gateway.md +++ b/docs/gateway.md @@ -234,6 +234,79 @@ State lives at `/var/lib/nixos-containers/hive-forge/var/lib/forgejo/` and survives container restart / host reboot. To wipe, destroy the container. +## Per-agent static frontend split + +When `services.hyperhive.frontend` is configured, hive-c0re injects +`HIVE_AGENT_FRONTEND_DIR = "${cfg.frontend}/agent"` into its service +environment. The nginx include generator (`gateway_nginx::write`) reads +this variable and, when set, emits split location blocks per agent +instead of the legacy single-proxy block. + +**Location priority for `/agent//...`:** + +```nginx +# 1. Compiled assets — content-addressed nix store path, cache forever +location ^~ /agent//static/ { + alias /static/; + expires 1y; + add_header Cache-Control "public, immutable, max-age=31536000"; +} + +# 2. Static dist + proxy fallback for dynamic paths +location /agent// { + alias /; + try_files $uri $uri.html $uri/index.html @_dynamic; +} + +# 3. Proxy catchall — API, events, icon, send, login, … +location @_dynamic { + proxy_pass ; + proxy_set_header X-Forwarded-Prefix /agent/; + proxy_intercept_errors on; + error_page 502 503 504 = /__hive_agent_unreachable; + # … (full proxy header block) +} +``` + +**`try_files` resolution** (nginx applies the `alias` mapping before +checking file existence): + +| request | resolved | outcome | +| --- | --- | --- | +| `/agent/iris/` | `/index.html` | main agent page | +| `/agent/iris/stats` | `/stats.html` | stats page | +| `/agent/iris/screen` | `/screen.html` | screen page | +| `/agent/iris/static/app.js` | caught by `^~` block first | served with immutable cache | +| `/agent/iris/api/state` | no file match → `@iris_dynamic` | proxied to agent daemon | +| `/agent/iris/events/live` | no file match → `@iris_dynamic` | proxied (SSE) | + +Adding a new HTML page to the frontend dist (`dist/.html`) +automatically makes it reachable at `/agent//` — no +generator change needed. + +**Why `^~` for `/static/`**: the `^~` prefix gives this block higher +priority than the plain prefix `location /agent//`, so compiled +JS/CSS assets skip `try_files` entirely and get the immutable cache +headers. Nix store paths are content-addressed — the hash changes on +any content change — so `max-age=31536000` is safe. + +**Why nix store is reachable from the gateway container**: nspawn +containers bind-mount `/nix/store` read-only by default. The +`HIVE_AGENT_FRONTEND_DIR` path is a nix store path baked in at +hive-c0re build time — the same path is visible to both c0re (writing +`agents.conf`) and the gateway nginx (serving files from it). + +**Graceful degradation**: if `HIVE_AGENT_FRONTEND_DIR` is empty or +unset (e.g. a build that predates `cfg.frontend`), each agent gets the +legacy single-proxy block and all traffic is forwarded to the agent +daemon as before. + +**`extraFiles`**: per-agent `hyperhive.frontend.extraFiles` are in +`mergedDist`, not in the base `cfg.frontend` dist. They are not under +the nix-store `alias` path, so requests for them fall through +`try_files` to `@_dynamic` and are served by the agent daemon +as before. + ## Per-agent error pages `/agent//` requests hit two failure modes; both get static