diff --git a/docs/features/kanban.md b/docs/features/kanban.md index 46bb520..173aee3 100644 --- a/docs/features/kanban.md +++ b/docs/features/kanban.md @@ -1,59 +1,71 @@ -# Feature: Kanban (Planlegging) -**Filsti:** `docs/features/kanban.md` - -## 1. Konsept -Et drag-and-drop Kanban-brett for planlegging. Primært brukt til episodeplanlegging i Redaksjonen, men også mottaker av AI-genererte action points fra Møterommet. - -## 2. Status -**PG-adapter ferdig og deployet (mars 2025).** SpacetimeDB-sync gjenstår. - -### Implementert -- Migrering `0002_kanban.sql`: `kanban_boards`, `kanban_columns`, `kanban_cards` -- Kanban-kort er nodes i kunnskapsgrafen (tilgangsstyrt via `node_access`-matrise) -- REAL-posisjon for midpoint-innsetting (`(1.0 + 2.0) / 2 = 1.5`) — ingen re-nummerering -- REST API: GET brett, POST kolonne/kort, PATCH kort/flytt, DELETE kort -- PG polling-adapter (`pg.svelte.ts`) med 5 sek intervall og optimistisk UI -- Adapter-factory (`create.svelte.ts`) — klar for SpacetimeDB-hybrid -- KanbanBlock.svelte: drag & drop, redigeringsmodal (tittel/beskrivelse/slett), enkelt kort-input som legger til i første kolonne - -### Gjenstår -- SpacetimeDB-modul + hybrid-adapter for sanntidsoppdatering -- Reposisjonering ved dra innad i kolonne (sortert rekkefølge) -- Tildeling (assignee) UI -- Fargekoder/labels på kort -- AI-integrasjon: møtereferent → nye kort - -## 3. Datamodell - -``` -kanban_boards (id FK→nodes, parent_id FK→nodes, name) -kanban_columns (id, board_id FK→kanban_boards, name, color, position REAL) -kanban_cards (id FK→nodes, column_id FK→kanban_columns, title, description, assignee_id, position REAL, created_by, created_at) -``` - -Kort og brett er nodes — tilgang styres via `node_access`-matrisen. - -## 4. API-endepunkter - -| Metode | Sti | Beskrivelse | -|---|---|---| -| GET | `/api/kanban/[boardId]` | Hent brett med kolonner og kort | -| POST | `/api/kanban/[boardId]/columns` | Opprett kolonne | -| POST | `/api/kanban/[boardId]/cards` | Opprett kort (oppretter node + kort) | -| PATCH | `/api/kanban/[boardId]/cards/[cardId]` | Oppdater tittel/beskrivelse | -| PATCH | `/api/kanban/[boardId]/cards/[cardId]/move` | Flytt kort til kolonne/posisjon | -| DELETE | `/api/kanban/[boardId]/cards/[cardId]` | Slett kort (cascader fra node) | - -## 5. Brukes av - -| Konsept | Bruk | -|---|---| -| Redaksjonen | Episodeplanlegging — dra Temaer inn i Kjøreplanen | -| Møterommet | AI-referenten foreslår nye kort basert på action points | -| Foreningen Liberalistene | Styreoppgaver (Å gjøre / Pågår / Ferdig) | - -## 6. Instruks for Claude Code -* Bruk native HTML5 Drag and Drop i SvelteKit, unngå tunge biblioteker. -* PG-adapter er autoritativ inntil SpacetimeDB-sync er på plass. -* Tilgang styres via `node_access`-matrisen. -* Sjekk `docs/erfaringer/adapter_moenster.md` for hybrid-strategi. +# Feature: Kanban (Planlegging) +**Filsti:** `docs/features/kanban.md` + +## 1. Konsept +Et drag-and-drop Kanban-brett for planlegging. Primært brukt til +episodeplanlegging i Redaksjonen, men også mottaker av AI-genererte +action points fra Møterommet. + +## 2. Status +**Implementert med nodes+edges (mars 2026).** Sanntid via SpacetimeDB. + +### Implementert +- Board = samlings-node (`node_kind: 'collection'`, `metadata.board: true`) +- Kolonner definert i board-metadata: `metadata.columns: ["todo", "in_progress", "done"]` +- Kort = content-noder med `belongs_to`-edge til board +- Status via `status`-edge (kort → board) med `metadata.value` +- Posisjon via `belongs_to`-edge `metadata.position` (REAL for midpoint-innsetting) +- Backend: `POST /intentions/update_edge` for statusendring +- Backend: `GET /query/board?board_id=...` for board-spørring +- Frontend: `/board/[id]` route med HTML5 drag-and-drop +- Sanntid via SpacetimeDB edge-subscriptions (ingen polling) +- Opprett kort direkte i kolonne (tittel-input) +- Oppretting av nye brett fra mottak-siden + +### Gjenstår +- Reposisjonering ved dra innad i kolonne (sortert rekkefølge) +- Redigeringsmodal for kort (tittel/beskrivelse) +- Tildeling (assignee) UI +- Fargekoder/labels på kort +- AI-integrasjon: møtereferent → nye kort +- Tilpassbare kolonnenavn + +## 3. Datamodell + +Ingen separate kanban-tabeller. Alt er noder og edges (kjerneprimitivene): + +``` +Board = collection-node (metadata.board: true, metadata.columns: [...]) +Kort = content-node + + belongs_to-edge → board (metadata.position: REAL) + + status-edge → board (metadata.value: "todo"|"in_progress"|"done") +``` + +Tilgang styres via `node_access`-matrisen. Brett er synlige for +brukere med `owner`/`admin`/`member_of`-edge til board-noden. + +## 4. API-endepunkter + +| Metode | Sti | Beskrivelse | +|---|---|---| +| GET | `/query/board?board_id=...` | Hent brett med kort, status og posisjon | +| POST | `/intentions/create_node` | Opprett kort (content-node) | +| POST | `/intentions/create_edge` | Koble kort til brett (belongs_to + status) | +| POST | `/intentions/update_edge` | Endre status ved drag-and-drop | +| POST | `/intentions/update_node` | Oppdater tittel/beskrivelse | +| POST | `/intentions/delete_node` | Slett kort (cascader edges) | + +## 5. Brukes av + +| Konsept | Bruk | +|---|---| +| Redaksjonen | Episodeplanlegging — dra Temaer inn i Kjøreplanen | +| Møterommet | AI-referenten foreslår nye kort basert på action points | + +## 6. Instruks for Claude Code +* Alt er noder og edges — ingen separate kanban-tabeller. +* Board er en collection-node med `metadata.board: true`. +* Status er en `status`-edge (kort → board) med `metadata.value`. +* Bruk native HTML5 Drag and Drop, unngå tunge biblioteker. +* Sanntid via SpacetimeDB edge-subscriptions. +* Tilgang styres via `node_access`-matrisen. diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index efcd7d6..e91a85c 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -66,6 +66,67 @@ export function createEdge( return post(accessToken, '/intentions/create_edge', data); } +// ============================================================================= +// Edge-oppdatering +// ============================================================================= + +export interface UpdateEdgeRequest { + edge_id: string; + edge_type?: string; + metadata?: Record; +} + +export interface UpdateEdgeResponse { + edge_id: string; +} + +export function updateEdge( + accessToken: string, + data: UpdateEdgeRequest +): Promise { + return post(accessToken, '/intentions/update_edge', data); +} + +// ============================================================================= +// Board / Kanban +// ============================================================================= + +export interface BoardCard { + node_id: string; + title: string | null; + content: string | null; + node_kind: string; + metadata: Record; + created_at: string; + created_by: string | null; + status: string | null; + position: number; + belongs_to_edge_id: string; + status_edge_id: string | null; +} + +export interface BoardResponse { + board_id: string; + board_title: string | null; + columns: string[]; + cards: BoardCard[]; +} + +export async function fetchBoard(accessToken: string, boardId: string): Promise { + const res = await fetch(`${BASE_URL}/query/board?board_id=${encodeURIComponent(boardId)}`, { + headers: { Authorization: `Bearer ${accessToken}` } + }); + if (!res.ok) { + const body = await res.text(); + throw new Error(`board failed (${res.status}): ${body}`); + } + return res.json(); +} + +// ============================================================================= +// Kommunikasjon +// ============================================================================= + export interface CreateCommunicationRequest { title?: string; participants?: string[]; diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 509072c..a19923e 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -120,6 +120,46 @@ return null; } + /** Check if a node is a kanban board (collection with board metadata) */ + function isBoard(node: Node): boolean { + if (node.nodeKind !== 'collection') return false; + try { + const meta = JSON.parse(node.metadata ?? '{}'); + return meta.board === true; + } catch { return false; } + } + + let isCreatingBoard = $state(false); + + /** Create a new kanban board */ + async function handleNewBoard() { + if (!accessToken || !nodeId || isCreatingBoard) return; + isCreatingBoard = true; + try { + const { node_id } = await createNode(accessToken, { + node_kind: 'collection', + title: 'Nytt brett', + visibility: 'hidden', + metadata: { + board: true, + columns: ['todo', 'in_progress', 'done'] + } + }); + + await createEdge(accessToken, { + source_id: nodeId, + target_id: node_id, + edge_type: 'owner' + }); + + window.location.href = `/board/${node_id}`; + } catch (e) { + console.error('Feil ved oppretting av brett:', e); + } finally { + isCreatingBoard = false; + } + } + let showNewChatDialog = $state(false); /** Open the new chat dialog to pick a participant */ @@ -238,12 +278,21 @@

Mottak

{#if connected && accessToken} - +
+ + +
{/if}
@@ -268,7 +317,30 @@