diff --git a/maskinrommet/src/intentions.rs b/maskinrommet/src/intentions.rs index ae70463..cfe2680 100644 --- a/maskinrommet/src/intentions.rs +++ b/maskinrommet/src/intentions.rs @@ -844,15 +844,25 @@ pub async fn update_node( let title = req.title.unwrap_or(existing.title.unwrap_or_default()); let content = req.content.unwrap_or(existing.content.unwrap_or_default()); - // Hent gammelt custom_domain før existing.metadata flyttes - let old_domain = existing.metadata + // Hent gamle publishing-verdier før existing.metadata flyttes + let old_publishing = existing.metadata .get("traits") - .and_then(|t| t.get("publishing")) + .and_then(|t| t.get("publishing")); + + let old_domain = old_publishing .and_then(|p| p.get("custom_domain")) .and_then(|d| d.as_str()) .unwrap_or("") .to_string(); + let old_theme = old_publishing + .and_then(|p| p.get("theme")) + .cloned(); + + let old_theme_config = old_publishing + .and_then(|p| p.get("theme_config")) + .cloned(); + let metadata = req.metadata.unwrap_or(existing.metadata); // -- Valider traits for samlingsnoder (oppgave 13.1) -- @@ -867,6 +877,20 @@ pub async fn update_node( .unwrap_or(""); let domain_changed = old_domain != new_domain && node_kind == "collection"; + // -- Sjekk om theme eller theme_config er endret (for bulk re-rendering, oppgave 14.14) -- + let theme_changed = if node_kind == "collection" { + let new_publishing = metadata + .get("traits") + .and_then(|t| t.get("publishing")); + + let new_theme = new_publishing.and_then(|p| p.get("theme")).cloned(); + let new_theme_config = new_publishing.and_then(|p| p.get("theme_config")).cloned(); + + old_theme != new_theme || old_theme_config != new_theme_config + } else { + false + }; + let metadata_str = metadata.to_string(); let node_id_str = req.node_id.to_string(); @@ -922,6 +946,26 @@ pub async fn update_node( }); } + // -- Bulk re-rendering hvis theme/theme_config endret (oppgave 14.14) -- + if theme_changed { + let db = state.db.clone(); + let collection_id = req.node_id; + tokio::spawn(async move { + match crate::publishing::trigger_bulk_rerender(&db, collection_id).await { + Ok(count) => tracing::info!( + collection_id = %collection_id, + articles = count, + "Bulk re-rendering trigget etter temaendring" + ), + Err(e) => tracing::error!( + collection_id = %collection_id, + error = %e, + "Feil ved bulk re-rendering etter temaendring" + ), + } + }); + } + Ok(Json(UpdateNodeResponse { node_id: req.node_id })) } diff --git a/maskinrommet/src/publishing.rs b/maskinrommet/src/publishing.rs index f301bda..44e312d 100644 --- a/maskinrommet/src/publishing.rs +++ b/maskinrommet/src/publishing.rs @@ -32,7 +32,7 @@ use crate::AppState; /// Renderer-versjon. Økes ved mal-/template-endringer. /// Brukes for å identifisere artikler som trenger re-rendering (oppgave 14.14). -pub const RENDERER_VERSION: i64 = 1; +pub const RENDERER_VERSION: i64 = 2; // ============================================================================= // Tema-konfigurasjon fra publishing-trait @@ -1411,6 +1411,110 @@ pub fn start_publish_scheduler(db: PgPool) { }); } +// ============================================================================= +// Bulk re-rendering ved temaendring (oppgave 14.14) +// ============================================================================= + +/// Paginert batch-jobb: finn artikler som trenger re-rendering og enqueue +/// render_article-jobber i grupper på 100. Artikler serveres med gammelt +/// tema til de er re-rendret — renderer_version i metadata identifiserer +/// hvilke som gjenstår. +/// +/// Kalles når theme eller theme_config endres på en samling. +pub async fn trigger_bulk_rerender( + db: &PgPool, + collection_id: Uuid, +) -> Result { + let batch_size: i64 = 100; + let mut total_enqueued: usize = 0; + + loop { + // Finn neste batch artikler som trenger re-rendering. + // Filtrerer ut artikler som allerede har pending/running render-jobb. + let article_ids: Vec<(Uuid,)> = sqlx::query_as( + r#" + SELECT e.source_id + FROM edges e + JOIN nodes n ON n.id = e.source_id + WHERE e.target_id = $1 + AND e.edge_type = 'belongs_to' + AND ( + n.metadata->'rendered'->>'renderer_version' IS NULL + OR (n.metadata->'rendered'->>'renderer_version')::bigint < $2 + ) + AND NOT EXISTS ( + SELECT 1 FROM job_queue jq + WHERE jq.job_type = 'render_article' + AND jq.status IN ('pending', 'running', 'retry') + AND jq.payload->>'node_id' = n.id::text + ) + LIMIT $3 + "#, + ) + .bind(collection_id) + .bind(RENDERER_VERSION) + .bind(batch_size) + .fetch_all(db) + .await + .map_err(|e| format!("Feil ved henting av artikler for bulk rerender: {e}"))?; + + if article_ids.is_empty() { + break; + } + + let batch_count = article_ids.len(); + + for (article_id,) in &article_ids { + let payload = serde_json::json!({ + "node_id": article_id.to_string(), + "collection_id": collection_id.to_string(), + }); + if let Err(e) = jobs::enqueue(db, "render_article", payload, Some(collection_id), 3).await { + tracing::error!( + article_id = %article_id, + collection_id = %collection_id, + error = %e, + "Kunne ikke enqueue render_article ved temaendring" + ); + } + } + + total_enqueued += batch_count; + + tracing::info!( + collection_id = %collection_id, + batch = batch_count, + total = total_enqueued, + "Bulk rerender batch enqueued" + ); + + // Hvis batchen var mindre enn batch_size, er vi ferdige + if (batch_count as i64) < batch_size { + break; + } + } + + // Enqueue render av forsiden til slutt (lavere prioritet) + let index_payload = serde_json::json!({ + "collection_id": collection_id.to_string(), + }); + if let Err(e) = jobs::enqueue(db, "render_index", index_payload, Some(collection_id), 4).await { + tracing::error!( + collection_id = %collection_id, + error = %e, + "Kunne ikke enqueue render_index ved temaendring" + ); + } + + tracing::info!( + collection_id = %collection_id, + total_articles = total_enqueued, + "Bulk re-rendering enqueued ved temaendring" + ); + + Ok(total_enqueued) +} + // ============================================================================= // Tester // ============================================================================= diff --git a/tasks.md b/tasks.md index 81b8e19..1c1ba61 100644 --- a/tasks.md +++ b/tasks.md @@ -156,8 +156,7 @@ Uavhengige faser kan fortsatt plukkes. - [x] 14.11 Redaktørens arbeidsflate: frontend-visning av noder med `submitted_to`-edge til samling, gruppert på status. Kanban-stil drag-and-drop for statusendring. Siste kolonne ("Planlagt") setter `publish_at` i edge-metadata. - [x] 14.12 Planlagt publisering: maskinrommet sjekker periodisk (cron/intervall) for `belongs_to`-edges med `publish_at` i fortiden som ikke er rendret. Ved treff: render HTML → CAS → oppdater RSS. - [x] 14.13 Redaksjonell samtale: ved innsending kan redaktør opprette kommunikasjonsnode knyttet til artikkel + forfatter for diskusjon/feedback utover kort notat i edge-metadata. -- [~] 14.14 Bulk re-rendering: batch-jobb via jobbkø ved temaendring. Paginert (100 artikler om gangen), oppdaterer `renderer_version`. Artikler serveres med gammelt tema til re-rendret. - > Påbegynt: 2026-03-18T02:23 +- [x] 14.14 Bulk re-rendering: batch-jobb via jobbkø ved temaendring. Paginert (100 artikler om gangen), oppdaterer `renderer_version`. Artikler serveres med gammelt tema til re-rendret. - [ ] 14.15 Dynamiske sider: kategori-sider (filtrert på tag-edges), arkiv (kronologisk med månedsgruppering), søk (PG fulltekst). Alle paginerte, cachet i maskinrommet. Om-side som statisk CAS-node. - [ ] 14.16 Presentasjonselementer som noder: publisert tittel, ingress, OG-bilde, undertittel er egne noder med `title`/`summary`/`og_image`-edges til artikkelen. Frontend for å opprette/redigere varianter. Ref: `docs/concepts/publisering.md` § "Presentasjonselementer". - [ ] 14.17 A/B-testing: maskinrommet roterer varianter ved forside-rendering, logger impressions/klikk per variant, normaliserer CTR mot tidspunkt-baseline. Etter statistisk signifikans markeres vinner. Redaktør kan overstyre. Edge-metadata: `ab_status`, `impressions`, `clicks`, `ctr`.