diff --git a/backend/src/processing/processor/playlist.rs b/backend/src/processing/processor/playlist.rs index aad225b36..c2770c562 100644 --- a/backend/src/processing/processor/playlist.rs +++ b/backend/src/processing/processor/playlist.rs @@ -558,10 +558,16 @@ async fn download_input( ) -> (Vec, Box, Option) { // Coordination Logic let need_download = !ctx.is_input_downloaded(&input.name).await; + // Keep this lock for the whole critical section (download + persist/load + mark processed) + // so parallel sources sharing the same input cannot observe a half-written state. + let _input_lock = if need_download { + Some(ctx.get_input_lock(&input.name).await) + } else { + None + }; + let mut mark_as_processed = false; let playlist_download_result = if need_download { - // Acquire named lock to prevent thundering herd on same input - let _input_lock = ctx.get_input_lock(&input.name).await; // Check again after lock let already_processed = ctx.is_input_downloaded(&input.name).await; @@ -569,17 +575,13 @@ async fn download_input( // Use empty results, will load from disk below PlaylistDownloadResult::new(vec![], vec![], true, false) } else if ctx.pre_processed_inputs.as_ref().is_some_and(|s| s.contains(&input.name)) { - // Input was already processed in a prior session; skip download and mark as processed - // for this session to avoid redundant lock acquisitions from other targets. - ctx.mark_input_downloaded(input.name.clone()).await; + // Input was already processed in a prior session; skip download and load from disk. + // Mark only after load succeeds (or fails) to avoid exposing a half-ready state. + mark_as_processed = true; PlaylistDownloadResult::new(vec![], vec![], true, false) } else { - let res = playlist_download_from_input(&ctx.client, &ctx.config, input).await; - // Mark as processed if NO critical errors? - // playlist_download_from_input returns errors but also potentially a partial playlist. - // If it attempted download, we consider it processed for this session. - ctx.mark_input_downloaded(input.name.clone()).await; - res + mark_as_processed = true; + playlist_download_from_input(&ctx.client, &ctx.config, input).await } } else { PlaylistDownloadResult::new(vec![], vec![], true, false) @@ -595,6 +597,12 @@ async fn download_input( let (pl, err) = persist_input_playlist(&ctx.config, input, playlist_download_result.downloaded_playlist).await; (MemoryPlaylistSource::new(pl).boxed(), err) }; + + if mark_as_processed { + // Mark after persist/load so other workers only see this input as ready when data is usable. + ctx.mark_input_downloaded(input.name.clone()).await; + } + (playlist_download_result.download_err, playlist, error) } diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index db14a95bc..8bd38bbb4 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -28,6 +28,7 @@ futures-signals = "0.3" futures = "0.3" prost = "0" wasm-bindgen-futures = "0" +js-sys = "0.3" bytes = "1" regex = "1.12.3" base64 = "0.22.1" @@ -37,9 +38,11 @@ indexmap = "2.13.0" [dependencies.web-sys] version = "0.3" -features = ["Document", "Element", "Node", "Window", "HtmlCollection", "HtmlImageElement", "HtmlInputElement", "DomRect", "HtmlDialogElement", "CssStyleDeclaration"] +features = ["Document", "Element", "Node", "Window", "HtmlCollection", "HtmlImageElement", "HtmlInputElement", + "DomRect", "HtmlDialogElement", "CssStyleDeclaration", + "Event", "WebGlRenderingContext", "WebGlProgram", "WebGlShader", "WebGlBuffer", "WebGlUniformLocation", + "HtmlCanvasElement"] [dev-dependencies] -js-sys = "0.3" wasm-bindgen-test = "0.3" gloo-utils = "0.2" diff --git a/frontend/scss/_theme.scss b/frontend/scss/_theme.scss index 55e8a2680..2f1236b1a 100644 --- a/frontend/scss/_theme.scss +++ b/frontend/scss/_theme.scss @@ -78,6 +78,8 @@ --modest-text-color: #a7abb4; --background-color: #0a0b10; --floating-background-color: #071529; + --floating-background-accent-color-1: #6aa7ff; + --floating-background-accent-color-2: #7ab6ff; --border-color: rgba(60, 100, 195, 0.25); --input-background-color: rgba(255, 255, 255, 0); --input-border-color: var(--border-color); @@ -408,6 +410,8 @@ body[data-theme="bright"] { --background-color: #f8fafc; /* Very light slate */ --floating-background-color: #ffffff; + --floating-background-accent-color-1: #6aa7ff; + --floating-background-accent-color-2: #7ab6ff; --text-color: #0f172a; /* Slate 900 */ --modest-text-color: #64748b; @@ -646,4 +650,4 @@ body[data-theme="bright"] { --breadcrumb-color: #64748b; --breadcrumb-active-background-color: #dcfce7; --breadcrumb-active-color: #166534; -} \ No newline at end of file +} diff --git a/frontend/scss/app/_component.scss b/frontend/scss/app/_component.scss index 45854b1f4..b3d40d6db 100644 --- a/frontend/scss/app/_component.scss +++ b/frontend/scss/app/_component.scss @@ -81,7 +81,7 @@ @forward "components/loading_screen"; @forward "components/websocket_status"; @forward "components/playlist/epg_view"; -@forward "components/floating_background"; +@forward "components/particle_flow_background"; @forward "components/source_editor/source_editor"; @forward "components/source_editor/source_editor_form"; @forward "components/source_editor/source_editor_block"; diff --git a/frontend/scss/app/components/_floating_background.scss b/frontend/scss/app/components/_floating_background.scss deleted file mode 100644 index 39d5f0a47..000000000 --- a/frontend/scss/app/components/_floating_background.scss +++ /dev/null @@ -1,39 +0,0 @@ -.tp__floating-background { - position: fixed; - inset: 0; - width: 100vw; - height: 100vh; - overflow: hidden; - pointer-events: none; - - //background-color: var(--floating-background-color); - background: radial-gradient(var(--floating-background-color) 0%, var(--background-color) 70%); - z-index: -1; - - .tp__star { - position: absolute; - - pointer-events: none; - user-select: none; - - border-radius: 50%; - - //.organic-circle { - background: radial-gradient(circle, var(--floating-particle-gradient-color-1) 0%, var(--floating-particle-gradient-color-2) 70%); - animation: organic-wobble 8s ease-in-out infinite; - filter: blur(3px); - will-change: transform; - - @media (prefers-reduced-motion: reduce) { - animation: none; - transform: none; - } - } - @keyframes organic-wobble { - 0% { transform: scale(1); } - 25% { transform: scale(1.3); } - 50% { transform: scale(0.7); } - 75% { transform: scale(1.3); } - 100% { transform: scale(1); } - } -} diff --git a/frontend/scss/app/components/_particle_flow_background.scss b/frontend/scss/app/components/_particle_flow_background.scss new file mode 100644 index 000000000..496d8cdb4 --- /dev/null +++ b/frontend/scss/app/components/_particle_flow_background.scss @@ -0,0 +1,23 @@ +.tp__particle-flow-background, +.tp__floating-background { + position: fixed; + inset: 0; + width: 100%; + height: 100%; + overflow: hidden; + pointer-events: none; + z-index: 0; + isolation: isolate; + background: + radial-gradient( + ellipse at 20% 8%, + color-mix(in srgb, var(--floating-background-color), var(--floating-background-accent-color-1) 20%) 0%, + transparent 50% + ), + radial-gradient( + ellipse at 78% -5%, + color-mix(in srgb, var(--floating-background-color), var(--floating-background-accent-color-2) 12%) 0%, + transparent 45% + ), + linear-gradient(180deg, var(--floating-background-color) 0%, var(--background-color) 84%); +} diff --git a/frontend/src/app/components/floating_background.rs b/frontend/src/app/components/floating_background.rs deleted file mode 100644 index 77d53d7ab..000000000 --- a/frontend/src/app/components/floating_background.rs +++ /dev/null @@ -1,77 +0,0 @@ -use gloo_timers::callback::Interval; -use gloo_utils::window; -use yew::prelude::*; - -#[derive(Clone, Debug, Properties, PartialEq)] -struct Star { - x: f64, - y: f64, - size: f64, - dx: f64, - dy: f64, - anim_delay: f64, -} - -#[function_component] -pub fn FloatingBackground() -> Html { - let stars = use_state(|| { - let width = window().inner_width().unwrap().as_f64().unwrap_or(800.0); - let height = window().inner_height().unwrap().as_f64().unwrap_or(600.0); - let area = width * height; - let num_stars = ((area / 50000.0) as usize).clamp(10, 40); - let mut rng = fastrand::Rng::new(); - let stars: Vec = (0..num_stars) - .map(|_| Star { - x: rng.f64() * 100.0, - y: rng.f64() * 100.0, - size: 16.0 + fastrand::f64() * (5.0 - 2.0), - dx: -0.025 + rng.f64() * 0.1, - dy: -0.025 + rng.f64() * 0.1, - anim_delay: rng.f64() * 8.0, - }) - .collect(); - stars - }); - - { - let stars = stars.clone(); - use_effect(move || { - let interval = Interval::new(200, move || { - stars.set( - stars - .iter() - .map(|s| { - let mut star = s.clone(); - star.x += star.dx; - star.y += star.dy; - if star.x < 0.0 { - star.x = 100.0; - } - if star.x > 100.0 { - star.x = 0.0; - } - if star.y < 0.0 { - star.y = 100.0; - } - if star.y > 100.0 { - star.y = 0.0; - } - star - }) - .collect::>(), - ); - }); - move || drop(interval) - }); - } - - html! { -
- { for stars.iter().map(|s| html! { -
- })} -
- } -} diff --git a/frontend/src/app/components/login.rs b/frontend/src/app/components/login.rs index 14cf8ce2b..e10899568 100644 --- a/frontend/src/app/components/login.rs +++ b/frontend/src/app/components/login.rs @@ -1,5 +1,5 @@ use crate::{ - app::components::{floating_background::FloatingBackground, input::Input, svg_icon::AppIcon, TextButton}, + app::components::{input::Input, svg_icon::AppIcon, ParticleFlowBackground, TextButton}, hooks::use_service_context, }; use web_sys::HtmlInputElement; @@ -74,7 +74,7 @@ pub fn Login() -> Html { html! { <> - +