Skip to content

feat(vegas): let live content keep its place in the ticker - #457

Merged
ChuckBuilds merged 4 commits into
mainfrom
feat/vegas-live-weighting
Aug 13, 2026
Merged

feat(vegas): let live content keep its place in the ticker#457
ChuckBuilds merged 4 commits into
mainfrom
feat/vegas-live-weighting

Conversation

@ChuckBuilds

@ChuckBuilds ChuckBuilds commented Aug 12, 2026

Copy link
Copy Markdown
Owner

The problem

Live content preempts Vegas outright. While any plugin reports live priority, display_controller.py refuses to run the ticker and shows a full-screen scoreboard instead. So keeping the marquee meant not seeing live scores, and seeing live scores meant losing the marquee.

And even if the ticker did run, it wouldn't help much: the rotation is a strict round robin, one slot per plugin per cycle. With a dozen plugins enabled a live score comes round once a lap and can be minutes stale on screen.

Two changes, both off by default

vegas_scroll.live_in_ticker keeps the ticker running through a live game. Three places assumed the takeover and all three now honour it — the controller's gate, the coordinator's per-frame pause(), and the rotation switch that would otherwise move current_mode_index and stash a resume point underneath a ticker that never yields.

Weighted rotation. A plugin can hold several slots per cycle, placed by Smooth Weighted Round-Robin — the same scheduler football-scoreboard/sports.py:_build_weighted_schedule already uses to rotate its own games. The property that matters is that repeats are spread through the cycle, not clumped; three appearances in a row followed by a long silence would be worse than no boost at all.

12 plugins, a favourite's baseball game and an ordinary live hockey game:

baseball > hockey > weather  > clock    > baseball
stocks   > news   > flights  > baseball > hockey
calendar > f1     > music    > baseball > tides
birds    > hockey > baseball

18 slots for 12 plugins. Baseball 5×, hockey 3×, everything else once, nothing twice in a row.

Where weight comes from

  1. The plugin, via a new optional get_vegas_priority_weight().
  2. The core, when the plugin returns None: live_weight if has_live_priority() and has_live_content() are both true.
  3. 1 otherwise.

Because of step 2, existing plugins need no changes — any scoreboard with live_priority already gets extra turns. Step 1 exists for the one thing the core cannot work out: it can see that a game is live, not whose, so only the plugin can report a favourite. That's ledmatrix-plugins#273.

Weights clamp to 1–10. A raising hook is caught and logged, and the core then falls back to its own live-content check — so a plugin whose weight calculation is broken keeps the live boost and loses only the favourite distinction. (An earlier draft of this description said such a hook is treated as weight 1; the code and docs deliberately do the more useful thing, and has_live_priority/has_live_content are separate methods guarded separately.)

Documented

  • ADVANCED_FEATURES.md — new "Live content in the ticker" section: worked example, why weights are per plugin not per game, that the ticker is zero-sum (more slots lengthen the cycle rather than speeding it), and that frequency is not freshness.
  • CONFIG_REFERENCE.md — the three keys.
  • PLUGIN_API_REFERENCE.md — the hook, with an example and a note that most plugins don't need it.
  • config.template.json — the keys at their defaults.
  • Plus the reasoning inline at each decision point in the code.

Tests

19 in test/test_vegas_live_weighting.py: where weight comes from and its precedence, clamping, a raising plugin, an unknown plugin; and for the schedule — untouched when nothing is boosted, untouched when the feature is off, correct slot counts, every plugin still appears (a boost must not starve anything out), repeats spread not clumped (max-gap and no-two-in-a-row), and a favourite outranking another live game.

2765 passed on the full suite. The one failure, test_install_lowmem.py::TestDiskBackedTmpdir, is pre-existing on main and environment-dependent.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5

Summary by CodeRabbit

  • New Features

    • Added optional live-content display within the Vegas ticker.
    • Added configurable weighting for live content and favorite-team live content.
    • Improved ticker rotation to balance content while prioritizing higher-weight items.
    • Added support for customized content weighting through plugins.
  • Bug Fixes

    • Prevented live-priority interruptions when live content is configured to remain in the ticker.
  • Documentation

    • Expanded configuration and plugin documentation with live ticker settings, weighting behavior, defaults, and examples.

Live content used to preempt Vegas outright: while any plugin reported
live priority the display controller refused to run the ticker at all and
showed a full-screen scoreboard instead. Keeping the marquee meant not
seeing live scores; seeing live scores meant losing the marquee.

Two changes, both off by default.

vegas_scroll.live_in_ticker keeps the ticker running through a live game.
Three places assumed the takeover and all three now honour it: the
controller's gate, the coordinator's per-frame pause, and the rotation
switch that would otherwise move current_mode_index underneath a ticker
that never yields.

And the rotation is no longer a strict round robin. It was one slot per
plugin per cycle, so with a dozen plugins enabled a live score came round
once a lap and could be minutes old on screen. A plugin can now hold
several slots, placed by Smooth Weighted Round-Robin -- the same
scheduler the sports plugins already use to rotate their own games. The
property that matters is that repeats are spread through the cycle
rather than clumped: three in a row and then silence would be worse than
no boost at all.

Weight comes from the plugin first, via a new optional
get_vegas_priority_weight(), then from the core: live content earns
live_weight, everything else 1. So existing plugins gain the behaviour
without changes, and the hook exists for the one thing the core cannot
work out -- the core can see that a game is live but not whose, so only
the plugin can say a favorite is playing.

Documented in ADVANCED_FEATURES (worked example, why weights are per
plugin not per game, and that frequency is not freshness),
CONFIG_REFERENCE, PLUGIN_API_REFERENCE, and the config template.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
@codacy-production

codacy-production Bot commented Aug 12, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 38 complexity · 0 duplication

Metric Results
Complexity 38
Duplication 0

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Vegas mode adds live ticker configuration, plugin-defined priority weights, and smooth weighted round-robin scheduling. Live content can remain in the ticker instead of preempting Vegas mode. Documentation and tests cover configuration, weighting, fallback, distribution, and parsing.

Changes

Vegas live ticker

Layer / File(s) Summary
Ticker configuration and plugin weighting contracts
config/config.template.json, src/vegas_mode/config.py, src/plugin_system/base_plugin.py, docs/CONFIG_REFERENCE.md, docs/PLUGIN_API_REFERENCE.md
Vegas configuration adds live_in_ticker, live_weight, and favorite_live_weight. BasePlugin adds an optional get_vegas_priority_weight() hook. Documentation describes defaults, clamping, precedence, and scheduling behavior.
Live ticker control flow
src/display_controller.py, src/vegas_mode/coordinator.py
Live-priority interruption and Vegas live-mode takeover are skipped when live_in_ticker is enabled.
Weighted Vegas rotation and validation
src/vegas_mode/stream_manager.py, test/test_vegas_live_weighting.py, docs/ADVANCED_FEATURES.md
StreamManager resolves weights, applies smooth weighted round-robin scheduling, and repairs cycle seams when possible. Tests cover weight resolution, spacing, prioritization, empty rotations, defaults, and clamping. Advanced documentation describes the resulting ticker behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Mergeability Score: 🔵 Low · up to 5538a

The change is mergeable with owner follow-up: a schedule-repair edge case can place the same item in adjacent ticker slots, reducing the intended spread of boosted content. Excluding that candidate and adding a regression test would close the bounded display-quality risk.

Sequence Diagram(s)

