mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
14 KiB
14 KiB
Changelog
1.9.2
⚠ Major internal hardening release (the "#146" rewrite) — large blast radius. 1.9.2 rewrites the connection / maintenance / OTA hot paths to kill an event-loop death spiral, plus adds usage-metering (billing) and web-player fixes. If you bisect a regression to the 1.9.x line, 1.9.2 is the big one. Core invariant introduced: no synchronous op may block the event loop for more than ~50ms, ever. Every new subsystem has an env kill-switch.
Fixed — maintenance / prune (the death-spiral root cause)
- Non-blocking, chunked, per-device
device_status_logprune. The old whole-tableROW_NUMBERsort froze boot for 40–48s at ~1M rows → healthcheck fail → restart loop that wiped in-memory throttle state → the spiral. Prune is now per-device, indexed, batched withsetImmediateyields (lib/chunked-prune.js), async, re-entrant, and band-gated on the interval run (the startup prune is intentionally un-gated so a bloated table self-heals on first boot without freezing it). All table-growth sweeps (status-log, play-logs, provisioning, telemetry, lag) route through the chunked helper. New indexidx_devices_provisioning. Measured worst-case event-loop gap under the storm harness: <300ms across 300k rows (was 40–48s).
Fixed — reconnect / flap
- Per-device flap-rate limiter (
lib/flap-limiter.js): a device reconnecting faster thanCONNECT_RATE_MAX(20) perCONNECT_RATE_WINDOW_MS(5min) is refused at the register gate, keyed via a SNAT-safe identity chain (device_id → fingerprint → token → one bounded global anon bucket) — never by IP (the whole fleet egresses one IP). After repeated trips a hard flapper is quarantined IN-MEMORY for 30min and auto-clears — it is NOT a durable DB block. - Operator block kill-switch:
POST /api/devices/:id/{block,unblock}+ a dashboard button; the block check resolves the effective device_id via the identity chain so a device_id-less reconnect of a blocked device is still caught. Takes effect on next register, no restart. - Also folded in: false-offline fixes (live-socket liveness beats a lagged heartbeat clock; evicted-socket re-arm race) and per-connection fail-fast so one device's handler throw can never exit the process.
Fixed — OTA (SNAT-safe)
/api/update/checkearly-returns before any filesystem call when there's no offer; APK metadata is cached./download/apkgains a band-aware global concurrency + rate guard that sheds with 503 Retry-After only under elevated/critical loop-lag — under normal band, downloads serve freely (a coordinated fleet rollout is never staggered when healthy). All limiting is global/aggregate — no per-IP limiting (SNAT).
Added — telemetry / logging / observability
- Batched
event_loop_laginserts (buffered, flushed every 10s) and coalesced high-frequency logging (one summarized line per key per 30s; band changes stay immediate). - Throughput counters (running total + last-completed-window) in the
/api/statusdebug block so a flapper/flood shows on the server itself (flap.refusedLastWindowclimbing whileband=normal= the limiter absorbing it cheaply). The debug block is now admin-toggleable (Admin tab, persisted, no restart; default followsSTATUS_DEBUG_ENABLED). devices_connectedon/api/status(always-on): the live WS-socket count from the heartbeat connection map (NOT the laggingdevices.status='online'column).
Added — billing (usage metering)
- Billable Screens metering per the ByteTinker–Bold agreement — the contractual
system-of-record. A durable daily rollup (
device_usage_daily) is accumulated incrementally off the heartbeat tick from live presence (retention-independent), pruned chunked. Exposed on a dedicated, admin-gatedGET /api/billing/usageroute (NOT on/api/status; billing is revenue data). Readable via an owner-minted, revocablebilling:readscoped token (scripts/mint-billing-token.js) that authorizes billing-read and nothing else, OR a platform-admin session. Seedocs/billing.md.
Fixed — web player
- "Unchanged" refresh no longer drops the video. On a reconnect the server re-emits
device:pairedwhile content is already playing; the player showed the idle "Waiting for content…" overlay unconditionally (covering live video; audio kept playing underneath) and the following "Playlist unchanged" left it up. Idle now shows only when genuinely idle, and an unchanged refresh is a strict no-op that leaves playback exactly as-is. - Hardened
PlayerMediaHealthcall sites to guard by method (not object) so a stale-cached player module can't throwshouldShowIdle is not a functionand abort a socket handler.
Added — translations
- Italian (
it) locale updated (#145).
1.9.2-beta1 — unreleased
Fixed — server resilience (#142)
- A single flapping device can no longer saturate the event loop. A new
load-aware, per-device reconnect throttle (
lib/reconnect-throttle.js) gates genuine reconnects before the heavy register work (DB writes + playlist build). The verdict is per-device; global event-loop lag only multiplies an already-flagged device's backoff and never throttles a healthy one. Hard ceiling- cold-start warm-up so a full-fleet reconnect after a deploy is never throttled.
device_status_loggrowth is bounded. Addedidx_device_status_log_device_ts, a global retention sweep (pruneStatusLog,STATUS_LOG_RETENTION_DAYSdefault 3) covering removed/idle devices and theoffline_timeoutpath, and de-duplicated the table'sCREATE TABLE.content-ackspam de-duplicated. Repeated identical(device_id, content_id, status)reports are suppressed withinCONTENT_ACK_DEDUP_MS(default 10s).- Provisioning cleanup window corrected. Unclaimed provisioning devices are now
swept after 24h (the code used
365 * 86400— a year — contradicting its own comment).
Added — observability (#142)
- Event-loop lag telemetry via
perf_hooks.monitorEventLoopDelay(). Sampled to a boundedevent_loop_lagtable (indexed + pruned,LAG_TELEMETRY_RETENTION_DAYS) and surfaced on/api/statusasloop_lag(mean/p50/p99/max + band).
Maintenance
- Operators whose
device_status_logis already bloated from a pre-1.9.2 deployment should reclaim disk with a one-time manualVACUUMin a maintenance window; retention now bounds further growth. Auto-VACUUM is intentionally not enabled. Seedocs/maintenance-device-status-log.md.
1.9.1-beta3 — unreleased
Fixed — Tizen player
- #118 Sticky "Not authenticated" banner. On TV sleep/wake the socket reconnects and
a heartbeat could fire on the fresh, not-yet-registered socket; the server rejected it
with
device:auth-error, which the player showed as a sticky toast over still-playing content (and, worse, dropped its saved credentials and re-paired). Heartbeats are now gated on a per-connectionauthenticatedflag (set only betweendevice:registeredanddisconnect/auth-error), the heartbeat timer is stopped onconnect/disconnect/auth-error, the stale banner is cleared ondevice:registered, and theauth-errortoast is non-sticky so any transient case self-clears. - #119
app_versionstuck at1.0.0. The hardcoded constant made every Tizen device report1.0.0regardless of the installed.wgt. The version now resolves at runtime fromconfig.xmlvia the Tizen application API, with a fallback constant thatbuild-wgt.shstamps fromconfig.xml'sversion="".
Added — Tizen player
- Video walls (
wall:sync). The Tizen player now supports wall membership: when the payload carrieswall_config, a newWallControllerpositions the stage (vw/vh) as this screen's slice of the wall and drives the single-zone player as leader or follower. The leader broadcastswall:syncat 4Hz; followers align their index and keep their video locked to the leader's clock with a latency-compensated drift controller (hard-seek past 0.3s, gentle ±3% playbackRate nudge past 0.05s), and request an immediate position on (re)connect viawall:sync-request. Mirrors the web player (the Android player has no wall support). Per-tilerotationis not applied yet (web-player parity). Wall emits are gated on auth + connection so a pre-register tick can't tripdevice:auth-error. - Multi-zone layouts (Android parity). The Tizen player now renders assigned layouts,
not just fullscreen single-zone. A new
ZoneRenderer(ports the AndroidZoneManager) positions zones by percent geometry withz_index/fit_mode/background, groups assignments byzone_id(unassigned content goes to the first zone), and rotates each zone independently with the same per-item schedule gating (#74/#75).app.jsselects the renderer frompayload.layout; single-zone playback is unchanged. (Video wallswall:syncare still Android-only.) - #121 Remote commands. Added a
device:commandhandler (refresh,launch,screen_on,screen_off, plus honest no-op toasts forupdate/reboot/shutdown, which need B2B/MDM privileges a sideloaded app lacks). Removed the deaddevice:reloadlistener (the server never emitted it) in favour ofdevice:commandrefresh. - #120 Dashboard preview. Added
device:screenshot-request/device:remote-start/device:remote-stop. Images capture for real;<video>/YouTube fall back to a status card because the TV's hardware video plane and cross-origin iframes can't be read into a<canvas>. Seetizen/README.mdfor the support matrix. - #122 Updates / boot. Documented the supported paths —
.wgtre-sideload or URL Launcher/MDM refresh for updates, and display-level kiosk/URL-Launcher settings for auto-launch on boot (there is no in-app OTA orconfig.xmlautostart for a sideloaded consumer TV web app).
1.9.0 — 2026-06-11
Added
- Per-playlist-item schedules. Each playlist item can carry one or more schedule
blocks — active days, a start/end time-of-day, and optional start/end dates. An item
plays when the screen's local "now" matches at least one block; an item with no
blocks always plays. Edit per item via the clock icon in the playlist editor (a badge
summarises the schedule on each row).
- #74 dayparting: time-of-day + day-of-week windows, including overnight windows that cross midnight (a Fri 22:00–02:00 block is active Sat 01:00).
- #75 auto-expire: inclusive start/end dates; an item past its end date stops showing automatically — even on offline screens, because evaluation is on-device.
- All three players (web, Android, Tizen) evaluate schedules client-side against their
own clock, so dayparting and expiry work offline. They share one evaluator contract,
shared/schedule-vectors.json— 39 conformance vectors covering DST (US + AU), overnight-wrap day anchoring, timezone correctness, and date boundaries. CI runs the vectors against the JS evaluator (node) and the Kotlin port (Gradle/JUnit); the Tizen copy is byte-identical to the JS source and checked under node. - Device detail now shows the screen's reported timezone and clock, with a clock-skew warning when the device clock differs from the server by more than 2 minutes (a bad device clock makes schedules fire at the wrong local time).
Changed — device-level schedule timezone (behaviour change)
- Device/group schedule overrides (the existing calendar feature) are now evaluated
in each device's effective timezone instead of the server's local time. Previously the
schedules.timezonefield was never applied and "07:00" meant the server's 07:00. Now "07:00" means the screen's 07:00 — which is what was intended.- Who is affected: self-hosters whose server timezone differs from their screens' timezone — their existing device schedules will shift to fire at the screens' local time. Single-timezone deployments (server and screens in the same zone) are unaffected. A device with no timezone set and not reporting one falls back to the server clock (unchanged from before).
Fixed
- #81 — release APK is now v1 + v2 + v3 signed. With
minSdk 26, the Android Gradle Plugin defaulted the v1 (JAR) signature off, producing a v2-only APK that some MDM-managed commercial signage (e.g. MAXHUB via the Pivot MDM) silently removes on the next reboot — so screens that power-cycle nightly lost the app and fell back to the setup screen. SettingenableV1Signing = truehad no effect at minSdk ≥ 24; the release build now re-signs withapksignerand a low--min-sdk-versionto emit the JAR signature alongside v2/v3. Verified to install and run on Android 14+/API 36 as well.
Notes
- Scheduling fails open. If the on-device evaluator ever errors (bad timezone id, malformed block), the item plays rather than being hidden. A blank screen is worse than an over-running promo — this is a guarantee, enforced in all three players.
- Windows are enforced at item boundaries: a long item finishes before the schedule is re-checked, so it can overshoot its window by up to its own duration.
- A single video with a schedule now re-renders at each loop boundary so its window can be re-evaluated; seamless native looping still applies to unscheduled single videos. Deliberate tradeoff — a brief seam each loop for a scheduled lone video, in exchange for its daypart/expiry actually being honoured.
- Re-publish required: editing a schedule puts the playlist into draft; publish to push schedules to devices. Existing published playlists keep playing unchanged until re-published.
- Players that predate this release ignore the new fields and keep playing everything (graceful degradation) — update players to honour schedules.