diff --git a/backend/src/services/jobs.rs b/backend/src/services/jobs.rs deleted file mode 100644 index c41aa86..0000000 --- a/backend/src/services/jobs.rs +++ /dev/null @@ -1,73 +0,0 @@ -//! Shared shape for long-running background work. -//! -//! Today's [`compression`](crate::services::compression) and [`export`](crate::services::export) -//! pipelines each implement their own progress + SSE plumbing. They could converge on the -//! trait sketched here so future jobs (analytics, archival, ...) plug into one progress -//! pipeline. -//! -//! This module is intentionally a *sketch*: the existing services are not yet wired to -//! it. The aim is to (a) document the convention so new jobs follow it, (b) make the -//! refactor mechanical when someone is ready to do it. See `docs/IDEAS.md` — -//! "Maintainability principles" — for the rationale. -//! -//! Example of an eventual implementor: -//! -//! ```ignore -//! struct ZipExport { event_id: Uuid, /* … */ } -//! -//! impl BackgroundJob for ZipExport { -//! fn name(&self) -> &'static str { "zip-export" } -//! async fn run(self, ctx: JobContext) -> Result<()> { -//! for (i, item) in items.iter().enumerate() { -//! ctx.report(percent(i, items.len())).await?; -//! // … write to zip … -//! } -//! Ok(()) -//! } -//! } -//! ``` - -use anyhow::Result; - -/// Handle handed to a running job: reports progress and emits SSE events. -/// -/// Wraps the existing SSE broadcaster and an optional `export_job` row. Implementors -/// don't need to know about `state.sse_tx` directly — they call [`JobContext::report`] -/// and get the same effect. -pub struct JobContext { - pub job_id: Option, - pub event_kind: &'static str, - pub sse_tx: tokio::sync::broadcast::Sender, - pub pool: sqlx::PgPool, -} - -impl JobContext { - /// Update progress (0..=100) and broadcast an SSE tick. Cheap to call often — - /// rate-limit at the call site if a job emits at > 10 Hz. - pub async fn report(&self, percent: u8) -> Result<()> { - if let Some(job_id) = self.job_id { - sqlx::query("UPDATE export_job SET progress_pct = $1 WHERE id = $2") - .bind(percent as i16) - .bind(job_id) - .execute(&self.pool) - .await?; - } - let _ = self.sse_tx.send(crate::state::SseEvent::new( - self.event_kind, - serde_json::json!({ "progress_pct": percent }).to_string(), - )); - Ok(()) - } -} - -/// One unit of work that publishes progress through a [`JobContext`]. -/// -/// `run` consumes `self`; spawn with `tokio::spawn` at the caller. Errors propagate; -/// the caller is responsible for mapping them to `export_job.error_message` or -/// equivalent. Implementors stay small — the trait deliberately has no `cancel` -/// or `pause`; we have not needed those yet. -#[allow(async_fn_in_trait)] -pub trait BackgroundJob: Send + 'static { - fn name(&self) -> &'static str; - async fn run(self, ctx: JobContext) -> Result<()>; -} diff --git a/frontend/src/routes/feed/+page.svelte b/frontend/src/routes/feed/+page.svelte index 503b276..85774fc 100644 --- a/frontend/src/routes/feed/+page.svelte +++ b/frontend/src/routes/feed/+page.svelte @@ -214,6 +214,21 @@ onSseEvent('new-upload', (data) => { try { const upload: FeedUpload = JSON.parse(data); + // GRID view must NOT prepend live. Its rows are POSITIONAL windows + // (`uploads.slice(i * COLS, …)` in VirtualFeed), so inserting at the head shifts + // every tile by one slot: each row's keyed `{#each}` then sees a different set of + // ids and Svelte DESTROYS AND RECREATES the tile nodes. Two consequences at a + // party, where photos arrive continuously — a tap in flight is swallowed when its + // node is torn out, and the photo under the user's finger silently becomes a + // DIFFERENT photo, so they like or open one they never chose. + // + // The "neue Beiträge" pill already exists for exactly this: buffer, and let the + // user pull the new photos in when they are not mid-tap. List view is keyed by id + // at the top level and anchored, so its nodes survive a prepend — it stays live. + if (viewMode === 'grid') { + feedStale = true; + return; + } uploads = [upload, ...uploads]; } catch { /* ignore */ } }), diff --git a/frontend/src/routes/host/+page.svelte b/frontend/src/routes/host/+page.svelte index ff9d9b6..4d2fa7a 100644 --- a/frontend/src/routes/host/+page.svelte +++ b/frontend/src/routes/host/+page.svelte @@ -615,39 +615,56 @@ {:else if exportReady}

Keepsake ist bereit. - - - - Zum Download - + Zum Download

{:else}

Keepsake-Erstellung fehlgeschlagen.

- {/if} + + + {/if}