sequenceDiagram
  participant VegasModeConfig
  participant BasePlugin
  participant StreamManager
  participant VegasModeCoordinator
  participant DisplayController
  VegasModeConfig->>StreamManager: provide live ticker settings
  BasePlugin->>StreamManager: provide optional plugin weight
  StreamManager->>StreamManager: resolve weights and build rotation
  DisplayController->>VegasModeCoordinator: check live priority
  VegasModeCoordinator->>VegasModeConfig: read live_in_ticker
  VegasModeConfig-->>VegasModeCoordinator: return ticker setting
  VegasModeCoordinator-->>DisplayController: continue ticker when enabled
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: keeping live content in the Vegas ticker.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/vegas-live-weighting

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/ADVANCED_FEATURES.md`:
- Around line 67-69: Update the `display.vegas_scroll` key count in the
referenced documentation text from 29 to 30, leaving the surrounding explanation
and `CONFIG_REFERENCE.md` link unchanged.

In `@src/vegas_mode/config.py`:
- Around line 143-152: Update the configuration class’s to_dict() and update()
methods to include live_in_ticker, live_weight, and favorite_live_weight. Ensure
get_status() receives all three serialized values, allow partial updates to
modify them, and apply the same weight clamping used by from_config() when
updating live_weight and favorite_live_weight.

In `@src/vegas_mode/stream_manager.py`:
- Around line 443-445: Update the get_vegas_priority_weight() exception handler
in src/vegas_mode/stream_manager.py (lines 443-445) to return weight 1 after
logging the hook failure. In test/test_vegas_live_weighting.py (lines 25-43),
separate hook exceptions from live-content exceptions; add coverage at lines
94-96 for a live plugin whose hook alone raises and assert that its weight is 1.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2e8044f4-fb1d-4dd1-a6ee-27a1ca985ddd

📥 Commits

Reviewing files that changed from the base of the PR and between 9cf30bb and 8c00df2.

📒 Files selected for processing (10)
  • config/config.template.json
  • docs/ADVANCED_FEATURES.md
  • docs/CONFIG_REFERENCE.md
  • docs/PLUGIN_API_REFERENCE.md
  • src/display_controller.py
  • src/plugin_system/base_plugin.py
  • src/vegas_mode/config.py
  • src/vegas_mode/coordinator.py
  • src/vegas_mode/stream_manager.py
  • test/test_vegas_live_weighting.py

Comment thread docs/ADVANCED_FEATURES.md Outdated
Comment thread src/vegas_mode/config.py
Comment thread src/vegas_mode/stream_manager.py
ChuckBuilds and others added 2 commits August 12, 2026 16:59
Three findings from CodeRabbit, all valid.

to_dict() and update() enumerate keys explicitly and had not learned the
three new ones, so get_status() never reported them and a live config
change never applied -- turning live_in_ticker on in the web UI would
have done nothing until a restart. update() clamps the weights exactly
as from_config does.

The vegas_scroll key count in ADVANCED_FEATURES said 29; the template
has 30. My arithmetic, not the reviewer's.

The third was a documentation error rather than a code one, and I have
fixed it the other way round. The docs claimed a raising
get_vegas_priority_weight() is treated as weight 1. The code instead
falls through to the core's own live-content check, and that is the
better behaviour: the hook is only how a plugin asks for *more* than
live_weight, and has_live_priority/has_live_content are separate methods
guarded separately, so a plugin with a broken weight calculation should
lose the favorite distinction and keep the live boost. Said so in the
code, the base-plugin docstring and the API reference.

The test fake now fails in each place independently, because the two
failures mean different things: a broken hook still earns live_weight, a
plugin that cannot say whether it is live has nothing to fall back on
and weighs 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ui/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
Smooth Weighted Round-Robin spaces repeats well within a pass, but it
schedules the heaviest item first and usually last as well. The strip
loops, so those two are neighbours: the marquee showed the same plugin
twice running at exactly the one join a within-cycle check cannot see.
Observed on a live rig at 28 slots -- gaps of 6, 7, 7, 7 and then 1.

Rotating the list does not fix it. Rotation preserves the cyclic order
exactly, so it moves where the seam is drawn rather than the adjacency
itself; the trailing entry has to be swapped with one from the middle.

The first version swapped with the first slot that merely fitted, which
undid the spacing this exists to protect -- it moved a repeat from a gap
of 7 into a gap of 2, more clumped than the seam had ever been. It now
picks the candidate furthest from any other appearance, so the repeat
lands in the widest gap.

Left alone when no candidate exists. A plugin holding most of the slots
has to neighbour itself, and scheduling it is better than refusing to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/vegas_mode/stream_manager.py`:
- Around line 528-543: Update the candidate selection in the seam-repair logic
to exclude entries whose value matches schedule[-2], preventing the swap from
creating an adjacent duplicate at the end. Add a direct regression test covering
the provided schedule and verify the repaired schedule does not end with
identical adjacent values.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e0b321c6-73f1-46fe-843c-f91a30e8ccb4

📥 Commits

Reviewing files that changed from the base of the PR and between 8c00df2 and 5538af9.

📒 Files selected for processing (6)
  • docs/ADVANCED_FEATURES.md
  • docs/PLUGIN_API_REFERENCE.md
  • src/plugin_system/base_plugin.py
  • src/vegas_mode/config.py
  • src/vegas_mode/stream_manager.py
  • test/test_vegas_live_weighting.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/plugin_system/base_plugin.py
  • docs/PLUGIN_API_REFERENCE.md
  • docs/ADVANCED_FEATURES.md

Comment thread src/vegas_mode/stream_manager.py Outdated
Swapping the trailing repeat with a middle slot moves two elements, and
the candidate filter only guarded one of them. It checked the neighbours
`repeated` would acquire at j, but not what the displaced element would
sit beside at the end -- so ['a','b','c','d','x','y','x','a'] came back
as [...,'x','x'], the seam duplicate traded for a fresh one. Reported by
CodeRabbit with that exact case.

Adding the missing condition fixed it and immediately broke something
else: schedule[j] is schedule[-2] when j is the second-to-last slot, so
that candidate was always excluded, and ['a','b','c','a'] lost the only
repair it has. The same class of mistake twice, from reasoning about
which neighbours two moved elements end up with.

So it no longer reasons. It performs each candidate swap, counts the
cyclic duplicates in the result, and keeps the best one that has none --
preferring whichever leaves the boosted plugin most evenly spread. When
no such swap exists the schedule is returned untouched, which is the
unavoidable case: a plugin holding most of the slots has to neighbour
itself.

Fuzzed across 6,956 seam schedules: none made worse, none lost an entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Udr6MfaFLUPhX5Fgo67Jf5
@ChuckBuilds

Copy link
Copy Markdown
Owner Author

Both threads addressed.

Seam repair — real bug, exactly as described. ['a','b','c','d','x','y','x','a'] came back ending ['x','x']: the candidate filter guarded the neighbours repeated would acquire at j, but not what the displaced element would sit beside at the end.

Adding the suggested schedule[j] != schedule[-2] fixed that case and immediately broke another — schedule[j] is schedule[-2] when j is the second-to-last slot, so that candidate became permanently excluded and ['a','b','c','a'] lost the only repair it has. Same class of mistake twice, from reasoning about where two moved elements land.

So it no longer reasons: it performs each candidate swap, counts cyclic duplicates in the result, and keeps the best swap that has none. Fuzzed over 6,956 seam schedules — none made worse, none lost an entry. Three regression tests, two of which fail against the previous repair.

Hook-exception contract — the contradiction was real, but it was in the PR description, not the code. That line still read "a raising hook is caught and treated as 1"; the code and docs deliberately do something else, and I had not updated the description when I changed it. Corrected there.

The behaviour is intentional: the hook is only how a plugin asks for more than live_weight, and has_live_priority()/has_live_content() are separate methods guarded separately. A plugin whose weight calculation is broken should lose the favourite distinction and keep the live boost — demoting a genuinely live game to 1 because a different method raised is worse.

That is already pinned by three tests: test_a_broken_hook_still_earns_the_live_boost (weight 4), test_a_broken_hook_on_a_quiet_plugin_weighs_one, and test_a_plugin_that_cannot_say_whether_it_is_live_weighs_one — the last being the case where nothing is left to fall back on.

@ChuckBuilds
ChuckBuilds merged commit 08265c1 into main Aug 13, 2026
9 checks passed
@ChuckBuilds
ChuckBuilds deleted the feat/vegas-live-weighting branch August 13, 2026 21:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant