From 51eb089f0c466db768ece5745e0cd3a807ddd1d1 Mon Sep 17 00:00:00 2001 From: vegard Date: Wed, 18 Mar 2026 00:32:31 +0000 Subject: [PATCH] =?UTF-8?q?Trait-administrasjon:=20admin-UI=20for=20=C3=A5?= =?UTF-8?q?=20administrere=20traits=20p=C3=A5=20samlinger=20(oppgave=2013.?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Legger til et admin-panel på samlingssiden der man kan legge til/fjerne traits fra katalogen og konfigurere per-trait-innstillinger. Endringene sendes via updateNode til maskinrommet som validerer mot VALID_TRAITS. - Ny updateNode-funksjon i api.ts - Delt trait-katalog i lib/traits.ts (brukes av collection/new og TraitAdmin) - TraitAdmin.svelte: toggle traits, rediger config-nøkler, lagre - Integrasjon i collection/[id] med Traits-knapp i header Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/lib/api.ts | 24 ++ .../lib/components/traits/TraitAdmin.svelte | 265 ++++++++++++++++++ frontend/src/lib/traits.ts | 215 ++++++++++++++ .../src/routes/collection/[id]/+page.svelte | 50 +++- .../src/routes/collection/new/+page.svelte | 220 +-------------- tasks.md | 3 +- 6 files changed, 549 insertions(+), 228 deletions(-) create mode 100644 frontend/src/lib/components/traits/TraitAdmin.svelte create mode 100644 frontend/src/lib/traits.ts diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 5aebea3..7daccab 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -66,6 +66,30 @@ export function createEdge( return post(accessToken, '/intentions/create_edge', data); } +// ============================================================================= +// Node-oppdatering +// ============================================================================= + +export interface UpdateNodeRequest { + node_id: string; + node_kind?: string; + title?: string; + content?: string; + visibility?: string; + metadata?: Record; +} + +export interface UpdateNodeResponse { + node_id: string; +} + +export function updateNode( + accessToken: string, + data: UpdateNodeRequest +): Promise { + return post(accessToken, '/intentions/update_node', data); +} + // ============================================================================= // Edge-oppdatering // ============================================================================= diff --git a/frontend/src/lib/components/traits/TraitAdmin.svelte b/frontend/src/lib/components/traits/TraitAdmin.svelte new file mode 100644 index 0000000..69f80c2 --- /dev/null +++ b/frontend/src/lib/components/traits/TraitAdmin.svelte @@ -0,0 +1,265 @@ + + +
+ +
+

Administrer traits

+ +
+ +
+ + {#if activeTraitNames.length > 0} +
+

+ Aktive traits ({activeTraitNames.length}) +

+
+ {#each activeTraitNames as trait (trait)} +
+
+
+ {trait} + {#if Object.keys(workingTraits[trait]).length > 0} + + ({Object.keys(workingTraits[trait]).length} config) + + {/if} +
+
+ + +
+
+ + + {#if editingTrait === trait} +
+ + {#each Object.entries(workingTraits[trait]) as [key, value] (key)} +
+ + {key}: {JSON.stringify(value)} + + +
+ {/each} + + +
+ + + +
+
+ {/if} +
+ {/each} +
+
+ {/if} + + +
+

+ Legg til traits +

+
+ {#each traitCatalog as category (category.label)} + {@const availableTraits = category.traits.filter(t => !(t in workingTraits))} + {#if availableTraits.length > 0} +
+

{category.label}

+
+ {#each availableTraits as trait (trait)} + + {/each} +
+
+ {/if} + {/each} +
+
+ + + {#if error} +
+ {error} +
+ {/if} + {#if successMsg} +
+ {successMsg} +
+ {/if} + + +
+ + +
+
+
diff --git a/frontend/src/lib/traits.ts b/frontend/src/lib/traits.ts new file mode 100644 index 0000000..f58212e --- /dev/null +++ b/frontend/src/lib/traits.ts @@ -0,0 +1,215 @@ +/** + * Shared trait catalog and package definitions. + * Used by collection/new (package selector) and TraitAdmin (trait management). + */ + +export interface TraitCategory { + label: string; + traits: string[]; +} + +export const traitCatalog: TraitCategory[] = [ + { label: 'Innhold & redigering', traits: ['editor', 'versioning', 'collaboration', 'translation', 'templates'] }, + { label: 'Publisering & distribusjon', traits: ['publishing', 'rss', 'newsletter', 'custom_domain', 'analytics', 'embed', 'api'] }, + { label: 'Lyd & video', traits: ['podcast', 'recording', 'transcription', 'tts', 'clips', 'playlist'] }, + { label: 'Kommunikasjon', traits: ['chat', 'forum', 'comments', 'guest_input', 'announcements', 'polls', 'qa'] }, + { label: 'Organisering', traits: ['kanban', 'calendar', 'timeline', 'table', 'gallery', 'bookmarks', 'tags'] }, + { label: 'Kunnskap', traits: ['knowledge_graph', 'wiki', 'glossary', 'faq', 'bibliography'] }, + { label: 'Automatisering & AI', traits: ['auto_tag', 'auto_summarize', 'digest', 'bridge', 'moderation'] }, + { label: 'Tilgang & fellesskap', traits: ['membership', 'roles', 'invites', 'paywall', 'directory'] }, + { label: 'Ekstern integrasjon', traits: ['webhook', 'import', 'export', 'ical_sync'] }, +]; + +export interface Package { + id: string; + name: string; + description: string; + icon: string; + traits: Record>; +} + +export const packages: Package[] = [ + { + id: 'nettmagasin', + name: 'Nettmagasin', + description: 'Publiser artikler med RSS, kommentarer og nyhetsbrev', + icon: '📰', + traits: { + editor: { preset: 'longform' }, + publishing: {}, + rss: {}, + comments: {}, + analytics: {}, + custom_domain: {}, + newsletter: {}, + }, + }, + { + id: 'podcaststudio', + name: 'Podcaststudio', + description: 'Podcast med opptak, transkripsjon og kunnskapsgraf', + icon: '🎙️', + traits: { + podcast: {}, + recording: {}, + transcription: {}, + editor: { preset: 'shownotes' }, + rss: {}, + analytics: {}, + clips: {}, + knowledge_graph: {}, + }, + }, + { + id: 'nyhetsbrev', + name: 'Nyhetsbrev', + description: 'Skriv og distribuer nyhetsbrev med analyse', + icon: '✉️', + traits: { + editor: { preset: 'longform' }, + newsletter: {}, + analytics: {}, + versioning: {}, + }, + }, + { + id: 'wiki', + name: 'Wiki', + description: 'Samarbeidende kunnskapsbase med versjonering', + icon: '📚', + traits: { + wiki: {}, + editor: { preset: 'longform' }, + collaboration: {}, + versioning: {}, + knowledge_graph: {}, + glossary: {}, + }, + }, + { + id: 'diskusjonsklubb', + name: 'Diskusjonsklubb', + description: 'Forum, chat og avstemninger for en gruppe', + icon: '💬', + traits: { + forum: {}, + chat: {}, + polls: {}, + membership: {}, + roles: {}, + directory: {}, + }, + }, + { + id: 'kursplattform', + name: 'Kursplattform', + description: 'Kursinnhold med spillelister, Q&A og betaling', + icon: '🎓', + traits: { + editor: { preset: 'longform' }, + playlist: {}, + qa: {}, + membership: {}, + paywall: {}, + templates: {}, + }, + }, + { + id: 'moteplass', + name: 'Møteplass', + description: 'Opptak, chat, kanban og kalender for møter', + icon: '🤝', + traits: { + recording: {}, + chat: {}, + kanban: {}, + calendar: {}, + auto_summarize: {}, + guest_input: {}, + }, + }, + { + id: 'fotoblogg', + name: 'Fotoblogg', + description: 'Bildegalleri med publisering og kommentarer', + icon: '📷', + traits: { + gallery: {}, + publishing: {}, + comments: {}, + custom_domain: {}, + rss: {}, + }, + }, + { + id: 'prosjektstyring', + name: 'Prosjektstyring', + description: 'Kanban, kalender og chat for teamarbeid', + icon: '📋', + traits: { + kanban: {}, + calendar: {}, + chat: {}, + table: {}, + tags: {}, + roles: {}, + }, + }, + { + id: 'forskning', + name: 'Åpen forskning', + description: 'Akademisk publisering med versjonering og bibliografi', + icon: '🔬', + traits: { + editor: { preset: 'longform' }, + versioning: {}, + bibliography: {}, + publishing: {}, + comments: {}, + collaboration: {}, + api: {}, + }, + }, + { + id: 'community-radio', + name: 'Community radio', + description: 'Opptak, podcast, chat og avstemninger', + icon: '📻', + traits: { + recording: {}, + podcast: {}, + chat: {}, + polls: {}, + membership: {}, + clips: {}, + playlist: {}, + }, + }, + { + id: 'bokmerke-vegg', + name: 'Bokmerke-vegg', + description: 'Kuraterte lenker med tags og kommentarer', + icon: '🔖', + traits: { + bookmarks: {}, + tags: {}, + publishing: {}, + rss: {}, + comments: {}, + }, + }, + { + id: 'redaksjon', + name: 'Redaksjon', + description: 'Redaksjonelt arbeid med chat, kanban og kalender', + icon: '🗞️', + traits: { + chat: {}, + kanban: {}, + calendar: {}, + editor: { preset: 'longform' }, + knowledge_graph: {}, + guest_input: {}, + }, + }, +]; diff --git a/frontend/src/routes/collection/[id]/+page.svelte b/frontend/src/routes/collection/[id]/+page.svelte index 62b5557..0d86f8c 100644 --- a/frontend/src/routes/collection/[id]/+page.svelte +++ b/frontend/src/routes/collection/[id]/+page.svelte @@ -14,24 +14,31 @@ import RecordingTrait from '$lib/components/traits/RecordingTrait.svelte'; import TranscriptionTrait from '$lib/components/traits/TranscriptionTrait.svelte'; import GenericTrait from '$lib/components/traits/GenericTrait.svelte'; + import TraitAdmin from '$lib/components/traits/TraitAdmin.svelte'; const session = $derived($page.data.session as Record | undefined); const nodeId = $derived(session?.nodeId as string | undefined); const accessToken = $derived(session?.accessToken as string | undefined); + let showTraitAdmin = $state(false); const connected = $derived(connectionState.current === 'connected'); const collectionId = $derived($page.params.id ?? ''); const collectionNode = $derived(connected ? nodeStore.get(collectionId) : undefined); - /** Parse traits from collection metadata */ - const traits = $derived.by((): Record> => { + /** Parse full metadata */ + const parsedMetadata = $derived.by((): Record => { if (!collectionNode) return {}; try { - const meta = JSON.parse(collectionNode.metadata ?? '{}'); - if (meta.traits && typeof meta.traits === 'object' && !Array.isArray(meta.traits)) { - return meta.traits as Record>; - } - } catch { /* ignore */ } + return JSON.parse(collectionNode.metadata ?? '{}') as Record; + } catch { return {}; } + }); + + /** Parse traits from collection metadata */ + const traits = $derived.by((): Record> => { + const meta = parsedMetadata; + if (meta.traits && typeof meta.traits === 'object' && !Array.isArray(meta.traits)) { + return meta.traits as Record>; + } return {}; }); @@ -80,11 +87,32 @@ {traitNames.length} traits {/if} {childCount} noder + {#if connected && collectionNode && accessToken} + + {/if}
+ + {#if showTraitAdmin && accessToken} +
+ { showTraitAdmin = false; }} + /> +
+ {/if} + {#if !connected}

Venter på tilkobling…

{:else if !collectionNode} @@ -97,6 +125,14 @@

Denne samlingen har ingen aktive traits.

Traits bestemmer hvilke verktøy og visninger som er tilgjengelige.

+ {#if accessToken && !showTraitAdmin} + + {/if}
{:else} diff --git a/frontend/src/routes/collection/new/+page.svelte b/frontend/src/routes/collection/new/+page.svelte index 44f33a3..0c9e799 100644 --- a/frontend/src/routes/collection/new/+page.svelte +++ b/frontend/src/routes/collection/new/+page.svelte @@ -3,231 +3,13 @@ import { goto } from '$app/navigation'; import { connectionState } from '$lib/spacetime'; import { createNode, createEdge } from '$lib/api'; + import { traitCatalog, packages, type Package } from '$lib/traits'; const session = $derived($page.data.session as Record | undefined); const nodeId = $derived(session?.nodeId as string | undefined); const accessToken = $derived(session?.accessToken as string | undefined); const connected = $derived(connectionState.current === 'connected'); - // ========================================================================= - // Trait-katalog (kategorisert) - // ========================================================================= - - interface TraitCategory { - label: string; - traits: string[]; - } - - const traitCatalog: TraitCategory[] = [ - { label: 'Innhold & redigering', traits: ['editor', 'versioning', 'collaboration', 'translation', 'templates'] }, - { label: 'Publisering & distribusjon', traits: ['publishing', 'rss', 'newsletter', 'custom_domain', 'analytics', 'embed', 'api'] }, - { label: 'Lyd & video', traits: ['podcast', 'recording', 'transcription', 'tts', 'clips', 'playlist'] }, - { label: 'Kommunikasjon', traits: ['chat', 'forum', 'comments', 'guest_input', 'announcements', 'polls', 'qa'] }, - { label: 'Organisering', traits: ['kanban', 'calendar', 'timeline', 'table', 'gallery', 'bookmarks', 'tags'] }, - { label: 'Kunnskap', traits: ['knowledge_graph', 'wiki', 'glossary', 'faq', 'bibliography'] }, - { label: 'Automatisering & AI', traits: ['auto_tag', 'auto_summarize', 'digest', 'bridge', 'moderation'] }, - { label: 'Tilgang & fellesskap', traits: ['membership', 'roles', 'invites', 'paywall', 'directory'] }, - { label: 'Ekstern integrasjon', traits: ['webhook', 'import', 'export', 'ical_sync'] }, - ]; - - // ========================================================================= - // Pakkedefinisjoner - // ========================================================================= - - interface Package { - id: string; - name: string; - description: string; - icon: string; - traits: Record>; - } - - const packages: Package[] = [ - { - id: 'nettmagasin', - name: 'Nettmagasin', - description: 'Publiser artikler med RSS, kommentarer og nyhetsbrev', - icon: '📰', - traits: { - editor: { preset: 'longform' }, - publishing: {}, - rss: {}, - comments: {}, - analytics: {}, - custom_domain: {}, - newsletter: {}, - }, - }, - { - id: 'podcaststudio', - name: 'Podcaststudio', - description: 'Podcast med opptak, transkripsjon og kunnskapsgraf', - icon: '🎙️', - traits: { - podcast: {}, - recording: {}, - transcription: {}, - editor: { preset: 'shownotes' }, - rss: {}, - analytics: {}, - clips: {}, - knowledge_graph: {}, - }, - }, - { - id: 'nyhetsbrev', - name: 'Nyhetsbrev', - description: 'Skriv og distribuer nyhetsbrev med analyse', - icon: '✉️', - traits: { - editor: { preset: 'longform' }, - newsletter: {}, - analytics: {}, - versioning: {}, - }, - }, - { - id: 'wiki', - name: 'Wiki', - description: 'Samarbeidende kunnskapsbase med versjonering', - icon: '📚', - traits: { - wiki: {}, - editor: { preset: 'longform' }, - collaboration: {}, - versioning: {}, - knowledge_graph: {}, - glossary: {}, - }, - }, - { - id: 'diskusjonsklubb', - name: 'Diskusjonsklubb', - description: 'Forum, chat og avstemninger for en gruppe', - icon: '💬', - traits: { - forum: {}, - chat: {}, - polls: {}, - membership: {}, - roles: {}, - directory: {}, - }, - }, - { - id: 'kursplattform', - name: 'Kursplattform', - description: 'Kursinnhold med spillelister, Q&A og betaling', - icon: '🎓', - traits: { - editor: { preset: 'longform' }, - playlist: {}, - qa: {}, - membership: {}, - paywall: {}, - templates: {}, - }, - }, - { - id: 'moteplass', - name: 'Møteplass', - description: 'Opptak, chat, kanban og kalender for møter', - icon: '🤝', - traits: { - recording: {}, - chat: {}, - kanban: {}, - calendar: {}, - auto_summarize: {}, - guest_input: {}, - }, - }, - { - id: 'fotoblogg', - name: 'Fotoblogg', - description: 'Bildegalleri med publisering og kommentarer', - icon: '📷', - traits: { - gallery: {}, - publishing: {}, - comments: {}, - custom_domain: {}, - rss: {}, - }, - }, - { - id: 'prosjektstyring', - name: 'Prosjektstyring', - description: 'Kanban, kalender og chat for teamarbeid', - icon: '📋', - traits: { - kanban: {}, - calendar: {}, - chat: {}, - table: {}, - tags: {}, - roles: {}, - }, - }, - { - id: 'forskning', - name: 'Åpen forskning', - description: 'Akademisk publisering med versjonering og bibliografi', - icon: '🔬', - traits: { - editor: { preset: 'longform' }, - versioning: {}, - bibliography: {}, - publishing: {}, - comments: {}, - collaboration: {}, - api: {}, - }, - }, - { - id: 'community-radio', - name: 'Community radio', - description: 'Opptak, podcast, chat og avstemninger', - icon: '📻', - traits: { - recording: {}, - podcast: {}, - chat: {}, - polls: {}, - membership: {}, - clips: {}, - playlist: {}, - }, - }, - { - id: 'bokmerke-vegg', - name: 'Bokmerke-vegg', - description: 'Kuraterte lenker med tags og kommentarer', - icon: '🔖', - traits: { - bookmarks: {}, - tags: {}, - publishing: {}, - rss: {}, - comments: {}, - }, - }, - { - id: 'redaksjon', - name: 'Redaksjon', - description: 'Redaksjonelt arbeid med chat, kanban og kalender', - icon: '🗞️', - traits: { - chat: {}, - kanban: {}, - calendar: {}, - editor: { preset: 'longform' }, - knowledge_graph: {}, - guest_input: {}, - }, - }, - ]; - // ========================================================================= // State // ========================================================================= diff --git a/tasks.md b/tasks.md index f7d403c..702d071 100644 --- a/tasks.md +++ b/tasks.md @@ -133,8 +133,7 @@ Uavhengige faser kan fortsatt plukkes. - [x] 13.1 Trait-metadata på samlingsnoder: maskinrommet validerer `metadata.traits`-objektet ved `create_node` og `update_node` for samlingsnoder. Avvis ukjente trait-navn. Ref: `docs/primitiver/traits.md`. - [x] 13.2 Trait-aware frontend: samlingssider leser `traits` fra metadata og rendrer kun aktive komponenter. Dynamisk komponent-lasting basert på trait-liste. - [x] 13.3 Pakkevelger: UI for å opprette ny samling med forhåndsdefinert pakke (nettmagasin, podcaststudio, redaksjon osv.) eller manuelt valg av traits. -- [~] 13.4 Trait-administrasjon: admin-UI for å legge til/fjerne traits på eksisterende samlinger med konfigurasjon per trait. - > Påbegynt: 2026-03-18T00:27 +- [x] 13.4 Trait-administrasjon: admin-UI for å legge til/fjerne traits på eksisterende samlinger med konfigurasjon per trait. ## Fase 14: Publisering