Skip to content

fix: looping behavior and timing calculations in QuickLoopService for looping Rundown Playlists#1704

Merged
jstarpl merged 4 commits intoSofie-Automation:mainfrom
nrkno:contrib/fix/looping-rundown
Apr 10, 2026
Merged

fix: looping behavior and timing calculations in QuickLoopService for looping Rundown Playlists#1704
jstarpl merged 4 commits intoSofie-Automation:mainfrom
nrkno:contrib/fix/looping-rundown

Conversation

@jstarpl
Copy link
Copy Markdown
Contributor

@jstarpl jstarpl commented Apr 1, 2026

About the Contributor

This pull request is posted on behalf of the NRK.

Type of Contribution

This is a:

Bug fix

Current Behavior

Countdowns for Parts before the current playing part don't show up if the entire playlist is looping. Looping playlist state is cleared when a Playlist is reset or deactivated.

New Behavior

Countdowns for Parts before the current playing Part are correctly shown and calculated for entire looping playlists. Looping playlist state is kept across resets and deactivations.

Testing

  • I have added one or more unit tests for this PR
  • I have updated the relevant unit tests
  • No unit test changes are needed for this PR

Affected areas

  • This PR affects the reset & deactivation logic for QuickLoop
  • This PR affects the Rundown timing calculations for QuickLoop/looping playlists.

Time Frame

This Bug Fix is critical for us, please review and merge it as soon as possible.

Other Information

Status

  • PR is ready to be reviewed.
  • The functionality has been tested by the author.
  • Relevant unit tests has been added / updated.
  • Relevant documentation (code comments, system documentation) has been added / updated.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Propagates an "entire playlist looping" flag through timing and multiple UI components and updates QuickLoopService to preserve locked quick-loop props (returns cloned quickLoop with running: false instead of undefined). Adds tests covering locked quick loops.

Changes

Cohort / File(s) Summary
Quick Loop Service Logic
packages/job-worker/src/playout/model/services/QuickLoopService.ts
getUpdatedPropsByClearingMarkers() now returns a cloned quickLoop with running: false when quickLoop.locked is true; unlocked case still clears start/end and sets running: false.
Playout model tests
packages/job-worker/src/playout/model/implementation/__tests__/PlayoutModelImpl.spec.ts
Added tests for locked playlist quick loops verifying quickLoop.locked and marker types persist across activate/reset/deactivate and that activation/currentPart behavior updates accordingly.
Timing Calculation Utility
packages/webui/src/client/lib/rundownTiming.ts
Precomputes entirePlaylistIsLooping and updates countdown/nullify/skip logic so quick-loop decisions consider entire-playlist looping (do not null/skip when entire playlist is looping).
SegmentList UI
packages/webui/src/client/ui/SegmentList/LinePart.tsx, packages/webui/src/client/ui/SegmentList/SegmentList.tsx
LinePart gains isEntirePlaylistLooping prop; outside-quickloop class now requires !isEntirePlaylistLooping. SegmentList computes and passes both loop flags.
SegmentStoryboard UI
packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx, packages/webui/src/client/ui/SegmentStoryboard/StoryboardPart.tsx
Compute and pass isPlaylistLooping and new isEntirePlaylistLooping; StoryboardPart adds prop and excludes outside-quickloop when entire playlist is looping.
SegmentTimeline UI
packages/webui/src/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx
Render logic now uses aliased getIs* resolvers and requires !getIsEntirePlaylistLooping(...) before applying outside-quickloop styling.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Web UI
    participant Resolver as RundownResolver
    participant Timing as RundownTiming
    participant Model as Playout Model
    participant Service as QuickLoopService

    User->>UI: open playlist / interact
    UI->>Resolver: getIsLoopRunning(playlist)
    UI->>Resolver: getIsEntirePlaylistLooping(playlist)
    UI->>Timing: request countdowns (pass entirePlaylistIsLooping)
    Timing-->>UI: countdowns (nullify/skip only if !entirePlaylistIsLooping)
    UI->>Model: activate/reset/deactivate playlist
    Model->>Service: getUpdatedPropsByClearingMarkers(...)
    Service-->>Model: return cloned quickLoop (if locked -> running=false)
    Model-->>UI: updated playlist state
    UI->>Resolver: recompute loop flags
    UI->>UI: render parts with isEntirePlaylistLooping affecting CSS
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • Julusian
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main changes: fixing looping behavior and timing calculations in QuickLoopService for looping Rundown Playlists, which aligns with the core objectives of the changeset.
Description check ✅ Passed The description is directly related to the changeset, explaining the current behavior (missing countdowns, cleared looping state), new behavior (correct countdowns and preserved state), and affected areas (QuickLoop logic and timing calculations).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@jstarpl jstarpl added Contribution External contribution Contribution from NRK Contributions sponsored by NRK (nrk.no) labels Apr 1, 2026
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 1, 2026

