The Timers module becomes the one place a production tech looks for every
timer and clock: ProPresenter's timers as before, the LTC timecode, a
time-of-day clock (the Clock module's format / seconds / date, folded in as
a sub-part — modules/clock itself is untouched), and timers other modules
offer over the module guide's provider contract: PCO Plan's service /
current item / countdown, or anything else declaring provides: ["timers"].
Server: a small `sources` abstraction. Each source turns its own world into
the guide's standard timer objects and reports health — ProPresenter (the
existing poller, wrapped, behind ppEnabled), LTC (the existing listener,
untouched, as a kind:"clock" timer whose status.text is the timecode, with
fps/df/ageMs in an `ltc` extension), Clock, and one module source per
provider found in the shell's /api/modules over loopback (shell.port):
/timers/stream when it answers as SSE, else /timers polled every 2 s;
re-discovered every 30 s; backoff 3 s → 15 s; a provider's timers survive a
10 s grace and then go. PCO Plan sits behind pcoEnabled, the rest behind
moduleTimersEnabled. The module is itself a provider: GET /timers and
GET /timers/stream serve the merged set. The header comment says how to
add a platform in a dozen lines.
Client: one tile, every timer, grouped under source headings — label, the
big time derived locally from the snapshot and a server-offset-corrected
clock (so digits tick smoothly between snapshots), a status line, a detail
line. Countdowns count down, elapsed count up, clocks show the time; the
LTC card still counts frames. Feedback colours (running / warning near the
end / overrun / idle) are per-tile colour settings applied as custom
properties on the tile root, so they win over dashboard themes; left at
their defaults they follow the theme. Gear settings: Clock, ProPresenter
timers, LTC timecode, Module timers, Source headings, Status lines, the
clock's format / seconds / date; a "Timers ▾" header menu hides any single
timer (source:id, saved with the layout; the old hiddenItems and
timer:<uuid> / ltc picker variants keep working).
Manifest: 1.3.0, proddash >=1.4.0, provides timers, configGroups with a
toggle switch per integration (ProPresenter / LTC Timecode / PCO Plan /
Other modules) and group help; switch labels are nouns.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A new Display-group select (Default / Monospace) sets the font for the
timer and LTC digits. Monospace keeps the numbers from shifting sideways
as they change — easier to read on a fast-moving countdown or timecode.
Flows through server state → client applies a `tm-mono` class and re-fits
the digit sizing; both timer and LTC values (.tm-time) honor it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Simplify LTC to its one working source — the built-in audio listener — and
make the admin UI reflect it.
Module:
- Remove the stage-display websocket source (stage-ws.js, stagePassword)
and the /v1/timecode/status API probe — both dead on ProPresenter 21.4.
- Remove the remote-reader ingest (POST /ltc, ingestToken, tools/
ltc-reader.js): the module owns LTC end to end now. The decoder selftest
moves to modules/propresenter-timers/ltc.selftest.js so the engine stays
covered.
- composeLtc collapses to the listener: running/stopped/nosignal, or a
clear note when disabled / no device / capture failed / starting.
- Manifest: ltcEnabled is now a `switch`; ltcDevice and ltcChannel carry
showWhen:"ltcEnabled"; ProPresenter and LTC fields carry `group` labels.
Shell (admin) — backward-compatible additions, coordinate with the shell lane:
- New `switch` field type (boolean shown as the enable-toggle control).
- `group`: consecutive fields render under a labeled subsection.
- `showWhen: "<key>"`: a field is shown only while that boolean/switch is
on (and actually collapses — .field[hidden] override, since .field's
display:flex outranks the [hidden] UA default).
- applyConfigPatch coerces `switch` as boolean. Documented in MODULE-GUIDE.
Verified: engine selftest (29 checks); listener decodes BlackHole loopback
(running 30fps, ageMs); device picker lists inputs; POST /ltc now 404s;
admin shows two labeled groups, the switch, and device/channel appearing
only when the switch is on (computed display none↔flex confirmed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The LTC card repainted only on server anchors (~4x/s), so the frame
counter jumped in ~7-frame steps. Now the client counts every frame
locally between anchors and re-syncs on each one — frame-accurate and
smooth, with no extra network traffic.
- Server anchors carry ageMs (how old the decoded timecode is) so clients
can pin the anchor to their own clock. ltc-listener reports it while
running; composeLtc passes it through (listener) or derives it (reader).
- Client does exact SMPTE frame<->timecode math (parseTc/tcToFrames/
framesToTc, incl. 29.97 drop-frame minute counting) and advances the
display in a requestAnimationFrame loop at the LTC's own rate.
- Robust when rAF is throttled (backgrounded tab): render() paints the
extrapolated frame at the 4x/s anchor rate too, so the display never
freezes — the rAF loop only refines it to every frame when visible.
- Anchor jitter can't tick the counter backwards (small regressions
clamped; genuine seeks/re-strikes pass); freshness cap stops freewheel
into phantom frames when anchors stop arriving.
Verified: SMPTE math against the encoder over 200k drop-frame frames plus
round-trips; the extrapolation pipeline simulated at 30/25/29.97DF showing
strict +1-per-frame advance and correct DF boundary crossing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The LTC listener now lives inside the module: it spawns the native
ltc-capture helper (moved into the module folder, compiled on first use,
exact-name device matching) for the admin-selected input device, extracts
the configured channel, and decodes SMPTE LTC in-process (ltc.js — the
engine, shared with tools/ltc-reader.js, which remains the remote-machine
option posting to /ltc).
Admin config: an "LTC listener" switch, an audio-device select populated
live from the new GET /ltc/devices route, and a 1-based channel number.
The switch off means LTC is gone entirely: no LTC entry in the Add-tile
picker, no LTC row in the Timers menu or All-timers grid, no stage/API
probes, and POST /ltc answers 409. Source precedence with it on:
listener-with-data, fresh remote reader, listener's own no-signal verdict,
then the legacy API/stage chain.
Shell (admin page): `select` fields learn `optionsRoute` — options fetched
from a module route at render, saved value kept selectable when the route
or device is gone. Documented in MODULE-GUIDE; backward compatible.
Coordinate with the shell lane before its next admin.js change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ProPresenter 21.4 exposes no timecode through any API, so read the LTC
where it actually lives — the audio feed:
- tools/ltc-reader.js: zero-dependency SMPTE LTC decoder (biphase-mark,
adaptive bit clock, envelope-scaled comparator) fed s16le PCM on stdin
from ffmpeg/sox; POSTs running/stopped/nosignal + timecode + frame rate
(incl. 29.97 drop-frame) to the module. --selftest runs an encoder→
decoder suite (24/25/30 fps, DF minute counting, low level + noise + DC,
dropout recovery); --demo synthesizes live LTC through the real decoder
and ingest path with no audio hardware.
- Module: POST /ltc ingest route (optional bearer token, validated body,
6 s staleness with automatic fallback to the API probe / stage source),
richer LTC state (status, fps, df, reader source), health messages.
- Client: LTC card shows Running · <rate> / Stopped (held frame) /
No signal; works LTC-only with no ProPresenter host configured; the
reader-fed card no longer grays out when only ProPresenter is down.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ProPresenter's HTTP API has no timecode route (verified: 21.4 returns
404), but its classic stage-display websocket still answers — so LTC now
comes from a stage layout field labeled "LTC": a zero-dependency RFC6455
client (stage-ws.js) authenticates (new stagePassword admin config),
resolves the LTC-labeled frame's uid from the layout list, and treats any
message carrying that uid as the timecode text. "Receiving" is a 2.5s
staleness heuristic; frozen or placeholder text reads as no-signal. The
HTTP probe stays authoritative when it ever answers (a 404 verdict is
retried once a minute), and the LTC card surfaces the reason when
timecode is unavailable (bad stage password, no LTC field). pp-timers-mock
grows a /stagedisplay websocket with --stage-pwd and --no-ltc-field to
exercise every path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adopt main's new multi-tile contract: the module's server exports
tiles() (answered from the poller's held state, never a fresh upstream
request) listing All timers, one entry per discovered timer, and LTC
timecode. The client branches on moduleApi.variant — solo tiles render
exactly one card with no selection menu, and a solo timer that vanishes
from ProPresenter shows a labeled "Not in ProPresenter" card instead of
erroring (visible even in the compact layout). pp-timers-mock gains
--drop=<uuid,...> to simulate a timer deleted in ProPresenter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New propresenter-timers module: polls /v1/timers + /v1/timers/current
(shapes verified against openapi.propresenter.com), probes the
undocumented /v1/timecode/status for LTC and honestly reports
"not available on this ProPresenter" when absent. Per-tile timer/LTC
selection via a Timers ▾ header menu (never-seen timers default on),
responsive auto-sized cards with state-derived colors (the API exposes
no per-timer color), unmistakable overrun. Includes tools/pp-timers-mock.js
for developing without a real ProPresenter.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>