Implementer message_placements (oppgave 20.1)
Plasseringsrelasjon som sporer hvor meldinger vises på tvers av kontekster (chat, kanban, storyboard, kalender, notes). Grunnmuren for universell overføring mellom verktøy-paneler. Tre deler: - PG-migrasjon 016: message_placements tabell med UNIQUE constraint og indekser for kontekst- og meldingsoppslag - SpacetimeDB: MessagePlacement tabell + place_message, remove_placement, move_on_canvas reducers for sanntids UI-oppdatering - Maskinrommet: STDB-klientmetoder for de tre reducerne Avvik fra spec: FK refererer nodes(id) i stedet for messages(id) siden meldinger er noder (node_kind = 'melding'). Spec oppdatert tilsvarende. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aa42b0e257
commit
8bf82a78d9
5 changed files with 212 additions and 4 deletions
|
|
@ -43,7 +43,7 @@ Når en melding vises i en kontekst (chat, kanban, storyboard, kalender), trenge
|
||||||
```sql
|
```sql
|
||||||
CREATE TABLE message_placements (
|
CREATE TABLE message_placements (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
message_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
||||||
context_type TEXT NOT NULL, -- 'chat', 'kanban', 'storyboard', 'calendar', 'notes'
|
context_type TEXT NOT NULL, -- 'chat', 'kanban', 'storyboard', 'calendar', 'notes'
|
||||||
context_id UUID NOT NULL, -- channel_id, board_id, episode_id, calendar_id, note_id
|
context_id UUID NOT NULL, -- channel_id, board_id, episode_id, calendar_id, note_id
|
||||||
entered_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- når objektet ankom denne konteksten
|
entered_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- når objektet ankom denne konteksten
|
||||||
|
|
|
||||||
|
|
@ -346,6 +346,75 @@ impl StdbClient {
|
||||||
self.call_reducer("close_live_room", &Args { room_id }).await
|
self.call_reducer("close_live_room", &Args { room_id }).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Placement-operasjoner (message_placements)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Plasser en melding i en kontekst. Idempotent (upsert).
|
||||||
|
pub async fn place_message(
|
||||||
|
&self,
|
||||||
|
id: &str,
|
||||||
|
message_id: &str,
|
||||||
|
context_type: &str,
|
||||||
|
context_id: &str,
|
||||||
|
position_json: &str,
|
||||||
|
) -> Result<(), StdbError> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Args<'a> {
|
||||||
|
id: &'a str,
|
||||||
|
message_id: &'a str,
|
||||||
|
context_type: &'a str,
|
||||||
|
context_id: &'a str,
|
||||||
|
position_json: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.call_reducer(
|
||||||
|
"place_message",
|
||||||
|
&Args { id, message_id, context_type, context_id, position_json },
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fjern en meldings plassering fra en kontekst.
|
||||||
|
pub async fn remove_placement(
|
||||||
|
&self,
|
||||||
|
message_id: &str,
|
||||||
|
context_type: &str,
|
||||||
|
context_id: &str,
|
||||||
|
) -> Result<(), StdbError> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Args<'a> {
|
||||||
|
message_id: &'a str,
|
||||||
|
context_type: &'a str,
|
||||||
|
context_id: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.call_reducer(
|
||||||
|
"remove_placement",
|
||||||
|
&Args { message_id, context_type, context_id },
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flytt en plassering (oppdater posisjon).
|
||||||
|
pub async fn move_on_canvas(
|
||||||
|
&self,
|
||||||
|
placement_id: &str,
|
||||||
|
new_position_json: &str,
|
||||||
|
) -> Result<(), StdbError> {
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Args<'a> {
|
||||||
|
placement_id: &'a str,
|
||||||
|
new_position_json: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.call_reducer(
|
||||||
|
"move_on_canvas",
|
||||||
|
&Args { placement_id, new_position_json },
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Vedlikehold
|
// Vedlikehold
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
|
||||||
33
migrations/016_message_placements.sql
Normal file
33
migrations/016_message_placements.sql
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
-- 016_message_placements.sql
|
||||||
|
-- Plasseringsrelasjon: sporer hvor meldinger vises på tvers av kontekster.
|
||||||
|
-- En melding (node) kan ha plasseringer i flere kontekster samtidig
|
||||||
|
-- (chat, kanban, storyboard, kalender, notes).
|
||||||
|
--
|
||||||
|
-- Ref: docs/features/universell_overfoering.md § 2
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE message_placements (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
message_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
|
||||||
|
context_type TEXT NOT NULL, -- 'chat', 'kanban', 'storyboard', 'calendar', 'notes'
|
||||||
|
context_id UUID NOT NULL, -- channel_id, board_id, episode_id, calendar_id, note_id
|
||||||
|
entered_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
position JSONB, -- kontekst-spesifikk posisjon (se § 2.2)
|
||||||
|
UNIQUE (message_id, context_type, context_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Oppslag: alle meldinger i en gitt kontekst, sortert etter ankomst
|
||||||
|
CREATE INDEX idx_placements_context ON message_placements(context_type, context_id, entered_at);
|
||||||
|
|
||||||
|
-- Oppslag: alle plasseringer for en gitt melding
|
||||||
|
CREATE INDEX idx_placements_message ON message_placements(message_id);
|
||||||
|
|
||||||
|
COMMENT ON TABLE message_placements IS 'Plasseringsrelasjon — sporer hvor meldinger vises. Ref: docs/features/universell_overfoering.md § 2';
|
||||||
|
COMMENT ON COLUMN message_placements.message_id IS 'Meldingens node-id (node_kind = melding)';
|
||||||
|
COMMENT ON COLUMN message_placements.context_type IS 'Konteksttype: chat, kanban, storyboard, calendar, notes';
|
||||||
|
COMMENT ON COLUMN message_placements.context_id IS 'ID til kontekstnoden (kanal, brett, episode, etc.)';
|
||||||
|
COMMENT ON COLUMN message_placements.entered_at IS 'Når meldingen ankom denne konteksten (ikke created_at)';
|
||||||
|
COMMENT ON COLUMN message_placements.position IS 'Kontekst-spesifikk posisjon som JSONB. null=sortert etter entered_at (chat). {x,y}=canvas (storyboard). {column_id,position}=kanban. {date,all_day}=kalender. {position}=notes.';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
@ -273,6 +273,28 @@ pub fn delete_edge(ctx: &ReducerContext, id: String) -> Result<(), String> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Plasseringsrelasjon (message_placements)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Sporer hvor meldinger vises på tvers av kontekster (chat, kanban, storyboard, etc.).
|
||||||
|
/// Sanntidskopi — PG er autoritativ, SpacetimeDB gir instant UI-oppdatering.
|
||||||
|
/// Synk: STDB → PG via sync-worker.
|
||||||
|
/// Ref: docs/features/universell_overfoering.md § 2, 5
|
||||||
|
#[spacetimedb::table(accessor = message_placement, public)]
|
||||||
|
pub struct MessagePlacement {
|
||||||
|
#[primary_key]
|
||||||
|
pub id: String,
|
||||||
|
|
||||||
|
#[index(btree)]
|
||||||
|
pub message_id: String,
|
||||||
|
#[index(btree)]
|
||||||
|
pub context_id: String,
|
||||||
|
pub context_type: String, // 'chat', 'kanban', 'storyboard', 'calendar', 'notes'
|
||||||
|
pub entered_at: Timestamp,
|
||||||
|
pub position_json: String, // JSON-serialisert posisjon (null for chat, {x,y} for storyboard, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Mixer-kanaler (delt mixer-kontroll via SpacetimeDB)
|
// Mixer-kanaler (delt mixer-kontroll via SpacetimeDB)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -684,6 +706,87 @@ pub fn set_mixer_role(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Placement reducers
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/// Plasser en melding i en kontekst. Idempotent — oppdaterer hvis
|
||||||
|
/// (message_id, context_type, context_id) allerede finnes.
|
||||||
|
#[reducer]
|
||||||
|
pub fn place_message(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
id: String,
|
||||||
|
message_id: String,
|
||||||
|
context_type: String,
|
||||||
|
context_id: String,
|
||||||
|
position_json: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if id.is_empty() {
|
||||||
|
return Err("id kan ikke være tom".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sjekk om plassering allerede finnes (upsert-semantikk)
|
||||||
|
// Søk på message_id + context_id (context_type er implisitt i context_id)
|
||||||
|
let existing: Option<MessagePlacement> = ctx.db.message_placement()
|
||||||
|
.message_id()
|
||||||
|
.filter(&message_id)
|
||||||
|
.find(|p| p.context_type == context_type && p.context_id == context_id);
|
||||||
|
|
||||||
|
if let Some(existing) = existing {
|
||||||
|
ctx.db.message_placement().id().update(MessagePlacement {
|
||||||
|
position_json,
|
||||||
|
..existing
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
ctx.db.message_placement().insert(MessagePlacement {
|
||||||
|
id,
|
||||||
|
message_id,
|
||||||
|
context_type,
|
||||||
|
context_id,
|
||||||
|
entered_at: ctx.timestamp,
|
||||||
|
position_json,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fjern en meldings plassering fra en kontekst.
|
||||||
|
#[reducer]
|
||||||
|
pub fn remove_placement(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
message_id: String,
|
||||||
|
context_type: String,
|
||||||
|
context_id: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let existing: Option<MessagePlacement> = ctx.db.message_placement()
|
||||||
|
.message_id()
|
||||||
|
.filter(&message_id)
|
||||||
|
.find(|p| p.context_type == context_type && p.context_id == context_id);
|
||||||
|
|
||||||
|
if let Some(placement) = existing {
|
||||||
|
ctx.db.message_placement().id().delete(&placement.id);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Flytt en plassering på canvas (oppdater posisjon).
|
||||||
|
/// Brukes for storyboard, kanban-rekkefølge, kalender-dato, etc.
|
||||||
|
#[reducer]
|
||||||
|
pub fn move_on_canvas(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
placement_id: String,
|
||||||
|
new_position_json: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let existing = ctx.db.message_placement().id().find(&placement_id)
|
||||||
|
.ok_or_else(|| format!("Plassering {} ikke funnet", placement_id))?;
|
||||||
|
|
||||||
|
ctx.db.message_placement().id().update(MessagePlacement {
|
||||||
|
position_json: new_position_json,
|
||||||
|
..existing
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Warmup/vedlikehold
|
// Warmup/vedlikehold
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
@ -708,6 +811,10 @@ pub fn clear_all(ctx: &ReducerContext) -> Result<(), String> {
|
||||||
for m in all_mixer {
|
for m in all_mixer {
|
||||||
ctx.db.mixer_channel().id().delete(&m.id);
|
ctx.db.mixer_channel().id().delete(&m.id);
|
||||||
}
|
}
|
||||||
log::info!("Alle noder, edges, node_access og mixer_channels slettet (clear_all)");
|
let all_placements: Vec<_> = ctx.db.message_placement().iter().collect();
|
||||||
|
for p in all_placements {
|
||||||
|
ctx.db.message_placement().id().delete(&p.id);
|
||||||
|
}
|
||||||
|
log::info!("Alle noder, edges, node_access, mixer_channels og message_placements slettet (clear_all)");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
tasks.md
3
tasks.md
|
|
@ -223,8 +223,7 @@ Ref: `docs/retninger/arbeidsflaten.md`, `docs/features/canvas_primitiv.md`
|
||||||
|
|
||||||
Ref: `docs/features/universell_overfoering.md`, `docs/retninger/arbeidsflaten.md` § "Kompatibilitetsmatrise"
|
Ref: `docs/features/universell_overfoering.md`, `docs/retninger/arbeidsflaten.md` § "Kompatibilitetsmatrise"
|
||||||
|
|
||||||
- [~] 20.1 message_placements tabell: PG-migrasjon + SpacetimeDB-modul med `place_message`, `remove_placement`, `move_on_canvas` reducers. Synk STDB→PG. Ref: `docs/features/universell_overfoering.md` § 2.
|
- [x] 20.1 message_placements tabell: PG-migrasjon + SpacetimeDB-modul med `place_message`, `remove_placement`, `move_on_canvas` reducers. Synk STDB→PG. Ref: `docs/features/universell_overfoering.md` § 2.
|
||||||
> Påbegynt: 2026-03-18T07:54
|
|
||||||
- [ ] 20.2 source_material edge-type: legg til i edge-skjema + maskinrommet-validering. Støtt kontekst-metadata (quoted, summarized, referenced) og excerpt-felt. Ref: `docs/retninger/arbeidsflaten.md` § "source_material-edge".
|
- [ ] 20.2 source_material edge-type: legg til i edge-skjema + maskinrommet-validering. Støtt kontekst-metadata (quoted, summarized, referenced) og excerpt-felt. Ref: `docs/retninger/arbeidsflaten.md` § "source_material-edge".
|
||||||
- [ ] 20.3 BlockReceiver interface: implementer `canReceive()`, `receive()`, `renderDropZone()` i alle trait-komponenter (Chat, Kanban, Kalender, Editor, Studio). Kompatibilitetsmatrise bestemmer godkjente drops. Ref: `docs/features/universell_overfoering.md` § 4–5.
|
- [ ] 20.3 BlockReceiver interface: implementer `canReceive()`, `receive()`, `renderDropZone()` i alle trait-komponenter (Chat, Kanban, Kalender, Editor, Studio). Kompatibilitetsmatrise bestemmer godkjente drops. Ref: `docs/features/universell_overfoering.md` § 4–5.
|
||||||
- [ ] 20.4 Transfer service: `innholdstransfer`-modus (ny node + source_material edge) og `lettvekts-triage` (eksisterende node + ny edge/placement). Bestem modus fra verktøy-par. Shift-modifier for override. Ref: `docs/features/universell_overfoering.md` § 1, 3.
|
- [ ] 20.4 Transfer service: `innholdstransfer`-modus (ny node + source_material edge) og `lettvekts-triage` (eksisterende node + ny edge/placement). Bestem modus fra verktøy-par. Shift-modifier for override. Ref: `docs/features/universell_overfoering.md` § 1, 3.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue