# DyzineFrame release notes

## 4.1.0.7

**Splash IP = wlan0 admin, not POGCOM route.** Dual-radio: `wlan0` = admin
(e.g. `.148`), `wlan1` = POGCOM (e.g. `.43`). `get_IP()` no longer uses the
UDP-to-10.254.254.254 trick that followed wlan1. Splash + HUD prefer wlan0/eth0.

**Prior:** [4.1.0.6](#4106)

## 4.1.0.6

**HUD last octet follows admin route, not wlan0.** Dual-radio TopBoxes often put
POGCOM on `wlan0` (e.g. `.43`) while SSH/admin is another iface (`.148`). Splash
lower-left now uses the default-route source address (then wlan1/eth0/wlan0).

**Prior:** [4.1.0.5](#4105)

## 4.1.0.5

**ASCII-only operator banners / HUD octets.** HDMI listen/Heltec banners no longer
use em dashes that became mojibake (`â€"`). Unresolved IP last-octet HUD shows
`--` instead of blank or garbage.

**Prior:** [4.1.0.4](#4104)

## 4.1.0.4

**Win11 Supervisor UI cull miss.** `cpu_affinity_control.py` was imported by
`dyzineframe_supervisor_ui.py` but omitted from `build_dyzineframe_to_deploy.ps1`,
so the Alt-Tab Supervisor window crashed on spawn (`ModuleNotFoundError`). Module
is now in the ship set; live OHD hotfix copied alongside this stamp.

**Prior:** [4.1.0.3](#4103)

## 4.1.0.3

**Odometer penny 9→0 carry hitch.** In penny-driven mode, pennies free-ran to
scroll=1.0 then parked slightly high while slower dimes caught up. 9→0 now locks
the penny glyph to the dime cascade phase; the whole carry chain shares the
penny roll rate.

**Prior:** [4.1.0.2](#4102)

## 4.1.0.2

**Odometer dollar slow-start actually held.** Dollars were pre-ramping to a 0.2 s
curve (faster than maxed dimes) before decouple, so they looked dime-fast the
instant they took over. Dollar timer stays pinned slow until dollar-driven;
enter at 1.0 s with 2.5 s ease; visual floor never undercuts maxed-dime roll
(value still +$1/land).

**Prior:** [4.1.0.1](#4101)

## 4.1.0.1

**Odometer dollar-tier polish.** Dollar enter waits until dimes are maxed and idle
(enter ≥601¢ / layout 600); dollars always start at `dollar_roll_sec_min` and ease
faster; dollar→dime handoff waits for full cascade idle; dollar+/tens cascade only
while the driving roll is in flight (fixes dime-at-9 false start / drop-back).

**Prior:** [4.1.0.0](#4100)

## 4.1.0.0

**Odometer dollar-driven catch-up.** When lag reaches dime-maxed band (≥500¢),
dollars drive continuous +$1 lands on independent `dollar_roll_*` (0.50→0.20 s
@800¢) while pennies and dimes stay cosmetic-maxed — same algorithm as the dime
tier, one power up. Stops dollar snaps past `max_lag` when dimes alone cannot
keep throughput. Minor→minor product bump for the new catch-up tier.

**Prior:** [4.0.1.38](#40138)

## 4.0.1.38

**Odometer carry keep-up.** Independent dime rates were correct but layout
`carry_roll_sec_min=4.0` @ `dime_roll_max_lag=900` entered catch-up near-stalled
(~3.6 s/digit), so dimes lagged and dollars snapped (`max_lag`). Carry curve is
now `1.0→0.35 s` @ `500¢`; roll smoothing snaps accelerate / eases decelerate.

**Prior:** [4.0.1.37](#40137)

## 4.0.1.37

**Odometer independent dime/penny rates — actually shipped.** 4.0.1.36 stamped
VERSION but `To Deploy` / SGVI kept an Aug-31 `reusable_odometer` engine (shared
roll timer). This rev restages the fixed engine: dime-driven pennies maxed;
dimes use `carry_roll_*`.

**Prior:** [4.0.1.36](#40136)

## 4.0.1.36

**Odometer: independent dime vs penny roll rates.** While dime-driven catch-up is
active, cosmetic pennies spin at maxed `penny_roll_sec` / floor; dimes (and
dollars+) use the separate `carry_roll_sec` / `carry_roll_sec_min` ramp
(`dime_roll_max_lag_pennies`). Layout keys that were previously ignored for
timing are now live.

**Prior:** [4.0.1.35](#40135)

## 4.0.1.35

**HAT LED2 status (BCM3).** POG-COM-PI-HAT DyzineFrame LED: POST N-count, healthy
250 ms, degraded 150 ms + soft-fault codes (`hat_status_led.py`). Soft-fail instead
of bail for missing progressives (Win), missing required MP4s, and ads `sync.py`
failure (code 10). Pairs EG **4.1.0.12** LED1. No-op on Win11 / no GPIO.

**Prior:** [4.0.1.34](#40134)

## 4.0.1.34

**Boot config guard for Pi HS-idle.** `sync/install_hs_out_idle.sh` no longer
rewrites `/boot/firmware/config.txt` inline; it uses `boot_config_guard.py`
(golden on `/var/lib/ethergold/`, refuse wipe of `vc4-kms-v3d`, early
`ethergold-boot-config-guard.service`). Prefer EG sync copy when present.
Pairs with EG **4.0.0.72**.

**Prior:** [4.0.1.33](#40133)

## 4.0.1.33

**OHD pages link Home.** `/ohd/setup`, `/ohd/modes`, and setup-result pages
include nav back to EtherGold Home (`/`), plus Status where useful.

**Prior:** [4.0.1.32](#40132)

---

## 4.0.1.32

**Soft-import `ohd_win11_ota`.** 4.0.1.31 crashed on Pi
(`ModuleNotFoundError`) because `/ohd/setup` hard-imported the Win11 OTA
helper, which is not in the Pi cull. Pi stubs Updates as non-Win11; Win11
package still ships `ohd_win11_ota.py`.

**Prior:** [4.0.1.31](#40131)

---

## 4.0.1.31

**Local HTTP always-on with EtherGold.** When `ETHERGOLD_HTTP_EXTERNAL=1`,
DyzineFrame always binds `127.0.0.1:31235` (not only for pogcc owner) so
ethergold-http can proxy `/ohd/setup` and `/ohd/modes` on public `:31234`.
Pair with EG **4.0.0.62**.

**Prior:** [4.0.1.30](#40130)

---

## 4.0.1.30

**Stock Pi `prog_display_mode = 4`.** `config.ini`, golden-master, mini, and
strip templates default to activity-weighted dwell (same as Win11). OTA still
preserves an existing site `config.ini`.

**Prior:** [4.0.1.29](#40129)

---

## 4.0.1.29

**Mode 4 activity-weighted dwell.** Skips idle games (score=0); dwell is
proportional to activity score vs the weakest active (base =
`mode_1_display_time`, clamp ×12). Replaces the old leader-margin /
`activity_tiers` focus interrupt. Modes 1–3 unchanged.

**`/ohd/setup` progressive settings.** Edit `prog_display_mode` (1–4) and
`mode_1_display_time` (10–60); persist via `config.ini` + optional restart.

**`/ohd/modes` dashboard.** Live ranks, deltas, planned dwells, and schedule
(`GET /ohd/api/modes-status`, ~3s poll). Linked from setup.

**Prior:** [4.0.1.28](#40128)

---

## 4.0.1.28

**Win11 monitor select.** New `win32_monitor` (empty/`auto`, `primary`,
`secondary`, `0`/`1`/…, or `1920x360`). Empty auto-picks a panel matching
`display_mode` resolution (so MINI uses the 1920×360 strip instead of the
laptop primary). Kiosk HWND follows the same monitor.

**Prior:** [4.0.1.27](#40127)

---

## 4.0.1.27

**Mini overscan asset pack `1920x360-96`.** Shenzhen Link panels crop ~96
top rows. New config `asset_resolution` selects `Assets/**/{name}/` while HDMI
stays 1920×360. Pack is 264px content bottom-justified (black top 96); native
`1920x360` tree left intact for a future panel fix. Template `config.mini.ini`
sets `asset_resolution=1920x360-96`. Gen: `tools/gen_1920x360_assets.py
--out-res 1920x360-96 --top-inset 96 --content-h 264`.

**Prior:** [4.0.1.26](#40126)

---

## 4.0.1.26

**Edge test labeled ticks.** Replaces dense bar-count (moiré / easy to
miscount). Draws `y=0,10,20,…` lines with readable labels — report the first
fully visible `y=`. Splash can look uncropped while full-bleed progressives
clip if splash art has headroom.

**Prior:** [4.0.1.25](#40125)

---

## 4.0.1.25

**Edge ruler readable bars.** Overscan test starts at **y=10** (zero bars);
each next row adds one **3px** white bar with **5px** black gaps. Count bars
on the first visible row → N; first visible y ≈ **10+N**.

**Prior:** [4.0.1.24](#40124)

---

## 4.0.1.24

**Edge test row ruler.** `draw_edge_test` draws a per-scanline white-dot
count (row y has y dots; y=0 blank) so overscan missing top rows can be
counted from the first visible pattern. HDMI on Mini was still 1920×360;
crop is panel overscan, not a wrong mode.

**Prior:** [4.0.1.23](#40123)

---

## 4.0.1.23

**Edge bezel test.** `draw_edge_test=True` fills black and draws a **3px white**
border on all four framebuffer edges (plus `set_mode` / surface size text). Use
to check panel overscan vs true 1920×360. Default off.

**Prior:** [4.0.1.22](#40122)

---

## 4.0.1.22

**Mini progressive BGs inset.** Prior Mini BGs were full-bleed (center-crop of
half-OHD), so every game logo still sat flush to the panel top — digits
shrank in 4.0.1.21 but backgrounds looked unchanged. Regen now fits art into
a **300px** content band centered on 360 (30px top/bottom margin); FR is
1267×300 at `frame_anchor` y=30. `tools/gen_1920x360_assets.py`.

**Prior:** [4.0.1.21](#40121)

---

## 4.0.1.21

**Mini progressive art proportions.** 1920×360 digests had been scaled from
TBX (digits ~220px / ~61% of strip height, `anchor_bottom` 320), so slides
looked too tall and cropped at the top while splash (correct Mini art) looked
fine. Regenerated from OHD 3840 masters: half-X + 1/3-Y layout, center-cropped
BGs, digits ~112×130, `anchor_bottom` 237. Tool: `tools/gen_1920x360_assets.py`.

**Prior:** [4.0.1.20](#40120)

---

## 4.0.1.20

**Mini HDMI 1920×360.** Stock kanshi left Mini panels in **1920×1080**, so the
1920×360 splash sat in the top third. `pi_hdmi_mode.py` writes kanshi + applies
`wlr-randr` before pygame `set_mode` when `display_mode=MINI`.

**Prior:** [4.0.1.19](#40119)

---

## 4.0.1.19

**Dual Pi menu bars.** `labwc-pi` runs `labwc -m` (merge-config). Older
`harden_pi_desktop.sh` copied `/etc/xdg/labwc/autostart` into
`~/.config/labwc/autostart`, so panel / desktop / kanshi / xdg-autostart each
ran twice. Harden no longer copies that file; it strips duplicate launchers if
present. Repair on a live box: empty or delete `~/.config/labwc/autostart` (keep
`/etc`), kill the extra `lwrespawn`/`wf-panel-pi`/`pcmanfm`/`supervisor` pair.

**Prior:** [4.0.1.18](#40118)

---

## 4.0.1.18

**OHD Mini (1920×360).** `display_mode=MINI` (legacy `STRIP` maps to `MINI`).
Template `config.mini.ini`. Floor games: `floor_games_source=local_p3` reads
sibling `EtherGold/floor-games.json`; `http` pulls
`http://{floor_games_http_host}:31234/EtherGold/floor-games.json`. Meters stay
local CSV. Empty trusted list → splash hold (never invent 1–12).

**Prior:** [4.0.1.17](#40117)

---

## 4.0.1.17

**OHD Scan Network finds EtherGold feeds.** Previously only `/Topbox/info.json`
with hostname `topbox*` counted — listen Pis (e.g. `.140`) serving
`/EtherGold/progressives.csv` were invisible. Scan now probes `/health` +
progressives CSV as well; feeds show as mode **FEED**.

**Prior:** [4.0.1.16](#40116)

---

## 4.0.1.16

**Keyboard Q cool-off.** `exit_video()` exits **75**. Supervisor waits
**45 s** (`DYZINEFRAME_OPERATOR_QUIT_SETTLE_SEC`) before fullscreen respawn so
Alt-Tab / Terminal stay usable. Supervisor Start/Restart skips the cool-off.
Healthy non-Q exits still use the short 3 s settle.

**Prior:** [4.0.1.15](#40115)

---

## 4.0.1.15

**`/ohd/setup` 500 fixed.** CSS was injected before `str.format`, so `:root {
--bg: …}` became a format field (`KeyError: '\n  --bg'`). Format placeholders
first, then splice CSS.

**Win11 Supervisor dual-watchdog.** `.119` had both Startup
`DyzineFrame Watchdog.lnk` and task `DyzineFrameWatchdog` — two supervisors,
peer-already-running loops, no Alt-Tab UI. `install_win11_autostart.ps1` now
removes the Startup shortcut when the task registers. Supervisor takes
`run/supervisor.lock` so a second instance exits. UI stderr →
`log/supervisor_ui.log`; brief topmost show before iconify so Alt-Tab sees it.

**Prior:** [4.0.1.14](#40114)

---

## 4.0.1.14

**Win11 last-octet HUD on every screen.** Lower-left overlay no longer
early-returns on `win32`. Pi keeps wlan0; Win11 uses `get_IP()` last octet.
Splash still shows full `local_ip` like the Pi, then the octet HUD via
`_df_present`. Staging pending looks at sibling `DyzineFrame-Staging`
on Win11 (`C:\DyzineFrame-Staging`).

**Win11 Staging / Live / Rollback.** rclone Check →
`C:\DyzineFrame-Staging` from Dropbox `SGVI/DyzineFrame/Win11` (not
pog-atlas.com:51709). Promote snapshots Live → Rollback then Staging →
Live. Rollback restores. `/ohd/setup` Updates card: Check / Promote /
Rollback. `update.bat` = Update Now (stage+promote). Optional
`install_ohd_promote_schedule.ps1` for 03:00 local promote.

**Prior:** [4.0.1.13](#40113)

---

## 4.0.1.13

**Win11 Supervisor stays on the taskbar.** Starts minimized (`iconify`), not
hidden/withdrawn. Dropped `DETACHED_PROCESS` on the UI spawn (that often killed
the taskbar button on kiosk Win11). Sets AppUserModelID; never `-toolwindow`.

**OHD Feed Pi page.** `http://<ohd>:31234/ohd/setup` (and `/` on OHD/STRIP)
lets you type the listen/Topbox Pi IP + port, writes `progressives_source`,
optionally writes `run/supervisor.cmd` restart. Optional LAN scan kept for
Topboxes that answer `/Topbox/info.json`.

**Prior:** [4.0.1.12](#40112)

---

## 4.0.1.12

**P3 udev is VID:PID, never cable serial.** `/dev/ser_DT1` matches FTDI
`0403:6001` only. A serial (`ID_SERIAL_SHORT`) lock worked on one cable
(e.g. FTAMSC5F) and missed every other FTDI (e.g. AV0LTJ6N). Installer no
longer probes or substitutes a serial. Cable need not be plugged to install
the rule.

**Prior:** [4.0.1.11](#40111)

---

## 4.0.1.11

**CPU pin can be turned off.** HDMI Supervisor **CPU pin → Disable** (or
`bash set_cpu_affinity.sh disable`) writes `run/cpu_affinity.disabled` and
live-`taskset`s supervisor + DyzineFrame onto all cores — same as before the
2-core then 3-core pin. Enable restores DF **0–2** (EtherGold core 3). Flag
survives reboot and EtherGold `update.sh`. Windows Supervisor has no CPU pin
control.

**Prior:** [4.0.1.10](#40110)

---

## 4.0.1.10

**P3 screen capture from EtherGold Home.** Field HTTP **P3 capture** writes
`run/p3_grab.request`; DF consumes it (same TARGAFILE as the S key) and
writes a browser-saveable PNG next to the TGA under `~/p3-grabs`. Status
is `run/p3_grab.status.json`. Bank VRAM still freezes ~30–90 s.

**Prior:** [4.0.1.9](#4019)

---

## 4.0.1.9

**Listen can see HDMI game.** DF writes `~/EtherGold/df-displayed-game.json`
(`0` = POG logo) at start and on SDS. Watcher SDS when `current-game.json`
`updated_utc` changes even if `game_index` is unchanged, so listen can
force-republish SuperBall after Promote without a dummy 1→11 nudge.

**Prior:** [4.0.1.8](#4018)

---

## 4.0.1.8

**After splash, apply this-boot `current-game.json`.** Promote / Restart
DyzineFrame used to stay on the POG logo while listen already had SuperBall
(or any 1..16): the watcher records `last_index` on start and only SDS on
an index *change*. Pre-boot leftovers stay unlinked (Lucky Street 08-25).
Fireworks IPC is a separate path and was never the game switch.

**Prior:** [4.0.1.7](#4017)

---

## 4.0.1.7

**Pi HS OUT idle-low at boot on every Pi.** Supervisor harden installs
`sync/install_hs_out_idle.sh`: firmware `gpio=10=op,dl` / `gpio=11=op,dl`
(BCM10 OIS_OUT, BCM11 M-S_OUT) plus a sysinit systemd oneshot. Older
POG-COM-PI-HAT boards do not connect those pads. Rev E+ must never leave the
Pi pin high-Z. Installer drives the pins immediately (no Pi reboot). Pairs
with EtherGold HTTP `ethergold-http-2026-08-29-b`.

**Prior:** [4.0.1.6](#4016)

---

## 4.0.1.6

**Service Requested: early Cancel dismisses.** `/flag/helpreq/0` before 30s
takes HDMI down immediately. Timeout still ends the clip if Cancel never
comes. A Cancel after timeout is ignored (already down; re-arms the next
trigger). Same Heltec assertion cannot retrigger after timeout until `/0`
then a new `/1`.

**Prior:** [4.0.1.5](#4015)

---

## 4.0.1.5

**Service Requested times out at 30s.** Heltec `/flag/helpreq/1` (PIN11 Call
Attendant, same USB path as fireworks) starts `helpreq.mp4`. HDMI drops it
after 30 seconds even if Cancel never arrives. `/flag/helpreq/0` is ignored
for the screen. Heartbeat still reports the Heltec assertion so the same
session cannot retrigger; a new `/1` after `/0` starts another 30s.

**Prior:** [4.0.1.4](#4014)

---

## 4.0.1.4

**Pi owns cabinet identity.** Heltec Setup is Wi-Fi (firmware OTA) only.
Identity Apply (`/config`) stamps `config.ini` asset, theme (1–5), and volume
(0–62). DF USB-pushes `/asset/N` `/theme/T` `/volume/V` after hello and each
heartbeat. Inbound Heltec `/asset` and `/ip` are ignored so a leftover 2701
board cannot overwrite Identity (workshop 50004 vs Atlas 21044). Pairs with
Heltec **`POG_CC_2026082801`** and EtherGold HTTP `ethergold-http-2026-08-28-c`.
Restart DyzineFrame after Apply so HDMI/ads pick up the new asset; Heltec
gets the USB push on the next heartbeat without a Pi reboot.

**MinorFeature: wlan0 last-octet HUD.** Lower-left HDMI shows the Pi admin
Wi-Fi last octet (e.g. `239`, `39`) beside the Staging green discs. Field
**4.0.1.3** did not include this; `.239` had it from a LAN copy.

**Prior:** [4.0.1.3](#4013)

---

## 4.0.1.3

**BugFix: Staging older than Live is not an update.** Workshop LAN can run
ahead of SGVI (`.239` live **4.0.1.2**, Staging still **4.0.0.8**). That used
to set `staging-pending` and the HDMI green disc — and 03:00 would have
promoted the older tree over live. Stage now parks the older Staging into
Rollback, clears pending, and skips the disc. Promote refuses that downgrade
unless `CONFIRM_DOWNGRADE=1`. EtherGold HTTP `ethergold-http-2026-08-26-f`.

**Prior:** [4.0.1.2](#4012)

---

## 4.0.1.2

**BugFix: Heltec USB remounts when either side resets.** Web Restart used to
pulse DTR (and keep a dead ACM FD), which put the S3 shared PHY on JTAG
`303a:1001`. HDMI said Heltec USB Fail; Heltec said `bus=0`. DF now opens CDC
only when `303a:0002` is on the bus, asserts DTR without a False→True reset
pulse, closes a dead FD on I/O error, and `usbreset 303a:1001` (never `0002`)
so OTG remounts. Do not reboot the Pi.

**Prior:** [4.0.1.1](#4011)

---

## 4.0.1.1

**MinorFeature: SDS Enter Game IPC + Supervisor Restart from the field UI.**

Heltec `/sds` (1..16) writes `~/EtherGold/current-game.json` (`source=sds_enter_game`).
Copper gameplay and bank 0x0A do not. HTTP Config/Status **Restart DyzineFrame**
writes `run/supervisor.cmd` `restart` the same way HDMI Supervisor does — kills
`DyzineFrame.py` only; supervisor stays up (DISPLAY stays with it). Do not
`sudo reboot`. HDMI Supervisor also has a Restart button. EtherGold HTTP
`ethergold-http-2026-08-26-e`.

**Prior:** [4.0.0.12](#40012)

---

## 4.0.0.12

**SDS Enter Game writes `current-game.json`.** Heltec `/sds` (1..16) atomically
publishes `~/EtherGold/current-game.json` with `source=sds_enter_game`. Copper
gameplay and bank 0x0A do not. Missing until the first enter-game this boot is
expected on attract. Health UI stamp `ethergold-http-2026-08-26-d` matches that
copy. Watcher `source=current-game` does not echo-write.

**Prior:** [4.0.0.11](#40011)

---

## 4.0.0.11

**HDMI says when `progressives.csv` is missing.** After the 25 s boot grace,
TopBox shows `progressives.csv missing — listen will rewrite` even if
`pog422_listen.py` is still running. Listen-down still uses the older banner.
Digits stay blank until a live CSV (4.0.0.10). EtherGold listen supervisor
bounces listen when the file stays gone (HTTP `ethergold-http-2026-08-26-b`).

**Prior:** [4.0.0.10](#40010)

---

## 4.0.0.10

**No fake $0.00 without a fresh `progressives.csv`.** Missing or empty CSV used
to seed the odometer at 0 pennies. Digit area stays blank (BG + frame only)
until `peek_target` has a live value this boot.

**Prior:** [4.0.0.9](#4009)

---

## 4.0.0.9

**Leave a non-progressive logo when SDS changes games.** Black Gold 21 (game 15)
uses a 120 s Show hold. Attract `/sds` is ~13 s. `play_video` treated abort as
`loop_count=0` + seek frame B, which restarted the hold, so HDMI stayed on 15
while `curr_index` kept cycling. Abort now stops the clip; the main loop plays
the new game. Do not reboot the Pi — Supervisor Stop/Start.

**Prior:** [4.0.0.8](#4008)

---

## 4.0.0.8

**Do not close Heltec ACM0 on the 15 s USB watchdog.** 4.0.0.7 logged
“pulse DTR” but `_close_heltec_cdc()` dropped DTR, so Heltec saw Topbox
disconnect every 15 s with no hello. If `303a:0002` is on the bus, keep the
port open with DTR asserted. Field OTA this stamp.

**Prior:** [4.0.0.7](#4007)

---

## 4.0.0.7

**Lucky Street kiosk + Heltec CDC (workshop; unpublished).** M toggles the
pygame/X cursor so overlays can be clicked. Supervisor quick_crash no longer
sleeps 300 s with a blank UI: red countdown + Start now skips the wait.
`usbreset 303a:0002` is gone (self-powered Heltec does not reboot from the Pi);
HDMI says reboot the Heltec. `run_supervisor.sh` re-applies `harden_pi_desktop.sh`
(labwc/wayfire nm-applet) so OTA promote still kills the POGCOM auth popup.
Listen-fault banner when copper listen is dead (not a fake $0.00 jackpot).

**Prior:** [4.0.0.6](#4006)

---

## 4.0.0.6

**Splash GAMESTATS must not kill HDMI.** 4.0.0.5 `DyzineFrame.py` persisted
`parsed.machine_number` / `parsed.tid_hex` after splash, but a cull left the
older `p3_gamestats.py` dataclass without those fields. Result: splash, then
`AttributeError` and supervisor out-of-order wallpaper. This stamp ships the
matching parser, uses `getattr` for those fields, and catches apply failures
so a GAMESTATS identity mismatch cannot take down the player.

**Prior:** [4.0.0.5](#4005)

---

## 4.0.0.5

**No stale progressives after power-up.** Do not apply leftover
`current-game.json` at boot (that jumped Lucky Street HDMI to SuperBall from
the last office session). Before the odometer loads, unlink
`progressives.csv` / `progressives-sync.json` / `current-game.json` when their
mtime is before this OS boot. Meter stays empty until a live Master 0x0A this
power cycle; game index stays 0 until SDS or a **this-boot** current-game
write. Floor-games.json is kept.

**Production log upload (51709).** Enable/disable is Identity `/config`.
**curl -T** (not rclone, not FTP wput) to `{Enrollment Path}/Logs/{asset}-{UTC}-{product}-{file}.gz`
with Pi `wput.env` Basic auth. Unset asset skips IIS. Unknown asset → `/orphan/{asset}/`.
Identity **Apply** upserts this cabinet’s row in `Enrollment.csv` (machine move) and
`Enrollment.local.csv`. The Pi does not publish Dropbox SGVI. Fixture Dropbox
`logs/N` stays hostname / FIXTURE_POS.

**Prior:** [4.0.0.4](#4004)

---

## 4.0.0.4

**Promote restart is in this published stamp.** `sync/ota_lib.sh`
`ota_restart_dyzineframe()` kills **DyzineFrame.py only**. Supervisor and UI
stay up and respawn HDMI. OTA HTTP has no DISPLAY; killing the supervisor left
a black screen with no watchdog. Field SGVI **4.0.0.3** still had the old
kill-supervisor helper.

`promote_staging.sh` **exits 2** when Staging is on promote-hold (was exit 0,
so the Config UI showed success and did not restart DF).

**Prior:** [4.0.0.3](#4003)

---

## 4.0.0.3

**Promote restart:** `sync/ota_lib.sh` kills **DyzineFrame.py only**. Supervisor
and supervisor UI stay up so they can respawn HDMI. OTA HTTP has no DISPLAY;
killing the supervisor left a black screen with no watchdog.
Workshop notes; **field SGVI 4.0.0.3 still shipped the old helper** — use
**4.0.0.4**.

**HTTPS Date clock fallback** when NTP UDP/123 is blocked. `http_date_sync.py`
every 12 hours (+ 2 min after boot). Pi: `sync/install_http_date_sync.sh`
(system timer; also hooked from first-run `install_script.sh` and
`install_pi_deploy_schedule.sh`). Win11 OHD: `install_win11_http_date_sync.ps1`
as Admin (SYSTEM task; `setup_win11.ps1` / `update_win11.ps1` try to register
it). Skips if NTP is already synced. Does not `reboot`. `synctime.sh` calls
this instead of `ntpdate`.

**Prior:** [4.0.0.2](#4002)

---

## 4.0.0.2

**First field SGVI publish of the OTA stack.** Same product as 4.0.0.1 (hourly
stage, 03:00 America/St_Thomas promote, `/config` Live/Staging/Rollback, two
smaller HDMI discs). 4.0.0.1 was LAN-pushed on the island Pis only and was
**not** published to field SGVI, so hourly rclone could not restage 16-c over
it. This stamp is what Pis should pull after Chris `Publish-Sgvi -Channel field`.

**Prior:** [4.0.0.1](#4001)

---

## 4.0.0.1

**SGVI OTA split.** `build_dyzineframe_to_deploy.ps1` culls To Deploy only — it does
not copy into field SGVI. Field publish is `Publish-Sgvi.ps1 -Channel field` on
Chris's command. Staging pending uses VERSION/MANIFEST; promote/rollback kill
numeric python PIDs (never `pkill -f`). HDMI shows two smaller green discs
(DyzineFrame, EtherGold). `/config` shows Live/Staging/Rollback for both
products. `ROLLBACK.json` on SGVI applies once by id and holds 03:00 promote
of the broken stamp.

**Prior:** [2026-08-22-d](#2026-08-22-d)

---

## 2026-08-22-d

**Enter-game from the menu plays the full logo.** `/sds` from game menu faded
the menu clip then the same loop iteration went straight to the progressive
meter, so the logo never got its own `play_video`. Restart the loop when the
game changes during a clip. Also ignore leftover SDS abort 0/3 while still on
the same game (boot fade leftover) instead of dropping the first event of
every new clip.

**Prior:** [2026-08-22-c](#2026-08-22-c)

---

## 2026-08-22-c

**Logo no longer skipped after splash.** `current-game.json` boot-apply queued
`sds_event=3` (fade-to-black) while nothing was playing; splash did not consume
it, so the first `play_video` (the game logo) aborted on frame 0 and jumped to
the meter. Clear that event after splash and after boot-apply; drop a stale 0/3
on the first frame of a new clip. Staged meter fade-in is time-based and
set-alpha in place (no per-step full-screen copies) so it is less choppy.

**Prior:** [2026-08-22-b](#2026-08-22-b)

---

## 2026-08-22-b

**Enter-game logo plays again** when Heltec SDS 0xA2 names a game DyzineFrame
already has on the progressive meter. `sds_handler` used to ignore `/sds/N`
when `curr_index` already equaled N (`LAST_DISPLAY_STATE==1` then skipped
`play_video`). That happened after `current-game.json` gap-fill or a missed
`/sds/0` on game menu — USB working made the skip obvious. Heltec `/sds`
now retriggers the logo; current-game IPC still only switches on index
change. Watcher starts after `vids[]` load so boot apply is not a no-op.

**Prior:** [2026-08-22-a](#2026-08-22-a)

---

## 2026-08-22-a

**First-run does not `sudo reboot`.** EtherGold HAT/XR/listen do not come
back from a software reboot; continue into normal startup. `/config` and
`/config2` no longer tell the user to reboot the Pi (Supervisor Stop/Start
loads a copied `DyzineFrame.py` on HDMI).

**Prior:** [2026-08-21-a](#2026-08-21-a)

---

## 2026-08-21-a

**First-run is Wi‑Fi only.** OHD does not ask for asset or content URL (none
today; future RG/hurricane spots must be 3840×1080). Cabinet TopBox takes
asset and content host:port from **POG-CC USB** (`/asset`, `/ip`) even before
`CONFIGSET`. Identity QR `/config` is optional. `/config2` now writes
`configset=True` to disk; AP join quotes the PSK and deletes leftover
`Hotspot`/`Hotspot-N` profiles. Content host may be a name or an IP.

**Prior:** [2026-08-16-c](#2026-08-16-c)

---

## 2026-08-16-c

**Stock / TopBox seed:** `sync/seed_from_dropbox.sh` stages SGVI DyzineFrame and
promotes immediately with **no display restart** (Michigan Atlas cards).
`prepare_listen_stock.sh` runs that seed. Field hourly/03:00 Atlantic path
unchanged.

**Prior:** [2026-08-16-b](#2026-08-16-b)

---

## 2026-08-16-b

**Field DyzineFrame publish is `SGVI/DyzineFrame`** (rclone), not the Cursor
`To Deploy` cull. Hourly stage → green pending dot (lower left); promote at
**03:00 America/St_Thomas** or next boot. EtherGold `/EtherGold/config` has
**Update Now** and **Revert to last working**. Win11 package is
`SGVI/DyzineFrame/Win11`. `update.sh` still must not apt full-upgrade
(python 3.11 only; see `2026-08-16-a`).

**Prior:** [2026-08-16-a](#2026-08-16-a)

---

## 2026-08-16-a

**update.sh no longer apt full-upgrade or pip --upgrade** (those rolled OS/Python
and could brick Bookworm DyzineFrame). Shared `pi_python_policy.sh`: require
Python 3.9–3.11; apt missing-deps only with `DF_APT=1`; pip ensure-present only
(`FORCE_PIP_UPGRADE=1` to roll). Heltec COM installer prefers `~/EtherGold/prod`
over workshop `dev`. First-time `install_script.sh` also dropped full-upgrade.

**Prior:** [2026-08-15-a](#2026-08-15-a)

---

## 2026-08-15-a

**First-run setup pages match Status chrome:** `/config` (asset + content URL)
and AP-mode `/config2` (Wi‑Fi SSID/password) use the same dark/gold cards as
EtherGold `/status`. Save and reboot behavior unchanged.

**Prior:** [2026-08-14-h](#2026-08-14-h)

---

## 2026-08-14-h

**Win11 Supervisor UI:** same Alt-Tab window as Pi (Auto-Restart Enable/Disable,
Stop force-kill / Start one-shot). Win11 supervisor now polls hold/cmd like Pi
(does not exit on Disable). Spawns `pythonw` UI when available.

**Prior:** [2026-08-14-g](#2026-08-14-g)

---

## 2026-08-14-g

**Lab OHD progressives_source → pos5 `10.1.10.141`:** Win11 template and
INSTALL-GUIDE lab URL (was `10.1.10.45` after wlan0 admin move). Field Z:
`config.ini` / `config.win11.ini` updated to match.

**Prior:** [2026-08-14-f](#2026-08-14-f)

---

## 2026-08-14-f

**update.sh replaces stale supervisor:** when supervisor scripts are newer than
the running process, kill supervisor + UI + DF, clear lock/hold, then
`nohup ./run_supervisor.sh` (DISPLAY or `:0`; boot delay 0). 
`SKIP_SUPERVISOR_LAUNCH=1` to kill only.

**Prior:** [2026-08-14-e](#2026-08-14-e)

---

## 2026-08-14-e

**update.sh stale supervisor kill:** after deps refresh, if
`dyzineframe_supervisor.py` / `_ui.py` / `run_supervisor.sh` is newer than the
running supervisor process, SIGKILL that supervisor (+ UI). Restart from Pi
desktop `./run_supervisor.sh`. `SKIP_SUPERVISOR_KILL=1` /
`FORCE_SUPERVISOR_KILL=1`.

**Prior:** [2026-08-14-d](#2026-08-14-d)

---

## 2026-08-14-d

**Pi Supervisor Stop = force-kill by PID:** Stop no longer relies on DF
handling Q/SIGTERM. UI and supervisor SIGKILL the lock/child PID so a hung
pygame still dies; hold still blocks auto-restart until Start.

**Prior:** [2026-08-14-c](#2026-08-14-c)

---

## 2026-08-14-c

**Pi Supervisor Auto-Restart semantics:** Enable/Disable = auto-restart policy
only (`watchdog.disabled` persists across reboot; Disable does not kill a
running DF). Start = one-shot session (does not clear Disable). Stop or Q quits
DF. Stop-hold cleared on boot; Disable is not.

**Prior:** [2026-08-14-b](#2026-08-14-b)

---

## 2026-08-14-b

**Pi Supervisor UI controls:** Enable / Disable radios + Stop / Start buttons
(replaces Pause / Resume / Restart). Disable → `watchdog.disabled`; Stop →
`supervisor.hold` (sticky until Start even when Enable). Start clears hold +
disable and starts DF. Win11 unchanged.

**Prior:** [2026-08-14-a](#2026-08-14-a)

---

## 2026-08-14-a

**Pi — DyzineFrame Supervisor UI:** titled Alt-Tab window (starts minimized)
with Pause / Resume / Restart DF. Watchdog waits while `run/watchdog.disabled`
(does not exit) so Resume works; honors `run/supervisor.cmd` restart. Spawned
only on Linux with DISPLAY/WAYLAND. Win11 supervisor behavior unchanged.

**Prior:** [2026-08-12-c](#2026-08-12-c)

---

## 2026-08-12-c

**update.sh:** idempotently add `pogcc_com_owner = dyzineframe` when missing;
sync `ethergold-pogcc-com` via EG install script (auto — only enables when
owner=`ethergold`). Other seats stay on DF CDC after Dropbox pull + update.

**Prior:** [2026-08-12-b](#2026-08-12-b)

---

## 2026-08-12-b

**POG-CC local HTTP when ethergold-http owns 31234:** with
`pogcc_com_owner=ethergold` + `ETHERGOLD_HTTP_EXTERNAL=1`, DF binds
`127.0.0.1:31235` for `/flag` and `/pogcc/hb-state` so
`ethergold-pogcc-com` can notify display without fighting public 31234.

**Prior:** [2026-08-12-a](#2026-08-12-a)

---

## 2026-08-12-a

**POG-CC COM owner kill-switch:** `pogcc_com_owner = dyzineframe|ethergold` in
`config.ini`. When `ethergold`, DF skips Heltec USBCDC thread and boot
GAMESTATS credits — EtherGold `ethergold-pogcc-com.service` owns CDC + P3
`/credits`. New `GET /pogcc/hb-state` for EG heartbeat replies. Canary: set
pos5 to `ethergold` after installing the EG unit (order: DF first, then EG).

**Prior:** [2026-08-10-b](#2026-08-10-b)

---

## 2026-08-10-b

**CPU set (Pi 5):** exclusive core‑2 pin starved fireworks. DF now uses
`OHD_CPU_DF_SET=0-2` (all cores except EG core 3) via `cpu_affinity.sh` /
EtherGold `cpu_layout.sh`. EG remains exclusive on **CPU3**.

**Prior:** [2026-08-10-a](#2026-08-10-a)

---

## 2026-08-10-a

**CPU lane (Pi 5):** `runit.sh` / `run_supervisor.sh` pin DyzineFrame to **CPU2**
via `taskset` when `nproc>=4` (`cpu_affinity.sh`; optional override `OHD_CPU_DF`).
Pairs with EtherGold **CPU3** + IRQ affinity (`install_cpu_affinity.sh`). No-op on
thin CPUs or without `taskset`. Disable: `OHD_CPU_ENABLE=0`.

**Prior:** [2026-08-07-c](#2026-08-07-c)

---

## 2026-08-07-c

**Telemetry path/diagnostics:** prefer `/home/topbox/EtherGold` for
`status/pogcc-telemetry.json` (same root as EG). Log successful write once/min;
log write failures with target path. Helps when machine-status shows
`pogcc: null` while progressives update.

**Prior:** [2026-08-07-b](#2026-08-07-b)

---

## 2026-08-07-b

**POG-CC `/telemetry` → durable EtherGold status:** every ~5s Heltec sends power
rails (4× V/I/P), upper/lower cab temps, credit balance, and game menu. DyzineFrame
writes `~/EtherGold/status/pogcc-telemetry.json` (outside tip trees that
`update.sh` overwrites). EtherGold merges into `status/machine-status.json` with
progressives + last progressive hit. Temps no longer spam Serial once on FW
`POG_CC_2026080706+`.

**Prior:** [2026-08-07-a](#2026-08-07-a)

---

## 2026-08-07-a

**POG-CC `/hello` → EtherGold gap-fill arm:** Heltec USB-up sends
`/hello/<fw>/<local_ms>/<utc_ms>`. DyzineFrame writes
`~/EtherGold/pogcc-boot-arm.json` so the bridge force-publishes
`current-game.json` on the **next local play** (even if index unchanged).
SDS `/sds` enter-game remains primary when live. Does not set curr_index
from `/hello` alone.

**Prior:** [2026-08-06-a](#2026-08-06-a)

---

## 2026-08-06-a

**Fireworks exit crossfade:** natural end of celebration no longer hard-cuts back
to ads/progressives. Same alpha crossfade cadence as the entry fade — last
fireworks frame (software path) or black handoff after mpv → last progressive
snapshot when available, else black. Door/attendant still interrupt immediately.

**Prior:** [2026-08-05-a](#2026-08-05-a)

---

## 2026-08-05-a

**P3 `S` grab naming:** files land under `~/p3-grabs/posN/` and sync to
Dropbox `Screenshots/P3 Grabs/posN/` as
`T340_pos{N}_tid{TID}_{UTC}.tga` (+ `_raw.bin`). Pos from `FIXTURE_POS` /
hostname; TID from `LOCAL_TID` / `P3_GRAB_TID` / `FIXTURE_TID` (else `tidunk`).
Update `EtherGold/sync/p3_grab_sync.sh` on the Pi for path-preserving rclone.

**Prior:** [2026-08-04-a](#2026-08-04-a)

---

## 2026-08-04-a

**Odometer dollar+ scrolls with dime catch-up:** while lag-gated dimes drive
+10¢ lands, digits that change on that land (dollars, tens, …) roll in sync
again — was snap-only after 9→0 on lower places (e.g. `$2.90`→`$3.00`). Hold
the independent dime land until those higher cascades finish.

**Prior:** [2026-08-02-h](#2026-08-02-h)

---

## 2026-08-02-h

**POG-CC CDC reply tags (timesync skew fix):** Heartbeat replies are
`HB,<pennies>,…,<utc_ms>`; `/credits` replies are `CR,<pennies>`. Stops Heltec
from pairing a late/orphan credits line (or a stale heartbeat after timeout)
with `millis_now`, which made SDS error stamps ~8 s early vs EG. Needs Heltec
`POG_CC_2026080201` topbox drain/parse update (same day).

**Prior:** [2026-08-02-g](#2026-08-02-g)

---

## 2026-08-02-g

**POG-CC door stamps + CR fix:** Log `POG-CC door open|close` with
`pogcc_utc_ms` / `pogcc_local_ms` (same shape as SDS error). Strip `\r` on
USBCDC lines so the trailing local_ms field parses. Error clear also logged.

**Prior:** [2026-08-02-f](#2026-08-02-f)

---

## 2026-08-02-f

**POG-CC timesync + SDS error stamp:** Heartbeat reply appends Pi UTC ms
(6th CSV field) so Heltec can map `millis()` → wall time. USBCDC
`/flag/error/1/<utc_ms>/<local_ms>` logs `POG-CC SDS error pending` with
Pi receive time (pairs with firmware `POG_CC_2026080201`). Older Heltec
still accepts 5-field replies if stamps omitted on error clear.

**Prior:** [2026-08-02-e](#2026-08-02-e)

---

## 2026-08-02-e

**Dime catch-up polish:** enter at **$1** (100¢, was $2). Soft handoff waits
for cosmetic pennies to land on the settled digit with scroll idle — no
mid-roll digit/scroll reset when control returns to pennies.

**Prior:** [2026-08-02-d](#2026-08-02-d)

---

## 2026-08-02-d

**Cosmetic pennies free-spin 0–9** while dime-driven (was stuck animating
settled `0→1` then reset because value stays on a dime boundary).

**Prior:** [2026-08-02-c](#2026-08-02-c)

---

## 2026-08-02-c

**Dime catch-up taper + soft handoff:** dime-driven rolls follow the same
lag→`roll_curve_sec` taper as pennies (no longer locked at max floor). Stop
starting dimes one dime above the exit band; never snap remainder to target on
land. Fixes max-rate-all-the-way + snap when control returned to pennies.

**Prior:** [2026-08-02-b](#2026-08-02-b)

---

## 2026-08-02-b

**Pi boot crash fix:** hard-import of `get_ffmpeg_loglevel` / `set_ffmpeg_path`
(from newer pyvidplayer2) killed pos5 on field `0.9.29` — ImportError before
splash. Those helpers are now optional with stubs; Win11 path pinning unchanged
when the symbols exist.

**Prior:** [2026-08-02-a](#2026-08-02-a)

---

## 2026-08-02-a

**Odometer dime-driven catch-up:** when lag ≥ `dime_catchup_enter_pennies`
(default 200¢), dimes advance on their own schedule (+10¢ per aesthetic-max
digit roll) while pennies keep a readable 1¢/roll floor — not blur, not
multi-cent penny steps. Hand back to pennies below `dime_catchup_exit_pennies`
(80¢). Softened lag-10 roll bump; Shared layouts 1920×1080 / 3840 / strip.
Debug HUD shows `drive dime|penny`.

**Prior:** [2026-08-01-t](#2026-08-01-t)

---

## 2026-08-01-t

**Pi strip 1920×360:** new `display_mode=STRIP` (OHD progressive behavior at
1920×360). CLI art under `Assets/**/1920x360` from 3840/1920 masters
(`tools/gen_1920x360_assets.py`); BG+FR like 3840 (no BL). Template
`config.strip.ini`. HUD Y uses `SCREEN_HEIGHT` (no hardcoded 1064).

**Prior:** [2026-08-01-s](#2026-08-01-s)

---

## 2026-08-01-s

**Ship cut:** `TEMP_BOOT_FIREWORKS_TEST` off (no false boot celebration). Keep mpv
D3D11 fireworks present + centered `\an5` jackpot undulation for real wins.

**Prior:** [2026-08-01-r](#2026-08-01-r)

---

## 2026-08-01-r

**Jackpot center + undulate:** osd-overlay ignores ASS `\t` timing (no motion on
`-q`). Back to live `\fscx`/`\fscy` IPC; drop `\pos` (OSD PlayRes mismatch shoved
text right/down) and center with `\an5` + `\N` stack. FPS/hwdec still log-only.

**Prior:** [2026-08-01-q](#2026-08-01-q)

---

## 2026-08-01-q

**Jackpot overlay cleanup:** `osd-overlay` was fed full `Dialogue:` lines, which
mpv drew as literal text and shoved the jackpot right. Bare ASS events only;
undulation via libass `\t` (smooth, no 20 Hz rewrite); FPS/hwdec HUD log-only.

**Prior:** [2026-08-01-p](#2026-08-01-p)

---

## 2026-08-01-p

**Jackpot undulation fix:** live `sub-scale` did not visibly move ASS. Jackpot
text is now an `osd-overlay` rewritten ~20 Hz with `\fscx`/`\fscy` sine
(±12%, ~2.2s). Persistent IPC pipe; one-shot log of overlay IPC result.

**Prior:** [2026-08-01-o](#2026-08-01-o)

---

## 2026-08-01-o

**mpv start fix:** `-n`/`-m` used invalid `--background=0.0/0.0/0.0` and
`--alpha=no` on mpv 0.41 → fatal exit → software fallback (no undulation).
Now `--background=color --background-color=#FF000000`; log mpv stderr on
early exit.

**Prior:** [2026-08-01-n](#2026-08-01-n)

---

## 2026-08-01-n

**Jackpot text undulation:** live mpv `sub-scale` IPC sine (±6%, ~2.4s period)
on both ASS lines (JACKPOT WINNER + amount). TEMP FPS OSD unchanged.

**Prior:** [2026-08-01-m](#2026-08-01-m)

---

## 2026-08-01-m

**mpv handoff seam:** black pygame cover before start / before quit; wait for
`vo-configured`; stop re-asserting pygame topmost during playback (was fighting
mpv ontop and flashing desktop). Field saw ~59 fps with `hwdec=d3d11va` on `-l`.

**Prior:** [2026-08-01-l](#2026-08-01-l)

---

## 2026-08-01-l

**OHD fireworks GPU present:** Win11 OHD plays fireworks via bundled
`runtime\mpv\mpv.exe` (`--hwdec=d3d11va --vo=gpu --gpu-api=d3d11`) — D3D
textured quad, not FFmpeg→pygame. Jackpot amount via ASS; TEMP FPS HUD via
IPC shows real `hwdec-current` / `vo`. Soft-fallback to pyvidplayer2 if mpv
missing. Boot fireworks test still on.

**Prior:** [2026-08-01-k](#2026-08-01-k)

---

## 2026-08-01-k

**False-crash fix:** second launcher / second supervisor hit singleton
"already running" (exit 1) → supervisor treated as quick_crash → 300s OOO
backoff while a healthy instance could still be running. Singleton now exits
**2**; supervisor retries quietly (no OOO, no crash count). Remaining
em-dash prints sanitized for cp1252. Boot fireworks + FPS HUD still on.

**Prior:** [2026-08-01-j](#2026-08-01-j)

---

## 2026-08-01-j

**Crash fix:** `2026-08-01-i` printed a Unicode arrow in the hwaccel-off log line;
OHD `log.txt` is cp1252 → `UnicodeEncodeError` at boot (desktop / quick_crash).
ASCII-only for that print. Boot fireworks + FPS HUD still on.

**Prior:** [2026-08-01-i](#2026-08-01-i)

---

## 2026-08-01-i

**d3d11va→pygame closed:** HW decode into raw BGR + pygame blit ran ~11 fps
(worse than ~17 software). `TEMP_FFMPEG_HWACCEL_TEST` off; boot fireworks +
FPS HUD remain. Next: GPU present (mpv/D3D quad), not CPU download.

**Prior:** [2026-08-01-h](#2026-08-01-h)

---

## 2026-08-01-h

**Intel HW decode try:** probe `d3d11va` then `qsv` on full `fireworks.mp4`;
patch pyvidplayer2 FFmpeg reader (stock only knew CUDA); force `READER_FFMPEG`
when armed. FPS HUD shows `NN.N fps d3d11va|qsv|sw`. Boot fireworks still on.

**Prior:** [2026-08-01-g](#2026-08-01-g)

---

## 2026-08-01-g

**Half-res fireworks A/B closed:** decode-half + Lanczos upscale to 3840×1080
ran ~9.5 fps — worse than native full-res (~16–17). `TEMP_FIREWORKS_HALFRES_TEST`
off; boot fireworks + FPS HUD remain. Next path: Intel HW decode (d3d11va/qsv).

**Prior:** [2026-08-01-f](#2026-08-01-f)

---

## 2026-08-01-f

**TEMP half-res fireworks fill screen:** half-res A/B was incorrectly playing
native 1920×540 centered. Now Lanczos-upscales to 3840×1080 after load so the
test covers the same footprint as full-res (decode half pixels, display full).

**Prior:** [2026-08-01-e](#2026-08-01-e)

---

## 2026-08-01-e

**TEMP half-res fireworks A/B:** OHD uses
`Assets/Shared/3840x1080/fireworks_half_lanczos.mp4` (1920×540, Lanczos from
full clip) when `TEMP_FIREWORKS_HALFRES_TEST`. Full `fireworks.mp4` untouched
for later HW-decode retry. Boot fireworks + FPS HUD still on.

**Prior:** [2026-08-01-d](#2026-08-01-d)

---

## 2026-08-01-d

**TEMP FPS HUD:** during fireworks (while `TEMP_BOOT_FIREWORKS_TEST`), small
green `NN.N fps` in the upper-left for OHD frame-rate tuning. Boot fireworks
trigger kept on.

**Prior:** [2026-08-01-c](#2026-08-01-c)

---

## 2026-08-01-c

**Win11 FFmpeg hardened:** OHD `2026-08-01-b` logged *no Gyan/bundled ffmpeg*
(WinGet Packages not visible to the OHD user) and still hit Access Denied.
Now: search + **CreateProcess `-version` probe**, prefer
`runtime\ffmpeg\bin\ffmpeg.exe`, copy a verified Gyan binary into that folder
when needed, supervisor child PATH fronts the same bin. Field OHD got a local
copy of Gyan 8.1.2 under `C:\DyzineFrame\runtime\ffmpeg\bin\`. TEMP boot
fireworks test still on.

**Prior:** [2026-08-01-b](#2026-08-01-b)

---

## 2026-08-01-b

**Win11 FFmpeg symlink fix:** pyvidplayer2 was spawning `ffmpeg` via
`WinGet\Links` symlinks; on OHD that fails with *symlink type is disabled* /
`PermissionError` → static fireworks + overlay. Now pin absolute
`Gyan.FFmpeg_...\bin\ffmpeg.exe` (+ ffprobe) via `set_ffmpeg_path` and put
that real `bin` first on PATH in `run_win11*.bat`. TEMP boot fireworks test
still on (`TEMP_BOOT_FIREWORKS_TEST`).

**Prior:** [2026-08-01-a](#2026-08-01-a)

---

## 2026-08-01-a

**TEMP OHD fireworks boot test:** on `display_mode=OHD`, queue
`fireworks.mp4` + amount overlay (~20s) immediately after splash so FFmpeg
video path can be verified by quit/relaunch. Flag
`TEMP_BOOT_FIREWORKS_TEST` — remove after video works.

**Prior:** [2026-07-31-c](#2026-07-31-c)

---

## 2026-07-31-c

**Assets:** VAN `2026-07-31 set` — game **03** Wild Jokers art (`03-BG` /
`03-BL` 1920 + `03-BG` 3840). Layouts already matched; kept current Shared/01
odometer layouts (VAN curves older). Game 15 art unchanged vs prior merge.

**Prior:** [2026-07-31-b](#2026-07-31-b)

---

## 2026-07-31-b

**Win11 wallpaper / OUT OF ORDER restore:** OOO no longer saves Windows Spotlight
(`IrisService\…jpg`) as the “original” desktop. Enter/exit only restore art under
`C:\DyzineFrame\` (normally `Assets\Shared\3840x1080\pot_dyzine_logo.png`), and
Spotlight delivery knobs are disabled on ensure/OOO so room OHDs match the office
box instead of flashing a random Bing photo between crash-backoff cycles.

**Prior:** [2026-07-31-a](#2026-07-31-a)

---

## 2026-07-31-a

**Win11 fireworks static-frame fix:** `run_win11.bat` no longer prepends
WindowsApps Store stubs (they return `PermissionError: Access is denied` when
pyvidplayer2 spawns FFmpeg, so jackpots showed the amount overlay on a frozen
frame). PATH now matches the supervisor launcher: real Python + WinGet FFmpeg
Links (Program Files and `%LocalAppData%\Microsoft\WinGet\Links`).

**Assets:** Merged VAN drop — game **15** (Black Gold 21) 1920 art + generated
1920/3840 `layout.json` and upscaled 3840 BG. Existing game PNG/layouts were
already byte-identical; kept current Shared odometer curves (VAN Shared layouts
were older chase/roll tuning).

**Prior:** [2026-07-30-d](#2026-07-30-d)

---

## 2026-07-30-d

**Incomplete P3 GAMESTATS responses can no longer become floor lists.** Both the
direct DyzineFrame reader and EtherGold's daily floor-list refresh require the
serial response to end with the complete `END` terminator. The shared GAMESTATS
parser also requires exactly 16 recognizable game rows. A timeout, truncation, or
overlong/misaligned response is rejected; the existing known-good floor cache is
preserved instead of being replaced by a short list such as the suspect `[2, 3]`
captured on pos5 this morning.

**Prior:** [2026-07-30-c](#2026-07-30-c)

---

## 2026-07-30-c

**OHD source returns to the office Pi pos5 (`10.1.10.45`).** Only pos5 has the P3
cable, so it is the only host that can publish a real GAMESTATS floor list; it
currently serves `progressive_floor_games [2, 3]` with `source p3_gamestats`. The
star Master pos3 has no P3 or SDS attached and could only ever guess.

**No host may invent a floor list any more.** An unconfigured
`progressive_floor_games` used to expand to games 1-12, which put games on the OHD
that are not progressives on the floor. Empty config now stays empty, Black Gold 21
(game 15) is stripped from every list regardless of origin, and an OHD that ends up
with no valid floor games holds the splash screen with a short error naming the URL
it could not get a list from, instead of rotating through invented games.

**Known unrelated defect:** `Assets/03` ships a blackjack background with no game
logo, so game 3 (Wild Jokers Poker) renders as what looks like Black Gold 21. That
is an artwork problem, not a floor-list problem, and needs a corrected asset.

**Prior:** [2026-07-30-a](#2026-07-30-a)

---

## 2026-07-30-a

**OHD source moved to the star Master:** Win11 defaults and validation guidance
now use pos3 (`10.1.10.232`) for progressives, floor-games, and fireworks HTTP.
This removes the OHD dependency on pos5's optional POG-CC wlan0 address.

**Prior:** [2026-07-29-f](#2026-07-29-f)

---

## 2026-07-29-f

**OHD fireworks semantics cleanup:** Progressive-win fireworks now play even when
the OHD health gate would otherwise show PLEASE WAIT. Bank progressive wins on
OHD always show the JACKPOT WINNER amount overlay and never defer Call Attendant.
Win11 OHD no longer tries to delete the Pi's `fireworks-request.json` after
playback. Freshness window for remote OHD polls is 45 s to tolerate modest clock
skew. Overlay band follows the loaded clip size (full-width when re-rendered).

**Prior:** [2026-07-29-e](#2026-07-29-e)

---

## 2026-07-29-e

**Progressive-win fireworks on Win11 OHD:** OHD derives
`/EtherGold/fireworks-request.json` from its existing `progressives_source`,
polls the office Pi every 0.5 seconds, deduplicates by request sequence, and
plays `Assets/Shared/3840x1080/fireworks.mp4` full-width. The supplied clip is
encoded 3840×810, so it is vertically centered on the 3840×1080 canvas. Local
non-progressive cash-win requests are ignored on OHD; bank progressive requests
use the existing duration and jackpot-overlay behavior. Pi Topbox playback
remains local-file IPC with `content/fireworks.mp4`.

The built-in DyzineFrame HTTP fallback and external `ethergold-http.service`
both serve the same request endpoint. A request already present when OHD starts
is used only as the sequence baseline and is not replayed.

**Prior:** [2026-07-29-d](#2026-07-29-d)

---

## 2026-07-29-d

**PermDevMapper stopped-status fix:** Escape systemd percent expansion in `pogcc-permdev.service` so `ExecStopPost` writes valid `mapper_stopped` JSON instead of expanding `%s` to `/bin/bash`.

**Prior:** [2026-07-29-c](#2026-07-29-c)

---

## 2026-07-29-c

**PermDevMapper deployment and idle-client fix:** The DyzineFrame rclone whitelist now ships `pogcc-permdev.service` and `udev/99-pogcc-ser-dt1.rules.in`. The mapper also treats Linux PTY-master `EIO` while DyzineFrame has not opened `/dev/pogcc` as an idle app endpoint, not a lost physical POG-CC link.

**Prior:** [2026-07-29-b](#2026-07-29-b)

---

## 2026-07-29-b

**POG-CC PermDevMapper:** On Pi, DyzineFrame opens `/dev/pogcc` (persistent PTY) instead of the raw FTDI `/dev/ttyUSB0`. systemd unit `pogcc-permdev.service` owns `/dev/ser_DT1` (udev → FTDI **0403:6001** VID:PID, never serial), keeps the PTY up across cable/USB loss, and writes `/run/dyzineframe/pogcc_link.status`. DyzineFrame polls that status file for the `Warning: Connection to POG-CC Lost` banner. Install/upgrade: `bash install_pogcc_permdev.sh` (also called from `install_script.sh` / `update.sh`). Pre-mapper benches still fall back to `/dev/ttyUSB0` if `/dev/pogcc` is missing.

**Prior:** [2026-07-29-a](#2026-07-29-a)

---

## 2026-07-29-a

**POG-CC link loss is now survivable:** `/dev/ttyUSB0` open/reopen/write/read failures no longer leave a dead `P3Cable` with an unguarded `.open()` that could raise out of the GAMESTATS path. Losing the cable flags the link down, retries the open every 5 s in a daemon thread (open only — no GAMESTATS, so the T340+ is never blocked by retries), and draws a small `Warning: Connection to POG-CC Lost` banner top-left over playback, progressive, and splash screens. Banner clears and logs `POG-CC link restored` when the port reopens. Gated by `TEST_MODE_NO_SERIAL`, so Win11 OHD nodes never show it.

Note: this covers cable, power-cycle, and USB re-enumeration loss. It cannot survive `fuser -k` (SIGKILL) from the EtherGold driver reload on a shared Pi.

**Prior:** [2026-07-27-a](#2026-07-27-a)

---

## 2026-07-27-a

**EtherGold current-game IPC:** Watch `~/EtherGold/current-game.json` (inotify+mtime) and call `sds_handler` when `game_index` changes — same path as USBCDC `/sds` (last writer wins). Boot applies persisted file so a Pi reset mid-game recovers logo/progressive without a new SDS. Disable with `DYZINEFRAME_CURRENT_GAME_IPC=0`.

**Prior:** [2026-07-19-a](#2026-07-19-a)

---

## 2026-07-19-a

**Final 3840×1080 shared boot art:** Replaced placeholder `Assets/Shared/3840x1080/pot_dyzine_logo.png` (wallpaper) and `splash1.png` with full-resolution masters from Working Graphics (true 3840×1080, not chat-downsized).

**Prior:** [2026-07-18-a](#2026-07-18-a)

---

## 2026-07-18-a

**Resolution-specific boot art:** Wallpaper and splash images now live under `Assets/Shared/{resolution}/`. The supervisor enforces the 1920x1080 Pi or 3840x1080 Win11 wallpaper before every app start, and DyzineFrame loads/scales the matching shared splash.

**Prior:** [2026-07-17-b](#2026-07-17-b)

---

## 2026-07-17-b

**Win11 OHD (Y: room PC):** After Win11 update, machine-wide Python was gone. Supervisor bat now also prefers `C:\DyzineFrame\runtime\Python311\python.exe` (bundled runtime pushed over the share) before Program Files / WindowsApps. Same Store-stub PATH fix as `-a`.

**Prior:** [2026-07-17-a](#2026-07-17-a)

---

## 2026-07-17-a

**Win11 OHD autostart:** `run_win11_supervisor.bat` now uses absolute `Python311`/`Python312` under Program Files instead of putting `%LocalAppData%\Microsoft\WindowsApps` first. Store `python.exe` stubs under the OHD profile were shadowing real Python, so the logon watchdog exited with no `supervisor.log` update after switching autologon from `chris` to `OHD`.

**Prior:** [2026-07-16-d](#2026-07-16-d)

---

## 2026-07-16-d

**Install/update ecosystem:** First-time `install.sh` (alias of `install_script.sh`) and `update.sh` now share `ensure_pi_desktop.sh` (autostart, wallpaper, chmod, harden). Install includes **pygame**. `update.sh` clearly does deps+desktop only (not Dropbox file sync; not EtherGold tip). `SKIP_APT=1` supported for faster refreshes.

**Prior:** [2026-07-16-c](#2026-07-16-c)

---

## 2026-07-16-c

**harden_pi_desktop.sh bash fix:** Pi bash rejected `[[ =~ --profile[= ](...) ]]` (`syntax error in conditional expression` at line 122) and aborted before finishing USB/WiFi hardening. Profile parse now uses a `case` statement.

**Prior:** [2026-07-16-b](#2026-07-16-b)

---

## 2026-07-16-b

**USB insert dialog still popped after harden:** `harden_pi_desktop.sh` now also sets `libfm` `autorun=0` (the real source of “Removable medium is inserted”), patches `/etc/xdg/pcmanfm/*/pcmanfm.conf`, and reloads the running `pcmanfm --desktop` so the change applies without a reboot.

**Prior:** [2026-07-16-a](#2026-07-16-a)

---

## 2026-07-16-a

**Pi desktop dialogs blocked:** New `harden_pi_desktop.sh` (also run from `install_script.sh`) suppresses WiFi password re-prompt dialogs after WLAN drop and USB/removable-media open/automount prompts (pcmanfm + nm-applet hide + system-stored WiFi PSK).

**Prior:** [2026-07-15-c](#2026-07-15-c)

---

## 2026-07-15-c

**OHD PLEASE WAIT health overlay:** OHD mode now shows a full-screen "PLEASE WAIT" message when connectivity is lost. "No Network Connection / Attempting to connect" when WiFi is down; "Lost contact with server .xxx" (last octet of Pi IP) when the Pi is unreachable for >15 seconds. Auto-resumes normal display when data feed recovers.

**Prior:** [2026-07-15-b](#2026-07-15-b)

---

## 2026-07-15-b

**Odometer one-sided smoothing:** Deceleration is now immediate (no lingering momentum) so the odometer always reaches its target from the slowest scroll speed. Acceleration still eases in via `roll_smooth_tau` to prevent jerks when speeding up.

**Prior:** [2026-07-15-a](#2026-07-15-a)

---

## 2026-07-15-a

**Odometer smooth speed transitions:** Exponential roll-rate smoothing eliminates visible jerks when the odometer accelerates or decelerates. Roll duration now eases toward the lag-derived target via `_smooth_roll_sec(dt)` with configurable `roll_smooth_tau` (default 0.4s). Carry columns share the same smoothed value. Both `reusable_odometer/core/engine.py` and legacy `odometer_engine.py` updated.

**Prior:** [2026-07-13-a](#2026-07-13-a)

---

## 2026-07-13-a

**EtherGold HTTP external mutex fix:** When `ETHERGOLD_HTTP_EXTERNAL=1` skipped the Bottle web server, `mutex` was never created — USB SDS + `play_video` crashed with `AttributeError: 'NoneType' object has no attribute 'acquire'`. Always create the lock for display/serial sync.

**Prior:** [2026-07-10-a](#2026-07-10-a) — EtherGold HTTP external skip.

---

## 2026-07-10-a

**EtherGold HTTP external:** When `ETHERGOLD_HTTP_EXTERNAL=1` (auto-set by `runit.sh` when `ethergold-http.service` is enabled), DyzineFrame skips the Bottle web server on port 31234 so **ethergold-http.service** can serve OHD endpoints.

**Prior:** `2026-07-09-a` — Call Attendant crash fix.

---

## 2026-07-09-a

**Call Attendant crash fix:** Restored missing `play_attendant()` — attendant loop had been left as dead code after `play_fireworks()`'s `return`, so `DISPLAY_ATTENDANT` triggered `NameError` on boot and autostart appeared broken.

**Prior:** `2026-07-08-h` — local cash win vs progressive fireworks.

---

## 2026-07-08-h

**Local cash win vs progressive:** Fireworks IPC now carries `is_progressive`. Bank progressive jackpots (Phase 1) keep the full duration-based celebration with the **JACKPOT WINNER** amount overlay. Local slot cash wins (Phase 2, e.g. 970C Triple 7's over `FireworksWinThreshold`) play a **single loop with no overlay text** and no Call Attendant defer. A missing flag is treated as progressive (older bridges unaffected).

**Prior:** `2026-07-08-g` — ignore/delete stale `fireworks-request.json` on boot and after celebration.

---

## 2026-07-08-g

**Fireworks boot fix:** Ignore/delete stale `fireworks-request.json` on boot and after celebration; watcher seeds mtime/seq so old jackpot IPC is not replayed on restart.

**Prior:** `2026-07-08-f` — transparent jackpot overlay, larger amount text.

---

## 2026-07-08-f

**Jackpot overlay:** Transparent text (no black fill) with outline; amount rendered larger than **JACKPOT WINNER** title.

**Prior:** `2026-07-08-e` — fireworks IPC watcher inotify/mtime fix.

---

## 2026-07-08-e

**Fireworks IPC watcher fix:** `fireworks_watcher.py` mtime poll every 0.5s alongside inotify — bridge `os.replace()` updates were missed (no HTTP fallback on localhost), so IPC jackpots showed no fireworks.

**Prior:** `2026-07-08-d` — attendant ordering + HTTP reads fresh IPC json.

---

## 2026-07-08-d

**Jackpot fireworks fix:** Main loop plays fireworks before Call Attendant when local winner defer is active. HTTP `/flag/fireworks` falls back to fresh `fireworks-request.json` when bridge writes IPC. Requires bridge `fireworks_ipc.py` on Pi (not HTTP-only).

**Prior:** `2026-07-08-c` — jackpot text overlay (needs IPC payload).

---

## 2026-07-08-c

**Jackpot winner overlay:** Local winner fireworks render **JACKPOT WINNER** + formatted amount (`progwin_cents` from IPC) centered over the fireworks video for the full 60s celebration.

**Prior:** `2026-07-08-b` — defer error/attendant on local winner during fireworks.

---

## 2026-07-08-b

**Jackpot winner error defer:** On the local progressive winner (`is_winner` / TID match in fireworks IPC), `/flag/error` attendant is **deferred** for the winner fireworks duration (default 60s). Door open still interrupts immediately. After fireworks (or when defer expires), if error is still set, attendant runs normally.

**Prior:** `2026-07-08-a` — full fireworks duration; SDS game switch ignored during fireworks.

---

## 2026-07-08-a

**Fireworks duration fix:** IPC sets `DISPLAY_FIREWORKS_LOOP=0` + `duration_sec=30/60` — `play_fireworks` no longer exits after one MP4 loop when loop count is zero. Video repeats until the deadline.

**Sequencing:** SDS game/attract switches ignored while fireworks play. Only **door open** and **machine error** (attendant) interrupt fireworks.

**Prior:** `2026-07-07-z` — fireworks IPC (inotify + bridge JSON).

---

## 2026-07-07-z

**Fireworks IPC (Pi / EtherGold bench):** DyzineFrame watches `~/EtherGold/fireworks-request.json` via inotify (mtime poll fallback). Bridge writes atomic JSON instead of HTTP `GET /flag/fireworks/N` on localhost — avoids wedged Bottle server on port 31234. Config: `fireworks_winner_duration_sec=60`, `fireworks_bank_duration_sec=30` (fallback if JSON omits duration).

**Includes:** `fireworks_ipc.py`, `fireworks_watcher.py`; unified pull via `pull_all_from_dropbox.sh`.

**Prior:** `2026-07-07-y` — TBX progressive display fix.

---

## 2026-06-21-01 (Cursor OHD pass)

**Baseline compared to:** Brett’s drop saved as  
`DyzineFrame 2026-06-21\_extracted\` in this workspace (`CURRENT_VERSION = "2026-06-18-01"`,  
`DyzineFrame.py` + `config.ini` only).

**Modified tree (share this folder with Brett):**

```
D:\Dyzine Tek Dropbox\Chris Cummings\Dyzine Tek, LLC\Engineering\Digital Signage\Overhead Bank Signage\DyzineFrame\Cursor OHD Project\DyzineFrame\
```

Same path under Dropbox sync. Zip or copy the entire `DyzineFrame\` directory (include  
`Assets\`, `content\`, shell scripts, and `reusable_odometer\` — not just the `.py` files).

---

### Summary

This pass adds **TBX vs OHD display modes**, **OHD logo intro animation**, **implemented  
progressive modes 1–4** (Brett’s modes 2–4 were `pass` stubs), **P3 GAMESTATS floor-game  
discovery** (once per boot), and supporting CSV/activity plumbing. TBX remains the  
1920×1080 Topbox path; OHD is 3840×1080 overhead signage.

---

### Version

| Item | Brett (2026-06-21) | This release |
|------|-------------------|--------------|
| `CURRENT_VERSION` | `2026-06-18-01` | `2026-06-21-01` |

---

### New files

| File | Purpose |
|------|---------|
| `p3_gamestats.py` | Parse P3 `GAMESTATS` CSV (enabled games, progressive flags, credit balance). Used at boot and on POG-CC `/credits`. |
| `ohd_progressive_modes.py` | Schedule builders for `prog_display_mode` 1–4, floor-game list, activity tiers, interleaved highlight rotation. |

Supporting OHD stack (present in this tree; required at runtime but not in Brett’s two-file zip):

| File | Purpose |
|------|---------|
| `ethergold_reader.py` | Progressive CSV reader (local file or HTTP), rolling activity averages for modes 3/4. |
| `player_odometer.py` | Full-screen progressive slide renderer (uses `reusable_odometer/`). |
| `reusable_odometer/` | Shared odometer widget/engine. |
| `ohd_assets.py`, `Assets/` | Layout and art paths for 1920×1080 and 3840×1080. |

---

### `DyzineFrame.py` — changes from Brett

#### Display platform: TBX vs OHD

- New config key **`display_mode`**: `TBX` (1920×1080) or `OHD` (3840×1080).
- Resolution applied at `load_config()` via `_apply_display_mode_resolution()`.
- **TBX:** existing logo `play_video()` loop; single-game progressive for `curr_index` only; **`prog_display_mode` forced to 1** (multi-game modes are OHD-only).
- **OHD:** `play_ohd_logo_video()` — logo centered on 3840 canvas, ~2 s slide to x=0 + fade; progressive composited underneath during exit; then `play_ohd_progressive_modes()`.

#### Progressive modes 1–4 (OHD)

Brett’s build cycled all 16 games in mode 1 and had **empty stubs** for modes 2–4.

| Mode | Brett | This release |
|------|-------|--------------|
| 1 | Loop games 1–16, `mode_1_display_time` each | Loop **floor games only** (from GAMESTATS or config), 10–60 s clamp |
| 2 | `pass` | Highest jackpot ~75% of cycle; rotation fills remainder |
| 3 | `pass` | Most active (rolling avg CSV delta) ~75%; rotation persists index |
| 4 | `pass` | Activity tier % from config; below lowest tier → mode 1 behavior |

Implemented via `OhdProgressiveScheduler` + `play_ohd_progressive_modes()`.

#### P3 GAMESTATS integration

- **`P3_fetch_gamestats_text()`** — serial pull (same wire format as before).
- **`parse_gamestats_csv()`** — full CSV parse (CR/LF safe via `splitlines()` after LF strip).
- **Boot (splash, safe idle):** one pull caches **`progressive_floor_games`** and **`black_gold_21_enabled`** for the session. Enabled games do not change at runtime.
- **POG-CC `/credits`:** updates **credit balance only**; floor list not re-applied.
- Credit uses **`data[4][2]`** / getcredit (hundredths of a dollar, e.g. `38125` = $381.25). QA_VALUE (e.g. `25`) is credit tick size in cents, not balance.
- **`test_mode_no_serial = True`** skips all P3 I/O (Win11 dev); floor list falls back to `config.ini`.
- Rate limit unchanged: minimum **2 s** between GAMESTATS pulls (`P3_LAST_USAGE`).

**Timing (Brett’s rule):** GAMESTATS blocks the T340+ ~**600 ms** (battery-backed register reads + CSV format). Never issue during on-screen animation. Boot splash and POG-CC-scheduled `/credits` are the intended windows.

#### Other `DyzineFrame.py` notes

- Imports `static_file` from Bottle (progressive CSV HTTP serve if used).
- Progressive crossfade skipped on OHD (`DISPLAY_MODE != OHD` guard in `play_progressive_display`).
- Session globals: `P3_FLOOR_GAMES_LOADED`, `P3_SESSION_FLOOR_GAMES`, `P3_SESSION_BLACK_GOLD_21`.
- Boot log line: `P3 GAMESTATS boot cache: progressive_floor_games=[…] …`

---

### `config.ini` — new / changed keys

| Key | Notes |
|-----|--------|
| `display_mode` | `TBX` or `OHD` (first setting; drives resolution). **Office / 1920×1080 Topbox → `TBX`.** |
| `black_gold_21_enabled` | Fallback; overridden from GAMESTATS at boot when P3 present. |
| `progressive_floor_games` | Comma list 1–14,16; empty = default 1–12 (or 1–11 if BG21). **Pi: filled once from GAMESTATS at boot.** |
| `prog_display_mode` | 1–4; **ignored for rotation logic when `display_mode = TBX`** (still read; clamped to 1). |
| `mode_1_display_time` | Clamped **10–60** seconds (Brett default was 5). |
| `ohd_focus_fraction` | Default `0.75` (modes 2/3). |
| `ohd_rotation_batch` | Games per rotation batch before highlight (modes 2–4). |
| `activity_avg_min_samples` | Min CSV samples before activity ranking (modes 3/4). |
| `activity_avg_window_sec` | Rolling window for activity average (default 60 s). |
| `activity_tiers` | Mode 4, e.g. `5.00:100, 4.50:80, …` (dollars → display %). |
| `progressives_source` | Local path or HTTP URL for progressive CSV (empty = default EtherGold path). |

Brett’s mode 2–4 descriptions in `config.ini` comments were **renumbered/clarified** to match implemented OHD behavior (his original comment numbering differed from the code’s `prog_display_mode` values).

---

### `ethergold_reader.py` — additions relevant to this release

- Rolling **activity delta history** per game (`record_activity_sample`, `average_activity_delta`, floor-filtered helpers).
- Used by OHD modes 2–4 for highest-jackpot and most-active selection.
- Source resolution: env vars, then `config.ini` `progressives_source`, then default `../EtherGold/progressives.csv`.

---

### `ohd_progressive_modes.py` (new)

- `parse_progressive_floor_games()` — config fallback with BG21 cap (11 vs 12 games).
- `load_ohd_progressive_settings()` — accepts **`floor_games_override`** / **`black_gold_21_override`** from P3 boot cache.
- `OhdProgressiveScheduler.build_schedule(mode)` — modes 1–4 interleaved schedules.
- `summarize_schedule()` — debug log of cycle composition.

---

### `p3_gamestats.py` (new)

- Wire constants: `GAMESTATS_CMD`, `READ_UNTIL` (moved out of `DyzineFrame.py`).
- `normalize_gamestats_bytes()`, `parse_gamestats_csv()`, `credit_from_parse()`.
- Parses 16 game slots: enabled (` ON`/`OFF`), progressive (`Y`/`N`), names.
- Builds `progressive_floor_games` list (enabled + progressive, excluding game 15).

Bench-test copy with CLI report:  
`EtherGold/4-Board Test Fixture/fixture-timing/tools/p3_gamestats_test.py` (fixture only, not required on production Topbox).

---

### Deployment notes

**Minimum files to update on an existing Pi** (if not copying the full tree):

1. `DyzineFrame.py`
2. `p3_gamestats.py`
3. `ohd_progressive_modes.py`
4. `ethergold_reader.py`
5. `player_odometer.py`
6. Merge new keys into `config.ini` (do not blindly overwrite site `pog_asset_number`, download URL, etc.)

**TBX office / test setup:**

```ini
display_mode = TBX
test_mode_no_serial = False   # set True on Win11 without P3
```

GAMESTATS floor cache and credit still run on Pi with P3 at boot; OHD multi-game modes and 3840 logo path are inactive in TBX mode.

**Monitor:** TBX expects 1920×1080. Fixture monitors (512×256 POG scale, often max ~1080×764) are not sufficient for full Topbox UI — use a 1920×1080-capable panel for DyzineFrame testing.

---

### Not changed (Brett behavior preserved)

- USBCDC command set (`/sds`, `/credits`, `/heartbeat`, `/flag`, etc.).
- P3 serial: `/dev/ttyUSB0` @ 115200, 2 s debounce.
- Playlist, ads, fireworks, attendant/door/help overrides.
- `install_script.sh`, `runit.sh`, boot/desktop helpers (unchanged in this pass unless already edited elsewhere).

---

### Known follow-ups

- OHD 3840×1080 layout tune (`OHD_BACKLOG.md`).
- Win11 / no-P3: floor list remains manual via `progressive_floor_games` in config.
- Optional: HTTP progressive feed from Topbox for OHD bank without local CSV file.

---

### Quick diff reference (Brett → this)

```
DyzineFrame.py          Major: display_mode, OHD logo, modes 1–4, GAMESTATS session cache
config.ini              New keys (see table above); mode_1 clamp 10–60
p3_gamestats.py         NEW
ohd_progressive_modes.py NEW
ethergold_reader.py     Activity averaging + floor helpers (if not already on target Pi)
player_odometer.py      Progressive slide (required dependency)
```

For questions on GAMESTATS timing or floor-game policy, see also  
`EtherGold/4-Board Test Fixture/fixture-timing/P3_GAMESTATS_TEST.md`.