Codecov Report

❌ Patch coverage is 91.66667% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
packages/webui/src/client/lib/rundownTiming.ts 80.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@jstarpl jstarpl changed the title fix: looping behavior and timing calculations in QuickLoopService for looping Rundown Playlists. fix: looping behavior and timing calculations in QuickLoopService for looping Rundown Playlists Apr 1, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/webui/src/client/lib/rundownTiming.ts (1)

139-140: Consider centralizing the whole-playlist-loop special case.

findPartInstancesInQuickLoop() still models PLAYLIST -> PLAYLIST looping as an empty membership map, so updateDurations() now needs an extra side-channel to recover the right behavior. A shared helper, or normalizing the partsInQuickLoop contract in one place, would make the timing math and the three UI views much less likely to drift again.

Also applies to: 510-564

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/webui/src/client/lib/rundownTiming.ts` around lines 139 - 140, The
special-case for an entire-playlist loop is handled inconsistently:
findPartInstancesInQuickLoop models PLAYLIST->PLAYLIST looping as an empty
membership map while updateDurations uses the side-channel
entirePlaylistIsLooping; centralize this by normalizing the partsInQuickLoop
contract in one place (either inside findPartInstancesInQuickLoop or a new
helper like normalizePartsInQuickLoop) so that PLAYLIST->PLAYLIST returns an
explicit mapping instead of an empty map, then remove the extra
entirePlaylistIsLooping/side-channel usage in updateDurations and callers
(including where partsInQuickLoop is read) so all timing math and UI views
consume the unified representation consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/webui/src/client/lib/rundownTiming.ts`:
- Around line 139-140: The special-case for an entire-playlist loop is handled
inconsistently: findPartInstancesInQuickLoop models PLAYLIST->PLAYLIST looping
as an empty membership map while updateDurations uses the side-channel
entirePlaylistIsLooping; centralize this by normalizing the partsInQuickLoop
contract in one place (either inside findPartInstancesInQuickLoop or a new
helper like normalizePartsInQuickLoop) so that PLAYLIST->PLAYLIST returns an
explicit mapping instead of an empty map, then remove the extra
entirePlaylistIsLooping/side-channel usage in updateDurations and callers
(including where partsInQuickLoop is read) so all timing math and UI views
consume the unified representation consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 59a378b5-a634-4cc1-9d2b-85424a354a86

📥 Commits

Reviewing files that changed from the base of the PR and between 67f02a4 and 6a0f248.

📒 Files selected for processing (7)
  • packages/job-worker/src/playout/model/services/QuickLoopService.ts
  • packages/webui/src/client/lib/rundownTiming.ts
  • packages/webui/src/client/ui/SegmentList/LinePart.tsx
  • packages/webui/src/client/ui/SegmentList/SegmentList.tsx
  • packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx
  • packages/webui/src/client/ui/SegmentStoryboard/StoryboardPart.tsx
  • packages/webui/src/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/job-worker/src/playout/model/implementation/__tests__/PlayoutModelImpl.spec.ts (1)

242-255: Consider asserting quickLoop.running remains false through lifecycle calls.

The test already validates locked and marker types. Adding running === false assertions after activatePlaylist, resetPlaylist, and deactivatePlaylist would better pin the intended locked-loop behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/job-worker/src/playout/model/implementation/__tests__/PlayoutModelImpl.spec.ts`
around lines 242 - 255, Add assertions that quickLoop.running stays false after
the lifecycle calls: after calling model.activatePlaylist(false),
model.resetPlaylist(true), and model.deactivatePlaylist() assert
model.playlist.quickLoop?.running === false in each place; update the test block
that currently checks quickLoop.locked and marker types (and references
QuickLoopMarkerType.PLAYLIST) to include these running === false checks so the
intended locked-but-not-running behavior is explicitly validated.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@packages/job-worker/src/playout/model/implementation/__tests__/PlayoutModelImpl.spec.ts`:
- Line 227: The test calls jest.setSystemTime(TIME_NOW) inside the "quickLoop
for locked playlist loops" describe block but never enables fake timers; update
that describe block's beforeEach to call jest.useFakeTimers() before calling
jest.setSystemTime(TIME_NOW) (and ensure the corresponding afterEach restores
timers with jest.useRealTimers()); look for the describe titled "quickLoop for
locked playlist loops", the beforeEach that references TIME_NOW and
jest.setSystemTime, and add the jest.useFakeTimers() call there.

---

