Skip to content

fix: Fix slides from getting stuck on live photos caused by polling loop#586

Open
mikemulhearn wants to merge 2 commits into
damongolding:mainfrom
mikemulhearn:feat/live_photo_endless_loop_fix
Open

fix: Fix slides from getting stuck on live photos caused by polling loop#586
mikemulhearn wants to merge 2 commits into
damongolding:mainfrom
mikemulhearn:feat/live_photo_endless_loop_fix

Conversation

@mikemulhearn
Copy link
Copy Markdown
Contributor

@mikemulhearn mikemulhearn commented Nov 27, 2025

This PR fixes an issue where the slideshow would freeze indefinitely on some slides containing Live Photos.

The introduction of Live Photos, which is such an awesome feature, resulted in my slideshow getting stuck on certain live photos, usually the same ones each time. The progress bar was doing the initial polling from 0 to a small value, but then it kept appearing to restart the poll process, and the live photos were not playing nor were the slides advancing after the configured 15 seconds.

The fix ensures that the slideshow timer (polling) is only paused and reset for real slide-change requests (/asset/new and /asset/offline) and not for background requests such as /live/{id} (Live Photo video loading) or /refresh/check (server health checks).

Potentially relevant config options

layout: splitview
show_videos: true
live_photos: true
live_photo_loop_delay: 2
use_original_image: false
offline_mode:
  enabled: false
kiosk:
  cache: true
  prefetch: true

Logs prior to the fix:

[DEBUG] startPolling() called { pollInterval: 15000 }

-- Live Photo video loading --
[DEBUG] htmx:afterRequest { path: '/live/04d245d1-76a2-4e8d-b571...', status: 200 }
[DEBUG] htmx:afterRequest { path: '/live/d9c63e33...', status: 200 }

-- Server health check --
[DEBUG] htmx:afterRequest { path: '/refresh/check', status: 204 }
[DEBUG] htmx:afterRequest { path: '/refresh/check', status: 204 }
(repeats indefinitely)

Root Cause

Prior to this PR:

1. Every HTMX request triggered setRequestLock(), including:

  • /asset/new
  • /asset/offline
  • /live/<id>
  • /weather

setRequestLock() called:
pausePolling(false)

This canceled the active animation frame, pausing the slideshow progress.

2. Polling was not being restarted for non-asset endpoints

The original reset loop bug was fixed by ensuring only /asset/* calls restart polling.

But because /refresh/check and /live/<id> still invoked setRequestLock() (pause) and did not restart polling afterward, the slideshow remained indefinitely paused.

The Fix

1. Only pause polling for real slide-changing (asset) requests

In setRequestLock():


// Only lock/pause for asset requests
if (!path.startsWith("/asset/")) {
    return;
}

This prevent pausePolling() from running for:

  • /live/id (live photos)
  • /refresh/check
  • /weather

2. Polling restart is handled exclusively by TypeScript

Template hx-on::after-request="kiosk.startPolling()" was removed.

A new TS-only handler restarts polling exclusively for asset routes:

    const path = e.detail?.pathInfo?.requestPath || "";
    if (path.startsWith("/asset/")) {
        startPolling();
    }
});

Result

  • /asset/new loads -- slideshow starts normally
  • /live/<id> fails repeatedly -- slideshow keeps running anyway
  • /refresh/check continues -- slideshow unaffected
  • Slide advances normally after its configured duration
  • Live Photos no longer freeze

Testing

  • Only /asset/ triggers startPolling()
  • /live and /refresh/check do not pause or reset polling
  • Progress bar moves smoothly from 0% to 100% over slide duration
  • Slides advance even when Live Photo videos fail to load

Impact

This PR:

  • Fixes a major slideshow freeze affecting Live Photos
  • Fixes splitview / multi-photo cases with multiple Live Photos
  • Makes slideshow timing 100% reliable
  • Prevents background network activity from affecting slideshow behavior
  • Simplifies reasoning about polling lifecycle (asset-only)

Conclusion

This PR resolves an issue where Live Photos could freeze the slideshow. By ensuring polling is controlled only by actual asset transitions, and never by background requests, the slideshow becomes fully stable and predictable, even when Live Photo videos fail.

Summary by CodeRabbit

Release Notes

  • Refactor

    • Optimised polling and asset loading mechanisms for improved reliability and control.
  • Chores

    • Enhanced debug logging to support monitoring and troubleshooting.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 27, 2025

Walkthrough

Modified HTMX request handling to restart polling specifically when asset requests complete, whilst removing automatic polling restart from the Home template. Updated request locking to apply only to asset paths. Added debug logging for polling initiation timestamps.

Changes

Cohort / File(s) Summary
Request and polling event handling
frontend/src/ts/kiosk.ts, internal/templates/views/views_home.templ
Introduced HTMX afterRequest handler in kiosk.ts to resume polling upon asset request completion; modified setRequestLock to conditionally apply locking only for asset paths. Removed automatic polling restart from Home template's main element.
Polling debug instrumentation
frontend/src/ts/polling.ts
Added console.debug logging in startPolling function to output poll interval and timestamp.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant HTMX
    participant Kiosk as kiosk.ts
    participant Server

    rect rgb(240, 248, 255)
    Note over User,Server: Previous Flow (Removed)
    User->>HTMX: Trigger request
    HTMX->>Server: Send request
    Server-->>HTMX: Response
    HTMX->>Kiosk: hx-on::after-request<br/>(startPolling)
    Kiosk->>Kiosk: Resume polling
    end

    rect rgb(245, 255, 250)
    Note over User,Server: New Flow (Asset Request)
    User->>HTMX: Trigger asset request
    HTMX->>Server: GET /asset/...
    Server-->>HTMX: Asset response
    HTMX->>Kiosk: afterRequest handler<br/>(path check)
    alt Asset path detected
        Kiosk->>Kiosk: Resume polling
    else Non-asset path
        Kiosk->>Kiosk: Skip polling restart
    end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

  • Path-based conditional logic in setRequestLock and afterRequest handler requires verification of intended asset request filtering
  • HTMX event handler integration and template removal requires confirmation of polling restart semantics across request types
  • Debug logging addition should be verified for appropriate log levels and performance impact

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 Polling now pauses with care,
Only when assets float through the air,
No more constant restart on the beat,
Path-wise precision, our rhythm's now sweet!
With debug logs hoppingly bright,
This kiosk shall dance through the night.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: preventing slides from getting stuck during Live Photo playback by addressing a polling loop issue.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e4b142 and aae9cbb.

📒 Files selected for processing (3)
  • frontend/src/ts/kiosk.ts (2 hunks)
  • frontend/src/ts/polling.ts (1 hunks)
  • internal/templates/views/views_home.templ (0 hunks)
💤 Files with no reviewable changes (1)
  • internal/templates/views/views_home.templ
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/ts/kiosk.ts (1)
frontend/src/ts/polling.ts (1)
  • startPolling (379-379)
🔇 Additional comments (3)
frontend/src/ts/polling.ts (1)

129-132: LGTM! Helpful debugging addition.

The debug logging provides useful visibility into polling lifecycle events without affecting behaviour. This aligns well with the PR's focus on polling control issues.

frontend/src/ts/kiosk.ts (2)

430-435: Correct implementation of selective request locking.

The early return for non-asset paths ensures that requests to /live/{id}, /refresh/check, and similar endpoints no longer pause polling or disable navigation. This directly addresses the root cause described in the PR objectives where Live Photo video loading was freezing the slideshow.

The path-checking logic is consistent with the afterRequest handler above, both using startsWith("/asset/").


291-301: I attempted to run verification scripts but encountered a repository access issue. Let me provide my analysis based on the code snippet provided:

Unable to complete full verification due to repository access limitations.

The polling control logic at lines 291-301 is sound in principle: it restricts polling restarts to asset endpoints by checking path.startsWith("/asset/"). However, the verification request in the original review cannot be fully completed without access to the codebase to:

  1. Identify all asset-related endpoints and confirm they use the /asset/ prefix
  2. Check for alternative asset loading patterns (e.g., dynamically constructed paths, query parameters)
  3. Review the surrounding polling system context and confirm no edge cases are missed

Potential concerns:

  • The verification assumes all asset endpoints consistently use /asset/ prefix—this should be confirmed against the actual backend routes
  • The check uses only the path portion; if some asset requests include query strings or fragments, they're still matched correctly
  • No verification that non-asset requests (like /refresh/check, /weather) are properly excluded

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.

Copy link
Copy Markdown
Contributor

@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: 0

🧹 Nitpick comments (2)
internal/routes/routes_asset_helpers.go (1)

373-389: Well-designed helper function centralising blur-skip logic.

The shouldSkipBlur function clearly encapsulates the conditions for skipping blur processing:

  1. When background blur is explicitly disabled
  2. When using "cover" fit without Live Photos enabled

The function is well-documented and the logic is straightforward.

internal/routes/routes_test.go (1)

159-225: Comprehensive test coverage for blur-skipping logic.

The table-driven test covers the key scenarios for shouldSkipBlur, including combinations of BackgroundBlur, ImageFit, and LivePhotos. The test structure is clear and well-organised.

However, several test cases include fields like ImageEffect and Layout that aren't actually checked by shouldSkipBlur. For example, the test at line 181 ("image effect zoom") passes because LivePhotos defaults to false, not because of ImageEffect. Consider either:

  1. Removing irrelevant fields from test configs to avoid confusion
  2. Adding comments explaining which fields are relevant to the logic being tested

This is a minor clarity issue and doesn't affect correctness.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 14587f2 and 3e4b142.

📒 Files selected for processing (6)
  • frontend/src/ts/kiosk.ts (2 hunks)
  • frontend/src/ts/polling.ts (1 hunks)
  • internal/routes/routes_asset_helpers.go (1 hunks)
  • internal/routes/routes_test.go (1 hunks)
  • internal/templates/views/views_home.templ (0 hunks)
  • internal/video/video.go (0 hunks)
💤 Files with no reviewable changes (2)
  • internal/video/video.go
  • internal/templates/views/views_home.templ
🧰 Additional context used
🧬 Code graph analysis (3)
internal/routes/routes_test.go (1)
internal/config/config.go (1)
  • Config (208-436)
frontend/src/ts/kiosk.ts (1)
frontend/src/ts/polling.ts (1)
  • startPolling (379-379)
internal/routes/routes_asset_helpers.go (2)
internal/config/config.go (1)
  • Config (208-436)
internal/immich/immich.go (2)
  • AssetType (22-22)
  • ImageType (33-33)
🔇 Additional comments (4)
frontend/src/ts/polling.ts (1)

129-132: LGTM! Helpful debug logging added.

The debug log provides useful visibility into when polling starts and the configured interval, which will aid in diagnosing any future polling-related issues.

frontend/src/ts/kiosk.ts (2)

291-301: Path-aware polling restart correctly implemented.

This handler ensures startPolling() is only called for asset endpoint responses (paths starting with /asset/), preventing non-asset requests like /live/{id} and /refresh/check from interfering with the slideshow timing. The implementation aligns well with the PR objectives.


430-435: Request locking correctly limited to asset requests.

The early return for non-asset paths prevents /live/, /refresh/check, and /weather requests from pausing polling or setting the request lock, which resolves the root cause of the slideshow freeze. The path check is clear and follows the same pattern as the after-request handler.

internal/routes/routes_asset_helpers.go (1)

395-395: I'll help you verify the refactor of the blur-skipping logic. Let me search for the relevant implementations to confirm the behavior equivalence.
<function_calls>


#!/bin/bash

Search for processBlurredImage and related blur logic in the Go codebase

rg -nC5 'processBlurredImage' --type=go


</function_calls>


#!/bin/bash

Find shouldSkipBlur function definition

rg -nC10 'func shouldSkipBlur' --type=go


</function_calls>


#!/bin/bash

Look at the context around line 395 in routes_asset_helpers.go

sed -n '385,405p' internal/routes/routes_asset_helpers.go


</function_calls>
<function_calls>


#!/bin/bash

Search for git history to find previous implementation

git log -p --follow -S 'shouldSkipBlur' -- internal/routes/routes_asset_helpers.go | head -150


</function_calls>


#!/bin/bash

Get file line count to understand structure

wc -l internal/routes/routes_asset_helpers.go


</function_calls>


#!/bin/bash

Read the shouldSkipBlur function and surrounding context

rg -nB2 -A20 'func shouldSkipBlur' --type=go


</function_calls>


#!/bin/bash

Search for any inline blur condition logic that might have been replaced

rg -nC8 'skipBlur\s*:=' --type=go internal/routes/routes_asset_helpers.go


</function_calls>

Let me wait for these results to analyze the blur logic refactoring.

@mikemulhearn mikemulhearn force-pushed the feat/live_photo_endless_loop_fix branch from 3e4b142 to aae9cbb Compare November 27, 2025 08:24
@mikemulhearn
Copy link
Copy Markdown
Contributor Author

Accidentally included the background blur commits originally, rebased and removed those commits just now

@damongolding
Copy link
Copy Markdown
Owner

Did this fix the issue you were having?

setRequestLock is only called on <main> AJAX requests, which only deals with /assets/* so setRequestLock was/is never called for other endpoints such as /live/id, /refresh/check and /weather.

@mikemulhearn
Copy link
Copy Markdown
Contributor Author

Did this fix the issue you were having?

setRequestLock is only called on <main> AJAX requests, which only deals with /assets/* so setRequestLock was/is never called for other endpoints such as /live/id, /refresh/check and /weather.

So far, so good. I would like to continue to do some more A/B testing between existing version and this PR to ensure no bugs or other issues are introduced. I have a variety of low and high-power devices I am testing on to ensure solid performance and resilience: PC Web browsers, low and mid-range Android tablets, iPad, iPhone, Fire TV sticks, Echo Show 8 & 15, and Frameo Frame (Android 6).

During my initial debugging phase, I found that setRequestLock() was getting called via htmx:afterRequest in views_home.templ:

hx-on::after-request="kiosk.startPolling()"

During debugging, I changed the setRequestLock() function to a new function afterRequest() and added a console logging line to view when afterRequest() (previously setRequestLock()) was being called. I discovered that /weather, /live/{id}, and /refresh/check were all calling afterRequest() (previously setRequestLock()), in addition to /asset/new, which was causing lockups during playback of some live photos.

Snippet of actual debug log at that time:

kiosk.demo.js:2 [DEBUG] htmx:afterRequest {path: '/weather', status: 200, successful: true}
kiosk.demo.js:2 [DEBUG] kiosk polling afterRequest {path: '/weather'}
kiosk.demo.js:2 [DEBUG] htmx:afterRequest {path: '/asset/new', status: 200, successful: true}
kiosk.demo.js:2 [DEBUG] kiosk polling afterRequest {path: '/asset/new'}
kiosk.demo.js:2 [DEBUG] startPolling() called {pollInterval: 15000, timestamp: 2353.5}
kiosk.demo.js:2 [DEBUG] htmx:afterRequest {path: '/live/04d245d1-76a2-4e8d-b571-f3504261b52c', status: 200, successful: true}
kiosk.demo.js:2 [DEBUG] kiosk polling afterRequest {path: '/live/04d245d1-76a2-4e8d-b571-f3504261b52c'}
kiosk.demo.js:2 [DEBUG] htmx:afterRequest {path: '/live/d9c63e33-0be2-40f4-aa03-9e99d6b7e20a', status: 200, successful: true}
kiosk.demo.js:2 [DEBUG] kiosk polling afterRequest {path: '/live/d9c63e33-0be2-40f4-aa03-9e99d6b7e20a'}
kiosk.demo.js:2 [DEBUG] htmx:afterRequest {path: '/refresh/check', status: 204, successful: true}
kiosk.demo.js:2 [DEBUG] kiosk polling afterRequest {path: '/refresh/check'}
kiosk.demo.js:2 [DEBUG] htmx:afterRequest {path: '/refresh/check', status: 204, successful: true}
kiosk.demo.js:2 [DEBUG] kiosk polling afterRequest {path: '/refresh/check'}
kiosk.demo.js:2 [DEBUG] htmx:afterRequest {path: '/refresh/check', status: 204, successful: true}
kiosk.demo.js:2 [DEBUG] kiosk polling afterRequest {path: '/refresh/check'}
kiosk.demo.js:2 [DEBUG] htmx:afterRequest {path: '/refresh/check', status: 204, successful: true}
kiosk.demo.js:2 [DEBUG] kiosk polling afterRequest {path: '/refresh/check'}

I'd be happy to take another look, and move the setRequestLock() back to views_home.templ if you think that makes more sense. In that case, as long as we can effectively filter the hx:after-request in views_home/templ to only happen during /asset/ calls, we should be able to move it back.

@damongolding
Copy link
Copy Markdown
Owner

I think the thing that might have tripped you up here is that using hx-on::after-request= on a elements only triggers when that element (or children) makes requests.

<!-- kiosk.startPolling() only triggers from this element e.g. /assets/* --> 
<main
  id="kiosk"
  hx-post="/asset/new"
  hx-on::after-request="kiosk.startPolling()"
>

where as using this catches/is triggered on all htmx request

 htmx.on("htmx:afterRequest", (e: HTMXEvent) => {
  // gotta catch em all
});

That's why you would have seen setRequestLock being called all the time if you moved the setRequestLock function into the event listener htmx.on("htmx:afterRequest").

Does that make sense?

@damongolding
Copy link
Copy Markdown
Owner

Are you still seeing this issue @mikemulhearn ?

@hkuehl
Copy link
Copy Markdown

hkuehl commented May 5, 2026

Confirming this fixes the same bug for me — also reported in #673.

Setup: v0.38.0, splitview, live_photos: true, duration: 30. Without this PR my kiosk hangs after 20–60 min (live photos on) or up to ~1h
(live photos off), with the progress bar perpetually restarting at ~5%. With this PR applied: ran for 12h+ with no hangs.

Here is what Claude thinks is also worth mentioning:

@damongolding I think there's a small misunderstanding worth surfacing. hx-on::* attributes attach a normal DOM event listener to the element, and HTMX dispatches its events with bubbles: true. So hx-on::after-request="kiosk.startPolling()" on <main> fires for any HTMX request whose originating element is a descendant of <main> — not just /asset/*.

The relevant descendant is the live-photo poller in internal/templates/components/image/layout.templ:

<div
    style="display: none"
    hx-get={ "/live/" + imageData.ImmichAsset.LivePhotoVideoID }
    hx-swap="outerHTML"
    hx-trigger="load, every 1s"
></div>

Every second this fires htmx:afterRequest, the event bubbles to <main>, kiosk.startPolling() runs, lastPollTime is reset, and the slideshow timer never reaches pollInterval. If /live/<id> ever fails persistently (e.g. a transient DNS blip the browser then negative-caches), the kiosk is permanently stuck — exactly the ERR_NAME_NOT_RESOLVED loop in #673. setRequestLock is affected by the same bubbling.

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.

3 participants