Description
## Summary
iPad pairing with Sunshine fails silently when the `sunshine_state.json` named_devices list has accumulated prior entries. Two interacting bugs cause this. **Confirmed affecting all Windows body nodes as of 2026-05-24. All bodies cleared 2026-05-30.**
---
## Root Cause
### Bug A — HydraPairSession uses hardcoded UUID for unpair (hydra-moonlight-ios)
**Diagnosis revised 2026-05-30 — this bug does not exist.** `HydraPairSession.m` uses `@"0123456789ABCDEF"` consistently with `HttpManager._uniqueId`. Both values match, so unpair requests are addressed correctly. `[IdManager getUniqueId]` would return an empty string in this context (key never set), which would make things worse. The original diagnosis was incorrect.
---
### Bug B — named_devices accumulates without bound
Sunshine has no TTL or size limit on `named_devices`. Every pair attempt by any head appends a new entry. Cross-district pairing attempts from cheeky-cactus-86 (bxl1) to cosmic-pretzel-98 (bxl1-test) were one driver, but accumulation also occurs from legitimate re-pair cycles.
Once `sunshine_state.json` grows large enough, the Sunshine web API (port 47990) stops responding with JSON and returns HTML — the `/welcome` first-run setup page. hydrabody then fails with: `parsing apps: invalid character '<' looking for beginning of value` and reports `provider_status: sunshine_api_unreachable`.
**Credential loss on state deletion:** Deleting `sunshine_state.json` also removes the stored web UI credentials. Sunshine reverts to first-run state. The `/api/password` endpoint cannot be used to recover at this point. `sunshine.exe --creds` is the correct tool, but requires a two-pass sequence — see workaround below.
---
## Node State History
| Body | Entries (2026-05-24) | Entries (2026-05-30) | Status |
|------|---------------------|---------------------|--------|
| boom-pickle-38 (node-74f9fbf2) | 294 / 408 KB | 0 | Fixed 2026-05-30 |
| chunky-turnip-23 (node-b961f1c8) | 190 / 253 KB | 0 | Fixed 2026-05-30 |
| fluffy-dumpling-87 (node-11da9ea3) | 94 / 110 KB | ~100+ | Auto-trim active via v2.0.64 |
| cosmic-pretzel-98 (node-4c2be4b0) | 251 / 332 KB | 0 | Fixed 2026-05-24 |
---
## Operational Workaround (per body)
Apply when `provider_status = sunshine_api_unreachable` or iPad pairing silently returns "cert 0 bytes".
**Check first:** verify `stream_status = idle`. Never apply to a streaming node.
```powershell
# 1. Stop Sunshine and clear state
Stop-Process -Name sunshine -Force -ErrorAction SilentlyContinue
Stop-Service SunshineService -Force -ErrorAction SilentlyContinue
Start-Sleep -Seconds 2
Copy-Item C:\Sunshine\config\sunshine_state.json C:\Sunshine\config\sunshine_state.json.bak -Force
Remove-Item C:\Sunshine\config\sunshine_state.json -Force
# 2. First --creds pass (Sunshine stopped)
C:\Sunshine\sunshine.exe --creds sunshine sunshine
# Expected output: "New credentials have been created"
# 3. Wait for hydrabody to restart Sunshine (~30s)
# Sunshine will be in /welcome state at this point — that is expected
# 4. Second --creds pass (Sunshine live)
C:\Sunshine\sunshine.exe --creds sunshine sunshine
# This writes sunshine_state.json with credentials while Sunshine is running
# 5. Kill Sunshine — hydrabody restarts it and picks up the new state
Stop-Process -Name sunshine -Force
```
Verify: `GET /api/v1/nodes/<nodeId>` on hydracluster — expect `provider_status: running`.
**Why two passes:** The first `--creds` run (Sunshine stopped) creates the credential file. Hydrabody then starts Sunshine which initialises but stays in /welcome state because the state file format from `--creds` alone is not sufficient. The second `--creds` run against the live instance writes the full `sunshine_state.json`. A final restart picks it up cleanly.
---
## Operational Fix Log
### cosmic-pretzel-98 — 2026-05-24
- sunshine_state.json: 332 KB, 251 entries
- Cleared state, ran `--creds`, hydrabody restarted Sunshine
- provider_status: running
### boom-pickle-38 — 2026-05-30
- sunshine_state.json: 408 KB, 294 entries — was unstable (sunshine_api_unreachable) during mill opening
- Cleared state, two-pass `--creds`, kill + hydrabody restart
- provider_status: running
### chunky-turnip-23 — 2026-05-30
- sunshine_state.json: 253 KB, 190 entries
- Same two-pass procedure
- provider_status: running
---
## Structural Fix — hydrabody v2.0.64 (shipped 2026-05-30)
Added `TrimPairedClients(10)` to the idle tick in `pkg/provider/sunshine_provider.go`. On each idle API health check (every 30s when not streaming), hydrabody lists paired clients and calls `UnpairAll` if the count exceeds 10. Log line: `[sunshine] trim: N paired clients exceeds threshold 10, unpairing all`.
Heads re-pair automatically on the next session start. No user impact.
**All bodies auto-update within 6 hours of v2.0.64 release.** The manual workaround is no longer needed for bodies running v2.0.64+, and fluffy-dumpling-87 (~100 entries) will be trimmed automatically without manual intervention.
---
## Open Items
1. **Sunshine fork:** Consider adding a max-entry cap or TTL on `named_devices` upstream — belt-and-suspenders protection if hydrabody trim ever misses a node.
2. **hydraheadflatscreen / body discovery:** Investigate why cheeky-cactus-86 (bxl1) was sending pairing requests to cosmic-pretzel-98 (bxl1-test). Body assignment must be district-scoped.