Description
## Background
Portrait-mode experiences (e.g. mercator-talks) require three layers to all agree on the same resolution simultaneously:
1. **Stream encoding** — the Moonlight client (or moonlight-web-stream) must request portrait resolution (1080x1920) from Sunshine
2. **Display resolution** — Sunshine's `dd_manual_resolution` must be set to portrait
3. **App window** — the UE experience must be launched with portrait-aware args (`-ResX=1080 -ResY=1920 -windowed -ForceRes`)
Plus the Virtual Display Driver (VDD) must have portrait resolutions in `vdd_settings.xml` (hydrabody provisions this at startup).
All three must match simultaneously or the stream renders incorrectly: app in wrong resolution, captured at wrong frame size, or stretched in the browser.
## Two paths, same three layers
### hydraheadflatscreen path
- Layer 1 (stream encoding): `moonlight_*.go` directly passes `--resolution 1080x1920` to the Moonlight client CLI args
- Layers 2+3 (display + app): hydrabody owns both — reads orientation from its own experience cache (synced from hydraexperiencelibrary), sets `dd_manual_resolution`, and registers the Sunshine app with portrait launch args
- Result: robust and direct because the flatscreen IS the Moonlight client — layer 1 is always in sync with layers 2+3
### hydraheadwebstream (browser) path
- Layer 1 (stream encoding): webstream passes `orientation: portrait` string to hydraneckwebrtc, which sets `videoSizeCustom: {width: 1080, height: 1920}` in moonlight-web-stream DefaultSettings
- Layers 2+3 (display + app): hydrabody — same as flatscreen path, independently
- Fragility: if hydraneckwebrtc is on an old version, layer 1 silently breaks while layers 2+3 still work. This happened in production: issue #81 showed the Brussels node on v1.10.81 silently stripped the orientation field, producing a portrait body streaming into a landscape WebRTC frame.
## The problem
There is no cross-layer verification. Failures are silent:
- hydraneckwebrtc on old version: layer 1 wrong, layers 2+3 correct — portrait body but landscape WebRTC frame
- hydrabody with stale catalog: layers 2+3 wrong, layer 1 correct — correct stream resolution but app renders at wrong resolution
- Brussels controller not in hydracluster (issue #81): silently different behaviour per district as versions drift
## Proposed implementation
### 1. hydrabody — expose resolved orientation in the register/launch response
When hydraneckwebrtc calls hydrabody to register or launch an experience, hydrabody should include the orientation it actually applied in the response body. This gives hydraneckwebrtc the ability to verify its own layer 1 matches what hydrabody applied to layers 2+3.
### 2. hydraneckwebrtc — cross-layer orientation verification
In `worker.go handleCreateSession`, after calling RegisterSunshineApp (or receiving the hydrabody response), compare `req.Orientation` with the orientation hydrabody reports it applied. Log the result clearly:
```
[session abc] orientation: requested=portrait body_applied=portrait OK
[session abc] orientation: requested=portrait body_applied=landscape MISMATCH — check hydrabody catalog sync
```
Non-fatal: stream proceeds regardless. The log line makes silent mismatches visible.
### 3. hydraneckwebrtc — include orientation in session status response
Surface the orientation that was applied in `GET /session/{id}/status` so upstream callers (hydraheadwebstream) can read it back and use it to inform the browser user.
### 4. hydraheadwebstream — portrait hint in experience selection UI
The `/api/v1/experiences` response already includes `orientation` on every experience object. The experience dropdown in `internal/web/templates/stream.html` ignores it today. Add:
- A portrait badge (e.g. a small vertical icon or label) next to portrait experiences in the dropdown
- After a portrait experience is selected, show a contextual note below the dropdown: "Portrait experience — for the best view, hold your phone or tablet vertically."
Frontend-only change, no server-side changes needed.
### 5. hydraheadflatscreen — CLAUDE.md note
No code changes needed. Document in CLAUDE.md that hydraheadflatscreen controls layer 1 directly, and that any future path through hydraneckwebrtc must explicitly pass the orientation string — the same way hydraheadwebstream does.
## Acceptance criteria
- hydrabody register/launch API response includes `orientation` as actually applied
- hydraneckwebrtc logs orientation agreement/mismatch between layer 1 and hydrabody layers 2+3
- hydraneckwebrtc `GET /session/{id}/status` response includes `orientation` field
- hydraheadwebstream shows portrait badge in experience dropdown
- hydraheadwebstream shows rotate-device hint when portrait experience is selected
- Verified end-to-end: mercator-talks (portrait) and a landscape experience on the same body, both heads
## Affected repos
- `hydrabody` — expose orientation in API response
- `hydraneckwebrtc` — cross-layer orientation log + session status field
- `hydraheadwebstream` — portrait hint in UI
- `hydraheadflatscreen` — CLAUDE.md note only
## Related
- Issue #81 (Brussels node not in hydracluster) is a prerequisite for reliable orientation on the webstream Brussels path — version drift on that node is what triggered this investigation.