The main loop recalculated the runtime at 32ms unconditionally. With no
websocket client connected there is nothing observing the sub-second values,
so we fall back to a 1s rate and raise it again as soon as a client connects.
Coordination is done through a new leaf store, `connectedClients`: the socket
adapter publishes the count and the runtime service subscribes to it. Keeping
the module free of dependencies lets both sides use it without a cycle, and
keeps the transport layer unaware of the services reacting to it.
The rate policy lives in the config alongside the values it is coupled to.
`getSkipLimit` is now derived from the rate in use, since the skip threshold
has to stay above an ordinary refresh cycle: at the idle rate the previous
fixed 1s threshold would have read every tick as a time skip and reloaded roll.
Accuracy of the event boundaries is no longer tied to the rate. The ad-hoc end
callbacks are replaced by `scheduleNextBoundary`, which derives the next
boundary from the state after every update and anticipates it when it falls
inside the current cycle. This also covers the paths that previously had no
predictive end at all (roll, roll pre-roll, restored playback) and fixes the
renewal in `addTime`, which scheduled with a time of day where a duration was
expected and so never fired.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhEkCTUCQbPVYABYpf4zh2
Search every text field with optional filters, keep result selection stable, and expose clearer match context and counts. Cover query parsing, matching, mixed-entry indexing, and the reveal flow.
* fix: issue where a count-to-end would lead to incorrect expected times
* fix: include add time in overtime when countToEnd
* fix: ui and server use same calculation for expected end