Nitpick comments:
In
`@packages/job-worker/src/playout/model/implementation/__tests__/PlayoutModelImpl.spec.ts`:
- Around line 242-255: Add assertions that quickLoop.running stays false after
the lifecycle calls: after calling model.activatePlaylist(false),
model.resetPlaylist(true), and model.deactivatePlaylist() assert
model.playlist.quickLoop?.running === false in each place; update the test block
that currently checks quickLoop.locked and marker types (and references
QuickLoopMarkerType.PLAYLIST) to include these running === false checks so the
intended locked-but-not-running behavior is explicitly validated.
🪄 Autofix (Beta)

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

Run ID: 2e0ce276-15f7-4809-84f5-492c6a489c96

📥 Commits

Reviewing files that changed from the base of the PR and between 6a0f248 and c986297.

📒 Files selected for processing (1)
  • packages/job-worker/src/playout/model/implementation/__tests__/PlayoutModelImpl.spec.ts

@jstarpl jstarpl force-pushed the contrib/fix/looping-rundown branch 2 times, most recently from 9cdd590 to 321d21e Compare April 1, 2026 13:41
@jstarpl jstarpl removed the Contribution External contribution label Apr 2, 2026
@jstarpl jstarpl force-pushed the contrib/fix/looping-rundown branch from 321d21e to 174c24f Compare April 10, 2026 10:03
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@packages/webui/src/client/lib/rundownTiming.ts`:
- Line 139: The flag entirePlaylistIsLooping should reflect an active loop, not
just marker coverage: replace the current use of
isEntirePlaylistLooping(playlist) with a conjunction of isLoopRunning(playlist)
&& isEntirePlaylistLooping(playlist) where the variable entirePlaylistIsLooping
is defined/used so that loop behavior is gated by both the markers spanning the
playlist and the loop actually running (use the isLoopRunning and
isEntirePlaylistLooping helpers and update any downstream logic that relies on
entirePlaylistIsLooping accordingly).
🪄 Autofix (Beta)

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

Run ID: 528581cc-d01e-4595-942c-65923c2b81ca

📥 Commits

Reviewing files that changed from the base of the PR and between 9cdd590 and 174c24f.

📒 Files selected for processing (8)
  • packages/job-worker/src/playout/model/implementation/__tests__/PlayoutModelImpl.spec.ts
  • packages/job-worker/src/playout/model/services/QuickLoopService.ts
  • packages/webui/src/client/lib/rundownTiming.ts
  • packages/webui/src/client/ui/SegmentList/LinePart.tsx
  • packages/webui/src/client/ui/SegmentList/SegmentList.tsx
  • packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx
  • packages/webui/src/client/ui/SegmentStoryboard/StoryboardPart.tsx
  • packages/webui/src/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx
✅ Files skipped from review due to trivial changes (1)
  • packages/job-worker/src/playout/model/implementation/tests/PlayoutModelImpl.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/webui/src/client/ui/SegmentStoryboard/StoryboardPart.tsx
  • packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx
  • packages/webui/src/client/ui/SegmentList/SegmentList.tsx

let lastSegmentIds: { segmentId: SegmentId; segmentPlayoutId: SegmentPlayoutId } | undefined = undefined
let nextRundownAnchor: number | undefined = undefined

const entirePlaylistIsLooping = isEntirePlaylistLooping(playlist)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Gate the full-playlist loop flag on running, not just marker coverage.

isEntirePlaylistLooping() only tells you that the loop markers span the playlist. After this PR preserves locked quick-loop props with running: false, Line 510, Line 542, and Line 564 will still keep pre-current parts countdownable during reset/deactivation, even though the loop is no longer active. Please derive this flag from both isLoopRunning(playlist) and isEntirePlaylistLooping(playlist).

Suggested fix
-		const entirePlaylistIsLooping = isEntirePlaylistLooping(playlist)
+		const entirePlaylistLoopIsRunning =
+			isLoopRunning(playlist) && isEntirePlaylistLooping(playlist)
...
-					if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])] && !entirePlaylistIsLooping) {
+					if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])] && !entirePlaylistLoopIsRunning) {
...
-					if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])] && !entirePlaylistIsLooping) {
+					if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])] && !entirePlaylistLoopIsRunning) {
...
-					if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])] && !entirePlaylistIsLooping) continue
+					if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])] && !entirePlaylistLoopIsRunning) continue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/webui/src/client/lib/rundownTiming.ts` at line 139, The flag
entirePlaylistIsLooping should reflect an active loop, not just marker coverage:
replace the current use of isEntirePlaylistLooping(playlist) with a conjunction
of isLoopRunning(playlist) && isEntirePlaylistLooping(playlist) where the
variable entirePlaylistIsLooping is defined/used so that loop behavior is gated
by both the markers spanning the playlist and the loop actually running (use the
isLoopRunning and isEntirePlaylistLooping helpers and update any downstream
logic that relies on entirePlaylistIsLooping accordingly).

