Slot-håndtering i maskinrommet (oppgave 14.5)
Ny intention `POST /intentions/set_slot` for redaksjonell kontroll over forside-slots i publiseringssamlinger. Håndhever: - Maks 1 hero: gammel ikke-pinned hero flyttes til strøm - featured_max: eldste ikke-pinned featured FIFO til strøm - pinned-flagg beskytter mot automatisk fjerning - Krever owner/admin-tilgang til samlingen - Trigger forside-rerendering etter slot-endring Returnerer liste over displaced edges slik at frontend kan vise hva som ble flyttet. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6e6e4912c1
commit
141cac9292
5 changed files with 324 additions and 21 deletions
|
|
@ -2,19 +2,36 @@
|
||||||
|
|
||||||
## Kjerneflyt
|
## Kjerneflyt
|
||||||
|
|
||||||
En node reiser fra privat til publisert uten kopiering eller konvertering.
|
En artikkel reiser fra privat til publisert via edges. Kildematerialet —
|
||||||
Den får bare nye edges til samlinger med stadig rikere traits.
|
chatmeldinger, notater, transkripsjoner — er egne noder koblet med
|
||||||
|
`source_material`-edges.
|
||||||
|
|
||||||
|
### To veier til en artikkel
|
||||||
|
|
||||||
|
**1. Artikkel fra start.** Bruker åpner artikkelverktøyet på
|
||||||
|
arbeidsflaten og skriver. Artikkelen er en innholdsnode fra fødsel.
|
||||||
|
|
||||||
```
|
```
|
||||||
1. Privat tanke Node uten edges. Bare din.
|
1. Privat artikkel Node med metadata.document. Bare din.
|
||||||
↓ (legg til belongs_to → kommunikasjonsnode)
|
↓ (legg til intended_for → samling)
|
||||||
2. Delt med venner Diskutert, kommentert, forbedret.
|
2. Under arbeid Tema-forhåndsvisning i editoren.
|
||||||
↓ (legg til belongs_to → bredere gruppe)
|
↓ (submitted_to erstatter intended_for)
|
||||||
3. Bredere gruppe Flere perspektiver, mer feedback.
|
3. Innsendt Redaktøren vurderer.
|
||||||
↓ (legg til belongs_to → samling med publishing-trait)
|
↓ (belongs_to erstatter submitted_to)
|
||||||
4. Publisert Maskinrommet rendrer HTML, genererer URL.
|
4. Publisert Maskinrommet rendrer HTML, genererer URL.
|
||||||
↓
|
```
|
||||||
5. Verden leser synops.no/pub/slug/id — Caddy → maskinrommet → CAS.
|
|
||||||
|
**2. Artikkel fra kildemateriale.** Bruker drar chatbobler, notater eller
|
||||||
|
transkripsjoner til artikkelverktøyet. Ny artikkelnode opprettes med
|
||||||
|
`source_material`-edges tilbake til kildene. Artikkelen følger deretter
|
||||||
|
publiseringsreisen over. Se [arbeidsflaten](../retninger/arbeidsflaten.md).
|
||||||
|
|
||||||
|
```
|
||||||
|
Chatboble ←source_material── Artikkel-node
|
||||||
|
Notat ←source_material── ↑
|
||||||
|
Transkripsjon ←source_material── │
|
||||||
|
↓
|
||||||
|
intended_for → submitted_to → belongs_to → publisert
|
||||||
```
|
```
|
||||||
|
|
||||||
Tilbaketrekking: fjern `belongs_to`-edgen → artikkelen forsvinner fra
|
Tilbaketrekking: fjern `belongs_to`-edgen → artikkelen forsvinner fra
|
||||||
|
|
@ -72,8 +89,9 @@ Ole er medlem (member-edge til samlingen)
|
||||||
8. Maskinrommet rendrer → HTML til CAS ved publish_at
|
8. Maskinrommet rendrer → HTML til CAS ved publish_at
|
||||||
```
|
```
|
||||||
|
|
||||||
Artikkelen er alltid én node. Den kopieres aldri. Den reiser gjennom
|
Artikkelen er alltid én node. Den reiser gjennom systemet ved at edges
|
||||||
systemet ved at edges endres.
|
endres. Materialet den bygger på er andre noder, sporbare gjennom grafen
|
||||||
|
via `source_material`-edges.
|
||||||
|
|
||||||
## Innsending: `submitted_to`-edge
|
## Innsending: `submitted_to`-edge
|
||||||
|
|
||||||
|
|
@ -517,13 +535,15 @@ Tre plasser med økende automatisering:
|
||||||
sortert på dato. Ingen hero, ingen featured — forsiden er en ren
|
sortert på dato. Ingen hero, ingen featured — forsiden er en ren
|
||||||
kronologisk flyt som fungerer fra dag én uten redaksjonelt arbeid.
|
kronologisk flyt som fungerer fra dag én uten redaksjonelt arbeid.
|
||||||
|
|
||||||
**Når redaktøren griper inn:**
|
**Når redaktøren griper inn** (via `POST /intentions/set_slot`):
|
||||||
- Sett hero → maskinrommet setter `slot: "hero"`. Forrige hero flyttes
|
- Sett hero → maskinrommet setter `slot: "hero"`. Forrige hero flyttes
|
||||||
automatisk tilbake til strøm.
|
automatisk tilbake til strøm (med mindre den er pinned).
|
||||||
- Sett featured → `slot: "featured"`, `slot_order` bestemmer rekkefølge.
|
- Sett featured → `slot: "featured"`, `slot_order` bestemmer rekkefølge.
|
||||||
Overstiger antallet `featured_max` → eldste featured faller til strøm
|
Overstiger antallet `featured_max` → eldste ikke-pinned featured faller
|
||||||
(FIFO).
|
til strøm (FIFO).
|
||||||
- Pin → artikkel blir stående i slot uavhengig av alder.
|
- Pin → `pinned: true` i edge-metadata. Artikkel blir stående i slot
|
||||||
|
uavhengig av alder og automatisk fjerning.
|
||||||
|
- Krever owner/admin-tilgang til samlingen.
|
||||||
|
|
||||||
### Forside-administrasjon i frontend
|
### Forside-administrasjon i frontend
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ valideres i maskinrommet.
|
||||||
| `admin` | Administrerer noden | — |
|
| `admin` | Administrerer noden | — |
|
||||||
| `member_of` | Er medlem av | — |
|
| `member_of` | Er medlem av | — |
|
||||||
| `reader` | Kan kun lese | — |
|
| `reader` | Kan kun lese | — |
|
||||||
| `belongs_to` | Tilhører (innhold → samling) | — |
|
| `belongs_to` | Tilhører (innhold → samling) | `{ "slot": "hero"/"featured"/null, "slot_order": 1, "pinned": true, "publish_at": "..." }` |
|
||||||
| `alias` | Identitet (systemedge) | `system: true` |
|
| `alias` | Identitet (systemedge) | `system: true` |
|
||||||
| `mentions` | Refererer til | — |
|
| `mentions` | Refererer til | — |
|
||||||
| `reply_to` | Svar på | — |
|
| `reply_to` | Svar på | — |
|
||||||
|
|
@ -73,6 +73,9 @@ valideres i maskinrommet.
|
||||||
| `og_image` | Forsidebilde / OpenGraph-bilde (media → innhold) | `{ "variant": "editorial" }` |
|
| `og_image` | Forsidebilde / OpenGraph-bilde (media → innhold) | `{ "variant": "editorial" }` |
|
||||||
| `show_notes` | Show notes for episode | `{ "variant": "ai" }` |
|
| `show_notes` | Show notes for episode | `{ "variant": "ai" }` |
|
||||||
| `chapter` | Kapittelmarkør for episode | `{ "at": "00:05:23" }` |
|
| `chapter` | Kapittelmarkør for episode | `{ "at": "00:05:23" }` |
|
||||||
|
| `source_material` | Kildemateriale (avledet node → kilde) | `{ "context": "quoted", "excerpt": "..." }` |
|
||||||
|
| `derived_from` | Prosessert versjon av (f.eks. lydstudio-output → original) | — |
|
||||||
|
| `has_studio` | Studio-sesjon (sesjon → medienode) | — |
|
||||||
|
|
||||||
Listen vokser organisk. Nye typer legges til ved behov uten
|
Listen vokser organisk. Nye typer legges til ved behov uten
|
||||||
skjemaendring.
|
skjemaendring.
|
||||||
|
|
|
||||||
|
|
@ -948,6 +948,286 @@ pub async fn update_edge(
|
||||||
Ok(Json(UpdateEdgeResponse { edge_id: req.edge_id }))
|
Ok(Json(UpdateEdgeResponse { edge_id: req.edge_id }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// set_slot — Redaksjonell slot-håndtering for publiseringssamlinger
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct SetSlotRequest {
|
||||||
|
/// ID til belongs_to-edgen mellom artikkel og samling.
|
||||||
|
pub edge_id: Uuid,
|
||||||
|
/// Slot: "hero", "featured", eller null/tom for strøm.
|
||||||
|
pub slot: Option<String>,
|
||||||
|
/// Rekkefølge innen featured-slot (ignoreres for hero/strøm).
|
||||||
|
pub slot_order: Option<i64>,
|
||||||
|
/// Forhindrer automatisk fjerning fra slot (FIFO/hero-erstatning).
|
||||||
|
pub pinned: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct SetSlotResponse {
|
||||||
|
/// Edgen som ble oppdatert.
|
||||||
|
pub edge_id: Uuid,
|
||||||
|
/// Edges som ble flyttet til strøm pga. hero-erstatning eller featured-overflow.
|
||||||
|
pub displaced: Vec<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// POST /intentions/set_slot
|
||||||
|
///
|
||||||
|
/// Setter slot-metadata på en belongs_to-edge i en publiseringssamling.
|
||||||
|
/// Håndhever:
|
||||||
|
/// - Maks 1 hero: gammel (ikke-pinned) hero flyttes til strøm.
|
||||||
|
/// - featured_max: eldste (ikke-pinned) featured flyttes til strøm (FIFO).
|
||||||
|
/// - pinned-flagg beskytter mot automatisk fjerning.
|
||||||
|
///
|
||||||
|
/// Krever owner/admin-tilgang til samlingen.
|
||||||
|
pub async fn set_slot(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
user: AuthUser,
|
||||||
|
Json(req): Json<SetSlotRequest>,
|
||||||
|
) -> Result<Json<SetSlotResponse>, (StatusCode, Json<ErrorResponse>)> {
|
||||||
|
// Valider slot-verdi
|
||||||
|
let slot = req.slot.as_deref().unwrap_or("");
|
||||||
|
if !matches!(slot, "" | "hero" | "featured") {
|
||||||
|
return Err(bad_request("slot må være \"hero\", \"featured\", eller null/tom for strøm"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hent edgen og valider at det er en belongs_to-edge
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
struct FullEdgeRow {
|
||||||
|
source_id: Uuid,
|
||||||
|
target_id: Uuid,
|
||||||
|
edge_type: String,
|
||||||
|
metadata: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
let edge = sqlx::query_as::<_, FullEdgeRow>(
|
||||||
|
"SELECT source_id, target_id, edge_type, metadata FROM edges WHERE id = $1",
|
||||||
|
)
|
||||||
|
.bind(req.edge_id)
|
||||||
|
.fetch_optional(&state.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("PG-feil ved henting av edge: {e}");
|
||||||
|
internal_error("Databasefeil ved henting av edge")
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| bad_request(&format!("Edge {} finnes ikke", req.edge_id)))?;
|
||||||
|
|
||||||
|
if edge.edge_type != "belongs_to" {
|
||||||
|
return Err(bad_request("set_slot kan kun brukes på belongs_to-edges"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let collection_id = edge.target_id;
|
||||||
|
|
||||||
|
// Sjekk at target er en publiseringssamling
|
||||||
|
let pub_config = crate::publishing::find_publishing_collection_by_id(&state.db, collection_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("PG-feil ved sjekk av publiseringssamling: {e}");
|
||||||
|
internal_error("Databasefeil")
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| bad_request("Samlingen har ikke publishing-trait"))?;
|
||||||
|
|
||||||
|
// Sjekk at brukeren er owner/admin på samlingen
|
||||||
|
let is_admin = sqlx::query_scalar::<_, bool>(
|
||||||
|
r#"
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1 FROM edges
|
||||||
|
WHERE source_id = $1 AND target_id = $2
|
||||||
|
AND edge_type IN ('owner', 'admin')
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user.node_id)
|
||||||
|
.bind(collection_id)
|
||||||
|
.fetch_one(&state.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("PG-feil ved tilgangssjekk: {e}");
|
||||||
|
internal_error("Databasefeil ved tilgangssjekk")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !is_admin {
|
||||||
|
return Err(forbidden("Kun owner/admin kan endre slots"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let featured_max = pub_config.featured_max.unwrap_or(4);
|
||||||
|
let mut displaced: Vec<Uuid> = Vec::new();
|
||||||
|
|
||||||
|
// -- Slot-logikk: håndter hero-erstatning og featured-overflow --
|
||||||
|
|
||||||
|
if slot == "hero" {
|
||||||
|
// Finn eksisterende hero-edges (som ikke er denne edgen)
|
||||||
|
let existing_heroes: Vec<(Uuid, serde_json::Value)> = sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
SELECT id, metadata FROM edges
|
||||||
|
WHERE target_id = $1
|
||||||
|
AND edge_type = 'belongs_to'
|
||||||
|
AND metadata->>'slot' = 'hero'
|
||||||
|
AND id != $2
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(collection_id)
|
||||||
|
.bind(req.edge_id)
|
||||||
|
.fetch_all(&state.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("PG-feil ved hero-sjekk: {e}");
|
||||||
|
internal_error("Databasefeil")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Flytt ikke-pinned heroes tilbake til strøm
|
||||||
|
for (hero_edge_id, hero_meta) in &existing_heroes {
|
||||||
|
let pinned = hero_meta.get("pinned").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||||
|
if !pinned {
|
||||||
|
displace_to_stream(&state.db, &state.stdb, *hero_edge_id, hero_meta).await?;
|
||||||
|
displaced.push(*hero_edge_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if slot == "featured" {
|
||||||
|
// Tell nåværende featured-edges (ekskluder denne edgen om den allerede er featured)
|
||||||
|
let current_featured: Vec<(Uuid, serde_json::Value, Option<i64>)> = sqlx::query_as(
|
||||||
|
r#"
|
||||||
|
SELECT id, metadata,
|
||||||
|
(metadata->>'slot_order')::bigint as slot_order
|
||||||
|
FROM edges
|
||||||
|
WHERE target_id = $1
|
||||||
|
AND edge_type = 'belongs_to'
|
||||||
|
AND metadata->>'slot' = 'featured'
|
||||||
|
AND id != $2
|
||||||
|
ORDER BY (metadata->>'slot_order')::int ASC NULLS LAST,
|
||||||
|
created_at ASC
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(collection_id)
|
||||||
|
.bind(req.edge_id)
|
||||||
|
.fetch_all(&state.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("PG-feil ved featured-sjekk: {e}");
|
||||||
|
internal_error("Databasefeil")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Hvis vi legger til en ny featured og det overstiger featured_max,
|
||||||
|
// fjern eldste (FIFO) ikke-pinned featured
|
||||||
|
let new_count = current_featured.len() as i64 + 1; // +1 for den vi setter nå
|
||||||
|
if new_count > featured_max {
|
||||||
|
let overflow = (new_count - featured_max) as usize;
|
||||||
|
// Finn de eldste ikke-pinned featured-edges å fjerne (FIFO = sist i listen)
|
||||||
|
let removable: Vec<&(Uuid, serde_json::Value, Option<i64>)> = current_featured
|
||||||
|
.iter()
|
||||||
|
.rev() // Høyest slot_order / eldst først for FIFO
|
||||||
|
.filter(|(_, meta, _)| {
|
||||||
|
!meta.get("pinned").and_then(|v| v.as_bool()).unwrap_or(false)
|
||||||
|
})
|
||||||
|
.take(overflow)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for (feat_edge_id, feat_meta, _) in removable {
|
||||||
|
displace_to_stream(&state.db, &state.stdb, *feat_edge_id, feat_meta).await?;
|
||||||
|
displaced.push(*feat_edge_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Oppdater edgen med ny slot-metadata --
|
||||||
|
let mut new_meta = edge.metadata.clone();
|
||||||
|
if let Some(obj) = new_meta.as_object_mut() {
|
||||||
|
if slot.is_empty() {
|
||||||
|
obj.remove("slot");
|
||||||
|
obj.remove("slot_order");
|
||||||
|
} else {
|
||||||
|
obj.insert("slot".to_string(), serde_json::json!(slot));
|
||||||
|
if slot == "featured" {
|
||||||
|
if let Some(order) = req.slot_order {
|
||||||
|
obj.insert("slot_order".to_string(), serde_json::json!(order));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obj.remove("slot_order");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sett/fjern pinned
|
||||||
|
match req.pinned {
|
||||||
|
Some(true) => { obj.insert("pinned".to_string(), serde_json::json!(true)); }
|
||||||
|
Some(false) => { obj.remove("pinned"); }
|
||||||
|
None => {} // Behold eksisterende
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skriv til STDB (instant)
|
||||||
|
let edge_id_str = req.edge_id.to_string();
|
||||||
|
let meta_str = new_meta.to_string();
|
||||||
|
state
|
||||||
|
.stdb
|
||||||
|
.update_edge(&edge_id_str, "belongs_to", &meta_str)
|
||||||
|
.await
|
||||||
|
.map_err(|e| stdb_error("update_edge", e))?;
|
||||||
|
|
||||||
|
// Skriv til PG
|
||||||
|
sqlx::query("UPDATE edges SET metadata = $1 WHERE id = $2")
|
||||||
|
.bind(&new_meta)
|
||||||
|
.bind(req.edge_id)
|
||||||
|
.execute(&state.db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("PG-feil ved slot-oppdatering: {e}");
|
||||||
|
internal_error("Databasefeil ved slot-oppdatering")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
edge_id = %req.edge_id,
|
||||||
|
slot = %slot,
|
||||||
|
displaced_count = displaced.len(),
|
||||||
|
"Slot oppdatert"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Trigger forside-rerendering
|
||||||
|
trigger_render_if_publishing(&state.db, &state.index_cache, edge.source_id, collection_id).await;
|
||||||
|
|
||||||
|
Ok(Json(SetSlotResponse {
|
||||||
|
edge_id: req.edge_id,
|
||||||
|
displaced,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flytter en edge fra sin nåværende slot tilbake til strøm (slot=null).
|
||||||
|
/// Beholder annen metadata (publish_at, approved_by, etc.).
|
||||||
|
async fn displace_to_stream(
|
||||||
|
db: &PgPool,
|
||||||
|
stdb: &crate::stdb::StdbClient,
|
||||||
|
edge_id: Uuid,
|
||||||
|
current_meta: &serde_json::Value,
|
||||||
|
) -> Result<(), (StatusCode, Json<ErrorResponse>)> {
|
||||||
|
let mut meta = current_meta.clone();
|
||||||
|
if let Some(obj) = meta.as_object_mut() {
|
||||||
|
obj.remove("slot");
|
||||||
|
obj.remove("slot_order");
|
||||||
|
// pinned er irrelevant i strøm, fjern det
|
||||||
|
obj.remove("pinned");
|
||||||
|
}
|
||||||
|
|
||||||
|
// STDB (instant)
|
||||||
|
let edge_id_str = edge_id.to_string();
|
||||||
|
let meta_str = meta.to_string();
|
||||||
|
stdb.update_edge(&edge_id_str, "belongs_to", &meta_str)
|
||||||
|
.await
|
||||||
|
.map_err(|e| stdb_error("update_edge (displace)", e))?;
|
||||||
|
|
||||||
|
// PG
|
||||||
|
sqlx::query("UPDATE edges SET metadata = $1 WHERE id = $2")
|
||||||
|
.bind(&meta)
|
||||||
|
.bind(edge_id)
|
||||||
|
.execute(db)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("PG-feil ved displace_to_stream: {e}");
|
||||||
|
internal_error("Databasefeil ved fjerning fra slot")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
tracing::info!(edge_id = %edge_id, "Edge fjernet fra slot → strøm");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// create_communication
|
// create_communication
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,7 @@ async fn main() {
|
||||||
.route("/intentions/update_node", post(intentions::update_node))
|
.route("/intentions/update_node", post(intentions::update_node))
|
||||||
.route("/intentions/delete_node", post(intentions::delete_node))
|
.route("/intentions/delete_node", post(intentions::delete_node))
|
||||||
.route("/intentions/update_edge", post(intentions::update_edge))
|
.route("/intentions/update_edge", post(intentions::update_edge))
|
||||||
|
.route("/intentions/set_slot", post(intentions::set_slot))
|
||||||
.route("/intentions/create_communication", post(intentions::create_communication))
|
.route("/intentions/create_communication", post(intentions::create_communication))
|
||||||
.route("/intentions/upload_media", post(intentions::upload_media))
|
.route("/intentions/upload_media", post(intentions::upload_media))
|
||||||
.route("/cas/{hash}", get(serving::get_cas_file))
|
.route("/cas/{hash}", get(serving::get_cas_file))
|
||||||
|
|
|
||||||
3
tasks.md
3
tasks.md
|
|
@ -143,8 +143,7 @@ Uavhengige faser kan fortsatt plukkes.
|
||||||
- [x] 14.2 HTML-rendering av enkeltartikler: maskinrommet rendrer `metadata.document` til HTML via Tera, lagrer i CAS. Noden får `metadata.rendered.html_hash` + `renderer_version`. SEO-metadata (OG-tags, canonical, JSON-LD).
|
- [x] 14.2 HTML-rendering av enkeltartikler: maskinrommet rendrer `metadata.document` til HTML via Tera, lagrer i CAS. Noden får `metadata.rendered.html_hash` + `renderer_version`. SEO-metadata (OG-tags, canonical, JSON-LD).
|
||||||
- [x] 14.3 Forside-rendering: maskinrommet spør PG for hero/featured/strøm (tre indekserte spørringer), appliserer tema-template, rendrer til CAS (statisk modus) eller serverer med in-memory cache (dynamisk modus). `index_mode` og `index_cache_ttl` i trait-konfig.
|
- [x] 14.3 Forside-rendering: maskinrommet spør PG for hero/featured/strøm (tre indekserte spørringer), appliserer tema-template, rendrer til CAS (statisk modus) eller serverer med in-memory cache (dynamisk modus). `index_mode` og `index_cache_ttl` i trait-konfig.
|
||||||
- [x] 14.4 Caddy-ruting for synops.no/pub: Caddy reverse-proxyer til maskinrommet som gjør slug→hash-oppslag og streamer CAS-fil. `Cache-Control: immutable` for artikler. Kategori/arkiv/søk serveres dynamisk av maskinrommet med kortere cache-TTL.
|
- [x] 14.4 Caddy-ruting for synops.no/pub: Caddy reverse-proxyer til maskinrommet som gjør slug→hash-oppslag og streamer CAS-fil. `Cache-Control: immutable` for artikler. Kategori/arkiv/søk serveres dynamisk av maskinrommet med kortere cache-TTL.
|
||||||
- [~] 14.5 Slot-håndtering i maskinrommet: `slot` og `slot_order` i `belongs_to`-edge metadata. Ved ny hero → gammel hero flyttes til strøm. Ved featured over `featured_max` → FIFO tilbake til strøm. `pinned`-flagg forhindrer automatisk fjerning.
|
- [x] 14.5 Slot-håndtering i maskinrommet: `slot` og `slot_order` i `belongs_to`-edge metadata. Ved ny hero → gammel hero flyttes til strøm. Ved featured over `featured_max` → FIFO tilbake til strøm. `pinned`-flagg forhindrer automatisk fjerning.
|
||||||
> Påbegynt: 2026-03-18T01:10
|
|
||||||
- [ ] 14.6 Forside-admin i frontend: visuell editor for hero/featured/strøm. Drag-and-drop mellom plasser. Pin-knapp. Forhåndsvisning. Oppdaterer edge-metadata via maskinrommet.
|
- [ ] 14.6 Forside-admin i frontend: visuell editor for hero/featured/strøm. Drag-and-drop mellom plasser. Pin-knapp. Forhåndsvisning. Oppdaterer edge-metadata via maskinrommet.
|
||||||
- [ ] 14.7 Publiseringsflyt i frontend (personlig): publiseringsknapp på noder i samlinger med `publishing`-trait der `require_approval: false`. Forhåndsvisning, slug-editor, bekreftelse. Avpublisering ved fjerning av edge.
|
- [ ] 14.7 Publiseringsflyt i frontend (personlig): publiseringsknapp på noder i samlinger med `publishing`-trait der `require_approval: false`. Forhåndsvisning, slug-editor, bekreftelse. Avpublisering ved fjerning av edge.
|
||||||
- [ ] 14.8 RSS/Atom-feed: samling med `rss`-trait genererer feed automatisk ved publisering/avpublisering. `synops.no/pub/{slug}/feed.xml`. Maks `rss_max_items` (default 50).
|
- [ ] 14.8 RSS/Atom-feed: samling med `rss`-trait genererer feed automatisk ved publisering/avpublisering. `synops.no/pub/{slug}/feed.xml`. Maks `rss_max_items` (default 50).
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue