diff --git a/web/src/lib/serverEvents.svelte.ts b/web/src/lib/serverEvents.svelte.ts
index d3ab5bcc..98699045 100644
--- a/web/src/lib/serverEvents.svelte.ts
+++ b/web/src/lib/serverEvents.svelte.ts
@@ -12,7 +12,20 @@ import { user } from '$lib/auth/store.svelte';
// deliberately do NOT yank a playing queue here — that would interrupt a
// mid-song listen to restart the new mix at track 0.
+// Monotonic rebuild counter (#980). Bumped on every playlist.system_rebuilt.
+// An open system-playlist DETAIL page can't be invalidated in place (its id
+// rotates on rebuild — a refetch would 404), so instead it watches this
+// counter and offers a "this mix was refreshed → Refresh" affordance that
+// re-resolves the variant to the new playlist.
+let _systemRebuildCount = $state(0);
+export const systemRebuilt = {
+ get count(): number {
+ return _systemRebuildCount;
+ }
+};
+
function onSystemRebuilt(): void {
+ _systemRebuildCount++;
queryClient.invalidateQueries({ queryKey: qk.home() });
// Prefix match invalidates every kind ('user' | 'system' | 'all').
queryClient.invalidateQueries({ queryKey: ['playlists'] });
diff --git a/web/src/routes/playlists/[id]/+page.svelte b/web/src/routes/playlists/[id]/+page.svelte
index f35fcd06..5581aecb 100644
--- a/web/src/routes/playlists/[id]/+page.svelte
+++ b/web/src/routes/playlists/[id]/+page.svelte
@@ -1,6 +1,7 @@
@@ -187,6 +224,24 @@
playlistQuery.refetch()} />
{:else if playlistQuery?.data}
{@const pl = playlistQuery.data}
+ {#if staleSystemView}
+