From 7189925d082c494a96eceb276a8ea824fad39f30 Mon Sep 17 00:00:00 2001 From: vegard Date: Tue, 17 Mar 2026 15:44:02 +0100 Subject: [PATCH] =?UTF-8?q?Fullf=C3=B8r=20oppgave=205.1:=20create=5Fcommun?= =?UTF-8?q?ication-intensjon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ny intensjon POST /intentions/create_communication som oppretter en kommunikasjonsnode (node_kind='communication') med: - metadata.started_at satt til opprettelsestidspunkt - owner-edge fra innlogget bruker til noden - member_of-edges for alle angitte deltakere - Validering av deltaker-noder og visibility - Samme to-lags skriveflyt som andre intensjoner (STDB instant, PG async) Testet med curl mot produksjonsserver — node, edges og node_access opprettes korrekt i både STDB og PG. Co-Authored-By: Claude Opus 4.6 --- maskinrommet/src/intentions.rs | 189 +++++++++++++++++++++++++++++++++ maskinrommet/src/main.rs | 1 + tasks.md | 3 +- 3 files changed, 191 insertions(+), 2 deletions(-) diff --git a/maskinrommet/src/intentions.rs b/maskinrommet/src/intentions.rs index ad60ceb..123792f 100644 --- a/maskinrommet/src/intentions.rs +++ b/maskinrommet/src/intentions.rs @@ -503,6 +503,195 @@ pub async fn delete_node( Ok(Json(DeleteNodeResponse { deleted: true })) } +// ============================================================================= +// create_communication +// ============================================================================= + +#[derive(Deserialize)] +pub struct CreateCommunicationRequest { + /// Visningstittel for kommunikasjonsnoden (f.eks. "Redaksjonsmøte"). + pub title: Option, + /// Deltakere — liste med node_id-er (person-noder). + /// Innlogget bruker legges automatisk til som owner. + pub participants: Vec, + /// Synlighet. Default: "hidden" (privat). + pub visibility: Option, +} + +#[derive(Serialize)] +pub struct CreateCommunicationResponse { + pub node_id: Uuid, + /// Edge-IDer for opprettede deltaker-edges (owner + member_of). + pub edge_ids: Vec, +} + +/// POST /intentions/create_communication +/// +/// Oppretter en kommunikasjonsnode med deltaker-edges. +/// Innlogget bruker blir automatisk owner. Andre deltakere får member_of-edge. +/// Metadata inneholder started_at-tidsstempel. +/// +/// Ref: docs/primitiver/nodes.md (communication), docs/retninger/universell_input.md +pub async fn create_communication( + State(state): State, + user: AuthUser, + Json(req): Json, +) -> Result, (StatusCode, Json)> { + let visibility = req.visibility.unwrap_or_else(|| "hidden".to_string()); + if !VALID_VISIBILITIES.contains(&visibility.as_str()) { + return Err(bad_request(&format!( + "Ugyldig visibility: '{visibility}'. Gyldige verdier: {VALID_VISIBILITIES:?}" + ))); + } + + // Valider at alle deltakere eksisterer + for participant_id in &req.participants { + let exists = node_exists(&state.db, *participant_id) + .await + .map_err(|e| { + tracing::error!("PG-feil ved nodesjekk: {e}"); + internal_error("Databasefeil ved validering") + })?; + if !exists { + return Err(bad_request(&format!( + "Deltaker-node {} finnes ikke", + participant_id + ))); + } + } + + let title = req.title.unwrap_or_default(); + let now = chrono::Utc::now(); + let metadata = serde_json::json!({ "started_at": now.to_rfc3339() }); + let metadata_str = metadata.to_string(); + + // -- Opprett kommunikasjonsnoden -- + let node_id = Uuid::now_v7(); + let node_id_str = node_id.to_string(); + let created_by_str = user.node_id.to_string(); + + state + .stdb + .create_node( + &node_id_str, + "communication", + &title, + "", + &visibility, + &metadata_str, + &created_by_str, + ) + .await + .map_err(|e| stdb_error("create_node (communication)", e))?; + + tracing::info!( + node_id = %node_id, + created_by = %user.node_id, + participants = ?req.participants, + "Kommunikasjonsnode opprettet i STDB" + ); + + // Spawn PG-skriving for noden + spawn_pg_insert_node( + state.db.clone(), + node_id, + "communication".to_string(), + title, + String::new(), + visibility, + metadata, + user.node_id, + ); + + // -- Opprett deltaker-edges -- + let mut edge_ids = Vec::new(); + + // Owner-edge for innlogget bruker + let owner_edge_id = Uuid::now_v7(); + edge_ids.push(owner_edge_id); + + let owner_edge_id_str = owner_edge_id.to_string(); + let owner_metadata = serde_json::json!({}); + let owner_metadata_str = owner_metadata.to_string(); + + state + .stdb + .create_edge( + &owner_edge_id_str, + &created_by_str, + &node_id_str, + "owner", + &owner_metadata_str, + false, + &created_by_str, + ) + .await + .map_err(|e| stdb_error("create_edge (owner)", e))?; + + // Spawn PG-skriving for owner-edge (med access recompute) + spawn_pg_insert_edge( + state.db.clone(), + state.stdb.clone(), + owner_edge_id, + user.node_id, + node_id, + "owner".to_string(), + owner_metadata, + false, + user.node_id, + ); + + // member_of-edges for øvrige deltakere + for participant_id in &req.participants { + // Hopp over innlogget bruker — allerede owner + if *participant_id == user.node_id { + continue; + } + + let edge_id = Uuid::now_v7(); + edge_ids.push(edge_id); + + let edge_id_str = edge_id.to_string(); + let participant_id_str = participant_id.to_string(); + let member_metadata = serde_json::json!({}); + let member_metadata_str = member_metadata.to_string(); + + state + .stdb + .create_edge( + &edge_id_str, + &participant_id_str, + &node_id_str, + "member_of", + &member_metadata_str, + false, + &created_by_str, + ) + .await + .map_err(|e| stdb_error("create_edge (member_of)", e))?; + + spawn_pg_insert_edge( + state.db.clone(), + state.stdb.clone(), + edge_id, + *participant_id, + node_id, + "member_of".to_string(), + member_metadata, + false, + user.node_id, + ); + } + + tracing::info!( + node_id = %node_id, + edge_count = edge_ids.len(), + "Kommunikasjonsnode med deltaker-edges opprettet" + ); + + Ok(Json(CreateCommunicationResponse { node_id, edge_ids })) +} + // ============================================================================= // Bakgrunns-PG-operasjoner // ============================================================================= diff --git a/maskinrommet/src/main.rs b/maskinrommet/src/main.rs index 500c98a..eda0685 100644 --- a/maskinrommet/src/main.rs +++ b/maskinrommet/src/main.rs @@ -118,6 +118,7 @@ async fn main() { .route("/intentions/create_edge", post(intentions::create_edge)) .route("/intentions/update_node", post(intentions::update_node)) .route("/intentions/delete_node", post(intentions::delete_node)) + .route("/intentions/create_communication", post(intentions::create_communication)) .route("/query/nodes", get(queries::query_nodes)) .layer(TraceLayer::new_for_http()) .with_state(state); diff --git a/tasks.md b/tasks.md index 4165523..f761c92 100644 --- a/tasks.md +++ b/tasks.md @@ -78,8 +78,7 @@ Uavhengige faser kan fortsatt plukkes. ## Fase 5: Kommunikasjonsnoder -- [~] 5.1 Opprett kommunikasjonsnode: intensjon `create_communication` → node med `node_kind='communication'`, deltaker-edges, metadata (started_at). - > Påbegynt: 2026-03-17T15:35 +- [x] 5.1 Opprett kommunikasjonsnode: intensjon `create_communication` → node med `node_kind='communication'`, deltaker-edges, metadata (started_at). - [ ] 5.2 Kontekst-arv: input i kommunikasjonsnode → automatisk `belongs_to`-edge. - [ ] 5.3 Chat-visning i frontend: noder med `belongs_to`-edge til kommunikasjonsnode, sortert på tid, sanntid via STDB. - [ ] 5.4 Én-til-én chat: opprett kommunikasjonsnode med to deltakere. Full loop: skriv melding → vis i sanntid hos begge.