@jstarpl jstarpl force-pushed the contrib/fix/looping-rundown branch from 174c24f to f0201af Compare April 10, 2026 10:30
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/webui/src/client/lib/rundownTiming.ts (1)

143-143: ⚠️ Potential issue | 🟠 Major

Gate full-playlist behavior on active loop, not marker coverage alone.

At Line 143, entirePlaylistIsLooping is derived without checking whether looping is currently running. That leaks into Lines 514/546/568 and can keep pre-current countdown logic active when markers exist but loop runtime state is off (e.g., after reset/deactivate with locked markers).

Suggested fix
-		const entirePlaylistIsLooping = isEntirePlaylistLooping(playlist)
+		const loopIsRunning = isLoopRunning(playlist)
+		const entirePlaylistLoopIsRunning = loopIsRunning && isEntirePlaylistLooping(playlist)
...
-					if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])] && !entirePlaylistIsLooping) {
+					if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])] && !entirePlaylistLoopIsRunning) {
...
-					if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])] && !entirePlaylistIsLooping) {
+					if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])] && !entirePlaylistLoopIsRunning) {
...
-				if (isLoopRunning(playlist)) {
+				if (loopIsRunning) {
...
-					if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])] && !entirePlaylistIsLooping) continue
+					if (!partsInQuickLoop[unprotectString(this.linearParts[i][0])] && !entirePlaylistLoopIsRunning) continue
#!/bin/bash
set -euo pipefail

echo "Locate stateCacheResolver implementation:"
fd -i 'stateCacheResolver.*'

echo
echo "Inspect helper definitions:"
rg -n -C3 'isEntirePlaylistLooping|isLoopRunning' $(fd -i 'stateCacheResolver.*')

echo
echo "Inspect current rundownTiming call sites:"
rg -n -C2 'entirePlaylistIsLooping|entirePlaylistLoopIsRunning|isLoopRunning\(playlist\)' packages/webui/src/client/lib/rundownTiming.ts

Also applies to: 514-515, 546-548, 568-568

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/webui/src/client/lib/rundownTiming.ts` at line 143,
entirePlaylistIsLooping is currently set from isEntirePlaylistLooping(playlist)
without checking runtime loop state, which lets pre-current countdown logic
remain active when markers exist but looping is not running; change the
assignment to include the run-state check (e.g., set entirePlaylistIsLooping =
isEntirePlaylistLooping(playlist) && isLoopRunning(playlist) or use the
resolver-provided entirePlaylistLoopIsRunning flag), and update the dependent
uses (the places that reference entirePlaylistIsLooping in this file) so they
are gated by the loop-running condition rather than marker coverage alone.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/webui/src/client/lib/rundownTiming.ts`:
- Line 143: entirePlaylistIsLooping is currently set from
isEntirePlaylistLooping(playlist) without checking runtime loop state, which
lets pre-current countdown logic remain active when markers exist but looping is
not running; change the assignment to include the run-state check (e.g., set
entirePlaylistIsLooping = isEntirePlaylistLooping(playlist) &&
isLoopRunning(playlist) or use the resolver-provided entirePlaylistLoopIsRunning
flag), and update the dependent uses (the places that reference
entirePlaylistIsLooping in this file) so they are gated by the loop-running
condition rather than marker coverage alone.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4045cc2b-def9-4336-a299-8e26783f1dc4

📥 Commits

Reviewing files that changed from the base of the PR and between 174c24f and f0201af.

📒 Files selected for processing (8)
  • packages/job-worker/src/playout/model/implementation/__tests__/PlayoutModelImpl.spec.ts
  • packages/job-worker/src/playout/model/services/QuickLoopService.ts
  • packages/webui/src/client/lib/rundownTiming.ts
  • packages/webui/src/client/ui/SegmentList/LinePart.tsx
  • packages/webui/src/client/ui/SegmentList/SegmentList.tsx
  • packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx
  • packages/webui/src/client/ui/SegmentStoryboard/StoryboardPart.tsx
  • packages/webui/src/client/ui/SegmentTimeline/Parts/SegmentTimelinePart.tsx
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/webui/src/client/ui/SegmentStoryboard/StoryboardPart.tsx
  • packages/webui/src/client/ui/SegmentList/SegmentList.tsx
  • packages/webui/src/client/ui/SegmentList/LinePart.tsx
  • packages/webui/src/client/ui/SegmentStoryboard/SegmentStoryboard.tsx
  • packages/job-worker/src/playout/model/implementation/tests/PlayoutModelImpl.spec.ts

@jstarpl jstarpl merged commit 2165350 into Sofie-Automation:main Apr 10, 2026
24 checks passed
@jstarpl jstarpl deleted the contrib/fix/looping-rundown branch April 10, 2026 10:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Contribution from NRK Contributions sponsored by NRK (nrk.no)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant