Bulk re-rendering ved temaendring (oppgave 14.14): paginert batch-jobb via jobbkø

Når theme eller theme_config endres på en samling, trigges paginert
bulk re-rendering av alle artikler (100 om gangen). Artikler serveres
med gammelt tema til de er re-rendret — renderer_version identifiserer
hvilke som gjenstår. Duplikatsjekk mot eksisterende pending/running jobber.

- publishing.rs: trigger_bulk_rerender() med paginert SQL-query
- intentions.rs: theme/theme_config endring detekteres i update_node
- RENDERER_VERSION bumped til 2

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
vegard 2026-03-18 02:28:07 +00:00
parent 8155783fc4
commit cf38721459
3 changed files with 153 additions and 6 deletions

View file

@ -844,15 +844,25 @@ pub async fn update_node(
let title = req.title.unwrap_or(existing.title.unwrap_or_default()); let title = req.title.unwrap_or(existing.title.unwrap_or_default());
let content = req.content.unwrap_or(existing.content.unwrap_or_default()); let content = req.content.unwrap_or(existing.content.unwrap_or_default());
// Hent gammelt custom_domain før existing.metadata flyttes // Hent gamle publishing-verdier før existing.metadata flyttes
let old_domain = existing.metadata let old_publishing = existing.metadata
.get("traits") .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(|p| p.get("custom_domain"))
.and_then(|d| d.as_str()) .and_then(|d| d.as_str())
.unwrap_or("") .unwrap_or("")
.to_string(); .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); let metadata = req.metadata.unwrap_or(existing.metadata);
// -- Valider traits for samlingsnoder (oppgave 13.1) -- // -- Valider traits for samlingsnoder (oppgave 13.1) --
@ -867,6 +877,20 @@ pub async fn update_node(
.unwrap_or(""); .unwrap_or("");
let domain_changed = old_domain != new_domain && node_kind == "collection"; 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 metadata_str = metadata.to_string();
let node_id_str = req.node_id.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 })) Ok(Json(UpdateNodeResponse { node_id: req.node_id }))
} }

View file

@ -32,7 +32,7 @@ use crate::AppState;
/// Renderer-versjon. Økes ved mal-/template-endringer. /// Renderer-versjon. Økes ved mal-/template-endringer.
/// Brukes for å identifisere artikler som trenger re-rendering (oppgave 14.14). /// 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 // 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<usize, String> {
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 // Tester
// ============================================================================= // =============================================================================

View file

@ -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.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.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. - [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. - [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.
> Påbegynt: 2026-03-18T02:23
- [ ] 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.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.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`. - [ ] 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`.