Back to the resultsSource: night-2026-10-11/proof-w2-server.md

W2 lane S4 proof: per-look levels, blackout that keeps the state, haze hold

Branch w2-server-playbacks (worktree ~/trc-lighting/wt-server-playbacks), cut from two-screen 26cdd93. Mac dry run on port 7083 (--dry-run --ui-standalone --camera-from audit-2026-10-10/.camera-scratch); /api/dmx/nodes never called. Frame values read from GET /api/dmx/frame?u=N&from=1&to=512 on universes 1, 8 and 9 (a dry run renders on request). dotnet build -c Release: 0 errors, 0 CS warnings.

1. Per-look levels, HTP

Look A = 786 at level 100, look B = 812 at level 30, then 15 (/api/look 786, /api/scene/812/on, POST /api/look/812/level {v}). Each look's solo dims were read first; expected dim = max(A, B × level) for every fixture either look touches (30 fixtures).

Check Result
level 30: worst |dim − expected| × 255 over 30 fixtures 0
level 15: same 0
par 2201 (A 0.2, B 1.0), level 30 dim 0.3, frame dimmer 76 (B wins)
par 2201, level 15 dim 0.2, frame dimmer 51 (A wins)
par 2206 (A only, 0.2) 0.2, frame 51 at both levels
GET /api/look/levels and /api/status.levels [{id:786,level:100},{id:812,level:15}]
level on a look that is not live (799) 409 look 799 is not live
level without v 400
hard blackout levels [] (reset)
SSE levels events seen 2 (one per change)

2. Blackout that keeps the state (POST /api/blackout {})

State before: 786 @100 + 812 @30, haze cycle on (hazer 921 at u1 ch 509/510 = 128/26). "Light channels" = every Dimmer channel, plus the colour channels of fixtures without a real dimmer and of the Colorband bars (937 channels over u1/u8/u9, 50 lit before).

Check Result
reply mode: keep
highest light channel during blackout 0 (0 nonzero of 937)
other channels changed 36, all Colorband second/third cell triplets (15 → 0), which carry the bars' level. Pan, tilt, colour on dimmer fixtures: unchanged
hazer during blackout (exempt) 128 / 26, unchanged
/api/rig/state live[] [786, 812] before and during
/api/dmx/state dim (targets) 2201 = 0.3, 2206 = 0.2, unchanged
/api/status.blackout.on, /api/rig/state.blackout.on true, true
undo ring (depth + every label/time) before vs after blackout identical
release (POST /api/blackout/release {}): channels differing from before the blackout, all 3 universes, hazer excluded 0 (max diff 0/255)
undo ring before vs after release identical
blackout with fade:1, par 2201 every 100 ms 76 68 60 52 44 36 28 20 12 4 0 0
release with fade:1 0 8 16 24 33 41 49 58 66 74 76 76
POST /api/undo while blacked out 200, stage stays dark (0), blackout.on true
SSE blackout events 8; SSE rig carried blackout.on true and false

3. A hit under the blackout

POST /api/hit/selpulse/press {fixtures:[2201,2202]} (ms-only steps 140/460) during the kept blackout, sampled every 50 ms for 1.3 s:

Check Result
2201 target dim seen 0.25 and 1 (the hit runs)
2201 frame dimmer seen 0 only
after POST /api/hit/release dims 0.3 / 0.3, equal to before the hit
after blackout release 0 channels differ from before the blackout

4. Haze hold

Check Result
default GET /api/haze exempt: true, hazers: [921] (kind hazer in rig.json), written to data/haze.json
{exempt:false} then blackout hazer 0 / 0
{exempt:true}, master 50 hazer 128 / 26; par 2201 76 → 38
{exempt:true}, blackout hazer 128 / 26
SSE haze events 2

5. Hard blackout ({hard:true}) as before

live [], master 100, haze off, blackout.on false, a new undo entry labelled blackout. After it 135 u1 channels read 255; 134 equal the baseline capture data/dmx-baseline-u1.txt (blinder dimmers idle at 255 with emitters off), the other is a seeded/overridden baseline channel. The render path at blackout factor 1 is the old one.

6. Look fades and HTP stacking

Check par 2201 every 100 ms
/api/look 812 fade 1 from dark 0 28 57 84 111 139 166 192 219 245 255 255
812 stacked on 786 (51) with fade 1 51 73 94 117 137 160 181 203 226 247 255 255
re-GO of 786 (0.2) under 812 (1.0) with fade 1 255 throughout, no dip

7. Regress harness

tools/regress-v2.sh 7083 unchanged: RED on mood-solo, blackout, undo. It uses POST /api/blackout as its "clear the stage" step and asserts the old meaning. The same harness with {"hard":true} on its five blackout posts (lines 122, 145, 165, 168, 183): GREEN, 13 passed (stack, mood-solo, hit, blackout, undo, timeline, userscene included).

8. Follow-up after the rebase onto 99f2324

9. Contract flip (conductor, after risk 6): {} is HARD, {keep:true} keeps the state