# Synops — Implementeringsoppgaver

Sekvensiell liste. Hver oppgave gjøres i én Claude Code-sesjon.
Runner-scriptet plukker første ugjorte oppgave som ikke er blokkert.

## Statuser

- `- [ ]` — Klar til å gjøres
- `- [~]` — Pågår. En agent jobber på denne. Andre agenter hopper over.
- `- [x]` — Ferdig
- `- [?]` — Åpent spørsmål, trenger avklaring fra Vegard.
- `- [!]` — Blokkert av teknisk problem.

`[~]`, `[?]` og `[!]` blokkerer alle oppgaver som avhenger av denne.
Detaljer skrives som innrykket tekst med `>` prefix under oppgaven.
Runner-scriptet legger automatisk til `> Påbegynt: <timestamp>` for `[~]`.

Hvis en `[~]`-oppgave har stått i >60 min uten commit, anta at
sesjonen krasjet. Kjør `run-next-task.sh --unstale` for å frigjøre.

## Avhengigheter

Oppgaver innen en fase er sekvensielle (1.1 før 1.2, osv).
Faser avhenger av hverandre slik:

```
Fase 1 (infra) → Fase 2 (maskinrommet) → Fase 3 (frontend)
                                        ↘ Fase 4 (tilgang)
Fase 3 + 4 → Fase 5 (kommunikasjon)
Fase 2 → Fase 6 (CAS) → Fase 7 (lyd)
Fase 5 → Fase 8 (aliaser)
Fase 3 → Fase 9 (visninger)
Fase 2 → Fase 10 (AI)
Fase 5 + 6 + 7 → Fase 11 (produksjon)
Fase 3 + 4 → Fase 13 (traits)
Fase 6 + 13 → Fase 14 (publisering)
Fase 3 + 10 → Fase 15 (adminpanel)
Fase 11 + 13 → Fase 16 (lydmixer)
Fase 17 (lydstudio-utbedring) — ingen avhengigheter, kan kjøres parallelt
Fase 10 + 13 → Fase 18 (AI-verktøy)
Fase 3 + 13 → Fase 19 (arbeidsflaten — spatial canvas)
Fase 19 → Fase 20 (universell overføring + panelrework)
Fase 2 → Fase 21 (CLI-verktøy — unix-filosofi)
Alt → Fase 12 (herding)
Fase 12 → Fase 22 (SpacetimeDB-migrering — fullført)
Fase 22 → Fase 23 (validering — alle faser)
Fase 23 → Fase 24 (orkestrering)
Fase 24 → Fase 25 (web clipper)
Fase 25 → Fase 26 (epost)
Fase 25 → Fase 27 (tankekart) — uavhengig av epost
Fase 25 → Fase 28 (manglende CLI + AI-ruting) — uavhengig av epost
Fase 25 → Fase 29 (universell input) — uavhengig av epost
Fase 25 → Fase 30 (podcast-hosting) — uavhengig av epost
```

Hvis en oppgave er `[?]` eller `[!]`, hoppes den over — og alle
oppgaver som avhenger av den (innen fasen + avhengige faser).
Uavhengige faser kan fortsatt plukkes.

---

## Fase 1: Infrastruktur-fundament

- [x] 1.1 PostgreSQL schema: opprett database `synops`, enums (`visibility`, `access_level`), tabeller (`nodes`, `edges`, `node_access`, `auth_identities`) med indekser. Kjør på server via SSH. Ref: `docs/primitiver/nodes.md`, `docs/primitiver/edges.md`, `docs/retninger/bruker_ikke_workspace.md`.
- [x] 1.2 Seed-data: opprett Vegards brukernode (`node_kind='person'`, `title='Vegard'`) og `auth_identities`-rad. Opprett Sidelinja samlings-node og `owner`-edge fra Vegard.
- [x] 1.3 SpacetimeDB modul: opprett Rust-modul med `nodes` og `edges`-tabeller som speiler PG-skjema. Grunnleggende reducers for CRUD. Deploy til server. Ref: `docs/retninger/datalaget.md`, `docs/erfaringer/spacetimedb_integrasjon.md`.
- [x] 1.4 Caddy-config: reverse proxy for maskinrommet (api.sidelinja.org), SpacetimeDB, og SvelteKit. Auto-TLS. Ref: `docs/setup/produksjon.md`.
- [x] 1.5 Authentik: opprett OIDC-provider og applikasjon for Synops. Konfigurer redirect URIs. Ref: `docs/erfaringer/authentik_oppsett.md`.

## Fase 2: Maskinrommet — skjelett

- [x] 2.1 Rust-prosjekt: opprett `maskinrommet/` med axum, tokio, sqlx (PG), serde. Dockerfile. Kompilerer og starter. Ref: `docs/retninger/maskinrommet.md`.
- [x] 2.2 Auth-middleware: valider Authentik JWT-tokens, slå opp `auth_identities` → node_id. Returner 401 for ugyldige tokens.
- [x] 2.3 SpacetimeDB-klient i maskinrommet: koble til STDB, skriv noder og edges via reducers.
- [x] 2.4 Skrivestien: `POST /intentions/create_node` — valider, skriv STDB (instant), spawn async PG-skriving. Returner node_id umiddelbart.
- [x] 2.5 Flere intensjoner: `create_edge`, `update_node`, `delete_node`. Validering av tilgang (created_by eller owner/admin-edge).
- [x] 2.6 Docker Compose: legg maskinrommet inn i server-stacken. Intern nettverkstilgang til PG og STDB.

## Fase 3: Frontend — skjelett

- [x] 3.1 SvelteKit-prosjekt: opprett `frontend/` med TypeScript, TailwindCSS. PWA-manifest. Lokal dev med HMR.
- [x] 3.2 Authentik login: OIDC-flow (authorization code + PKCE). Session-håndtering. Redirect til login ved 401.
- [x] 3.3 STDB WebSocket-klient: abonner på noder og edges. Reaktiv Svelte-store som oppdateres ved endringer.
- [x] 3.4 Mottaksflaten v0: vis noder med edge til innlogget bruker, sortert på `created_at`. Enkel liste med tittel og utdrag.
- [x] 3.5 TipTap-editor: enkel preset (tekst, markdown, lenker). Send `create_node`-intensjon til maskinrommet ved submit.
- [x] 3.6 Sanntidstest: åpne to faner, skriv i én, se noden dukke opp i den andre via STDB.

## Fase 4: Tilgangskontroll

- [x] 4.1 `recompute_access` i maskinrommet: ved edge-endring, oppdater `node_access`-matrisen. Håndter direkte edges (owner, admin, member, reader).
- [x] 4.2 Team-transitivitet: member_of-edge til team → arv tilgang fra teamets edges.
- [x] 4.3 Visibility-filtrering: STDB-spørringer respekterer visibility-enum. Frontend ser bare noder brukeren har tilgang til.
- [x] 4.4 RLS-policies på PG: `node_access`-basert filtrering for tunge spørringer.

## Fase 5: Kommunikasjonsnoder

- [x] 5.1 Opprett kommunikasjonsnode: intensjon `create_communication` → node med `node_kind='communication'`, deltaker-edges, metadata (started_at).
- [x] 5.2 Kontekst-arv: input i kommunikasjonsnode → automatisk `belongs_to`-edge.
- [x] 5.3 Chat-visning i frontend: noder med `belongs_to`-edge til kommunikasjonsnode, sortert på tid, sanntid via STDB.
- [x] 5.4 Én-til-én chat: opprett kommunikasjonsnode med to deltakere. Full loop: skriv melding → vis i sanntid hos begge.

## Fase 6: CAS og mediefiler

- [x] 6.1 CAS-lagring: filsystem med content-addressable hashing (SHA-256). Katalogstruktur med hash-prefix. Deduplisering.
- [x] 6.2 Upload-endepunkt: `POST /intentions/upload_media` → hash fil, lagre i CAS, opprett media-node med `has_media`-edge.
- [x] 6.3 Serving: `GET /cas/{hash}` → stream fil fra disk. Caddy kan serve direkte for ytelse.
- [x] 6.4 Bilder i TipTap: drag-and-drop/paste → upload → CAS-node → inline i `metadata.document` via `node_id`.

## Fase 7: Lyd-pipeline

- [x] 7.1 faster-whisper oppsett: Docker-container, GPU hvis tilgjengelig, norsk modell. Ref: `docs/erfaringer/`.
- [x] 7.2 Transkripsjons-pipeline: lydfil i CAS → maskinrommet trigger Whisper → resultat i `content`-feltet.
- [x] 7.3 Voice memo i frontend: opptak-knapp i input-komponenten → upload → CAS → transkripsjon.
- [x] 7.4 Lyd-avspilling: spiller av original lyd fra CAS-node. Waveform-visning.
- [x] 7.5 Segmenttabell-migrasjon: opprett `transcription_segments`-tabell i PG. Oppdater `transcribe.rs` til SRT-format → parse → skriv segmenter. Miljøvariabler: `WHISPER_MODEL` (default "medium"), `WHISPER_INITIAL_PROMPT`. Ref: `docs/concepts/podcastfabrikken.md` § 3.
- [x] 7.6 Transkripsjonsvisning i frontend: segmenter med tidsstempler, avspillingsknapp per segment (hopper til riktig sted i lydfilen), redigerbare tekstfelt (setter `edited = true`). Universell komponent for podcast, møter, voice memos.
- [x] 7.7 Re-transkripsjonsflyt: ved ny transkripsjon, vis side-om-side med forrige versjon. Highlight manuelt redigerte segmenter fra forrige versjon. Bruker velger per segment.
- [x] 7.8 SRT-eksport: generer nedlastbar SRT-fil fra `transcription_segments`-tabellen.

## Fase 8: Aliaser

- [x] 8.1 Alias-noder: opprett alias-node med `alias`-edge (system=true) fra hovednoden. Usynlig for traversering.
- [x] 8.2 Kontekstbasert identitet: maskinrommet setter `created_by` til alias-node når brukeren opererer i kontekst der aliaset er vert/deltaker.

## Fase 9: Flere visninger

- [x] 9.1 Kanban-visning: noder med board-edge, gruppert på status-edge. Drag-and-drop for statusendring.
- [x] 9.2 Kalender-visning: noder med `scheduled`-edge, på tidslinje.
- [x] 9.3 Dagbok-visning: private noder (ingen delte edges), sortert på tid.
- [x] 9.4 Kunnskapsgraf: topic-noder, `mentions`-edges. Visuell graf-visning.

## Fase 10: AI og beriking

- [x] 10.1 LiteLLM oppsett: Docker-container, API-nøkler, modell-routing. Ref: `docs/infra/ai_gateway.md`.
- [x] 10.2 AI-foreslåtte edges: maskinrommet sender innhold til LLM → foreslår mentions, topics.
- [x] 10.3 Oppsummering: kommunikasjonsnode → AI-generert sammendrag som ny node.
- [x] 10.4 TTS: tekst → lyd via ElevenLabs. Mottaker-preferanse i metadata.

## Fase 11: Produksjons-pipeline

- [x] 11.1 LiveKit oppsett: Docker-container for WebRTC. Ref: `docs/setup/produksjon.md`.
- [x] 11.2 Sanntidslyd: kommunikasjonsnode med live-status → LiveKit-rom for deltakere.
- [x] 11.3 Pruning-logikk: TTL per modalitet, signaler som forlenger levetid, disk-nødventil.
- [x] 11.4 Podcast-RSS: samlings-node med publiserings-edges → generert RSS-feed.

## Fase 13: Trait-system

- [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.
- [x] 13.4 Trait-administrasjon: admin-UI for å legge til/fjerne traits på eksisterende samlinger med konfigurasjon per trait.

## Fase 14: Publisering

- [x] 14.1 Tera-templates: innebygde temaer (avis, magasin, blogg, tidsskrift) med Tera i Rust. Artikkelmal + forside-mal per tema. CSS-variabler for theme_config-overstyring. Ref: `docs/concepts/publisering.md` § "Temaer".
- [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.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.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.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.
- [x] 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.
- [x] 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).
- [x] 14.9 Custom domains: bruker registrerer domene i `publishing`-trait. Maskinrommet validerer DNS, Caddy on-demand TLS med validerings-callback. Re-rendring med riktig canonical URL.
- [x] 14.10 Redaksjonell innsending: `submitted_to`-edge med status-metadata (`pending`, `in_review`, `revision_requested`, `rejected`, `approved`). Maskinrommet validerer at kun roller i `submission_roles` kan opprette `submitted_to`, og kun owner/admin kan endre status eller opprette `belongs_to`. Ref: `docs/concepts/publisering.md` § "Innsending".
- [x] 14.11 Redaktørens arbeidsflate: frontend-visning av noder med `submitted_to`-edge til samling, gruppert på status. Kanban-stil drag-and-drop for statusendring. Siste kolonne ("Planlagt") setter `publish_at` i edge-metadata.
- [x] 14.12 Planlagt publisering: maskinrommet sjekker periodisk (cron/intervall) for `belongs_to`-edges med `publish_at` i fortiden som ikke er rendret. Ved treff: render HTML → CAS → oppdater RSS.
- [x] 14.13 Redaksjonell samtale: ved innsending kan redaktør opprette kommunikasjonsnode knyttet til artikkel + forfatter for diskusjon/feedback utover kort notat i edge-metadata.
- [x] 14.14 Bulk re-rendering: batch-jobb via jobbkø ved temaendring. Paginert (100 artikler om gangen), oppdaterer `renderer_version`. Artikler serveres med gammelt tema til re-rendret.
- [x] 14.15 Dynamiske sider: kategori-sider (filtrert på tag-edges), arkiv (kronologisk med månedsgruppering), søk (PG fulltekst). Alle paginerte, cachet i maskinrommet. Om-side som statisk CAS-node.
- [x] 14.16 Presentasjonselementer som noder: publisert tittel, ingress, OG-bilde, undertittel er egne noder med `title`/`summary`/`og_image`-edges til artikkelen. Frontend for å opprette/redigere varianter. Ref: `docs/concepts/publisering.md` § "Presentasjonselementer".
- [x] 14.17 A/B-testing: maskinrommet roterer varianter ved forside-rendering, logger impressions/klikk per variant, normaliserer CTR mot tidspunkt-baseline. Etter statistisk signifikans markeres vinner. Redaktør kan overstyre. Edge-metadata: `ab_status`, `impressions`, `clicks`, `ctr`.

## Fase 15: Adminpanel

- [x] 15.1 Systemvarsler: varslingsnode (`node_kind='system_announcement'`) med type (info/warning/critical), nedtelling og utløp. Frontend viser banner/toast for alle aktive klienter via STDB. Ref: `docs/concepts/adminpanelet.md`.
- [x] 15.2 Graceful shutdown: admin setter vedlikeholdstidspunkt → nedtelling i frontend → nye LiveKit-rom blokkeres → jobbkø stopper → vent på aktive jobber → restart. Vis aktive sesjoner før bekreftelse.
- [x] 15.3 Jobbkø-oversikt: admin-UI for aktive, ventende og feilede jobber. Filtrer på type/samling/status. Manuell retry og avbryt.
- [x] 15.4 AI Gateway-konfigurasjon: admin-UI for modelloversikt, API-nøkler (kryptert), ruting-regler per jobbtype, fallback-kjeder, forbruksoversikt per samling. Ref: `docs/infra/ai_gateway.md`.
- [x] 15.5 Ressursstyring: prioritetsregler mellom jobbtyper, ressursgrenser per worker, ressurs-governor for automatisk nedprioritering under aktive LiveKit-sesjoner, disk-status med varsling.
- [x] 15.6 Serverhelse-dashboard: tjeneste-status (PG, STDB, Caddy, Authentik, LiteLLM, Whisper, LiveKit), metrikker (CPU, minne, disk), backup-status, logg-tilgang.
- [x] 15.7 Ressursforbruk-logging: `resource_usage_log`-tabell i PG. Maskinrommet logger AI-tokens (inn/ut, modellnivå), Whisper-tid (sek), TTS-tegn, CAS-lagring (bytes), LiveKit-tid (deltaker-min). Båndbredde via Caddy-logg-parsing. Ref: `docs/features/ressursforbruk.md`.
- [x] 15.8 Forbruksoversikt i admin: aggregert visning per samling, per ressurstype, per tidsperiode. Drill-down til jobbtype og modellnivå.
- [x] 15.9 Brukersynlig forbruk: hver bruker ser eget forbruk i profil/innstillinger. Per-node forbruk synlig i node-detaljer for eiere.

## Fase 16: Lydmixer

Ref: `docs/features/lydmixer.md`

- [x] 16.1 LiveKit-klient i frontend: installer `livekit-client`, koble til rom, vis deltakerliste. Deaktiver LiveKit sin auto-attach av `<audio>`-elementer — lyd rutes gjennom Web Audio API i stedet.
- [x] 16.2 Web Audio mixer-graf: opprett `AudioContext`, `MediaStreamSourceNode` per remote track → per-kanal `GainNode` → master `GainNode` → `destination`. `AnalyserNode` per kanal for VU-meter.
- [x] 16.3 Mixer-UI (MixerTrait-komponent): kanalstripe per deltaker med volumslider (0–150%), nød-mute-knapp (stor, rød), VU-meter (canvas/CSS), navnelabel. Master-fader og master-mute. Responsivt design (mobil: kompakt fader-modus).
- [x] 16.4 Delt mixer-kontroll via SpacetimeDB: `MixerChannel`-tabell + reducers (`set_gain`, `set_mute`, `toggle_effect`). Frontend abonnerer og oppdaterer Web Audio-graf ved endring fra andre deltakere. Visuell feedback (sliders beveger seg i sanntid). Tilgangskontroll: eier/admin kan sette deltaker til viewer-modus.
- [x] 16.5 Sound pads: pad-grid UI (4×2), forhåndslast lydfiler fra CAS til `AudioBuffer`. Avspilling ved trykk (`AudioBufferSourceNode`). Pad-konfig i `metadata.mixer.pads` (label, farge, cas_hash). Synkronisert avspilling via LiveKit Data Message.
- [x] 16.6 EQ-effektkjede: fat bottom (`BiquadFilterNode` lowshelf ~200Hz), sparkle (`BiquadFilterNode` highshelf ~10kHz), exciter (`WaveShaperNode` + highshelf). Per-kanal toggles, synkronisert via STDB. Presets (podcast-stemme, radio-stemme).
- [x] 16.7 Stemmeeffekter: robotstemme (ring-modulasjon: `OscillatorNode` → `GainNode.gain`), monsterstemme (egenutviklet `AudioWorkletProcessor` med phase vocoder for pitch shift). Effektvelger-UI per kanal. Parameterjustering (pitch-faktor, oscillator-frekvens).

## Fase 17: Lydstudio-utbedring

Ref: Kodegjennomgang av `b4c4bb8` (Lydstudio: lydredigering via FFmpeg).

- [x] 17.1 Responsivt studio-layout: `/studio/[id]` sidebar stacker under waveform på mobil. Verktøypanel som modal/sheet på små skjermer. Ref: feedback om at alt UI skal være responsivt uten unntak.
- [x] 17.2 FFmpeg-parametervalidering: valider at alle numeriske verdier (threshold, gain, ratio, frekvenser) er innenfor sikre grenser i `audio.rs` før de interpoleres i filterstrenger. Avvis ugyldige verdier med feilmelding.
- [x] 17.3 Fade/silence-logikk: fiks negativ fade-out start (clamp til 0), og adaptiv silence-margin (margin skal ikke overstige halve regionens varighet). Gi feilmelding ved ugyldige fade-varigheter.
- [x] 17.4 Frontend input-begrensninger: legg til `min`/`max` på alle tallfelter i OperationPanel (silenceThreshold, fadeMs, normTarget, compRatio). Hindre ugyldig input.
- [x] 17.5 Job-polling opprydding: rydd opp interval/timeout ved navigering bort fra studio-siden. Vis feilmelding etter N mislykkede polling-forsøk. Wrap metadata JSON.parse i try/catch.
- [x] 17.6 Temp-fil opprydding: legg til periodisk jobb i maskinrommet som sletter gamle temp-filer i CAS tmp-katalog. Bruk `/tmp` eller sett TTL.
- [x] 17.7 FFmpeg feilmeldinger til bruker: propager stderr fra FFmpeg-feil til frontend via strukturert feilrespons. Vis i RenderDialog.

## Fase 18: AI-verktøy (arbeidsflate)

Ref: `docs/features/ai_verktoy.md`, `docs/retninger/arbeidsflaten.md`

- [x] 18.1 AI-preset node-type: `node_kind: 'ai_preset'` med metadata (prompt, model_profile, category, icon, color). Maskinrommet validerer ved opprettelse. Seed standardprompter (rens tekst, korrektør, oppsummering, oversett, skriv om, trekk ut fakta, forenkle, endre tone).
- [x] 18.2 AI-prosessering endepunkt: `POST /intentions/ai_process` med source_node_id, ai_preset_id, direction (node_to_tool / tool_to_node). Maskinrommet henter kilde-content og preset-prompt, mapper modellprofil → LiteLLM-alias, sender til AI Gateway. Logg forbruk i ai_usage_log.
- [x] 18.3 Direction-logikk: `tool_to_node` → lagre original som revisjon, oppdater node content. `node_to_tool` → opprett ny node med AI-output, opprett `derived_from`-edge til kilde + `processed_by`-edge til AI-preset.
- [x] 18.4 AI-verktøy panel (frontend): Svelte-komponent for arbeidsflaten. Prompt-velger med standardprompter, fritekst-felt for egendefinert prompt, modell-indikator (readonly). Drag-and-drop mottak for tekstnoder.
- [x] 18.5 Drag-and-drop integrasjon: node → verktøy (ny node), verktøy → node (in-place revisjon). Drop-sone feedback med verktøyets farge. Inkompatibilitet for lyd/bilde-noder med forklaring.
- [x] 18.6 Egendefinerte presets: brukere kan opprette egne AI-preset-noder med custom prompt. Dele via edges til samling/team. Modellprofil satt av admin.

## Fase 19: Arbeidsflaten — Spatial Canvas

Ref: `docs/retninger/arbeidsflaten.md`, `docs/features/canvas_primitiv.md`

- [x] 19.1 Canvas-primitiv Svelte-komponent: pan/zoom kamera med CSS transforms, viewport culling, pointer events (mus + touch), snap-to-grid (valgfritt), fullskjermsmodus. Ref: `docs/features/canvas_primitiv.md`.
- [x] 19.2 BlockShell wrapper-komponent: header med tittel + fullskjerm/resize/lukk-knapper, drag-handles for repositionering, resize-handles, drop-sone rendering (highlight ved drag-over). Responsivt (min-size, max-size).
- [x] 19.3 Arbeidsflaten layout: skriv om `/collection/[id]` fra vertikal stack til Canvas + BlockShell. Last brukerens lagrede arrangement eller bruk defaults fra samlingens traits. Persist arrangement i bruker-edge metadata. Desktop: spatial canvas, mobil: stacked/tabs. Ref: `docs/retninger/arbeidsflaten.md` § "Tre lag".
- [x] 19.4 Kontekst-header: header tilhører flaten, viser gjeldende node som nedtrekksmeny/kontekst-velger. Mest brukte noder øverst (frekvens/recency), søkbart. Verktøymeny for å instansiere nye paneler. Ref: `docs/retninger/arbeidsflaten.md` § "Kontekst-header".
- [x] 19.5 Snarveier: paneler kan minimeres til kompakt ikon/fane. Dobbeltklikk → minimer/gjenopprett. Bevarer posisjon og størrelse. Ref: `docs/retninger/arbeidsflaten.md` § "Snarveier".
- [x] 19.6 Personlig flate: brukerens standard arbeidsflate (node_kind: 'workspace'). Vises når ikke koblet til en annen node. Persistent layout.

## Fase 20: Universell overføring + panelrework

Ref: `docs/features/universell_overfoering.md`, `docs/retninger/arbeidsflaten.md` § "Kompatibilitetsmatrise"

- [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.
- [x] 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".
- [x] 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.
- [x] 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.
- [x] 20.5 Panelrework — Chat: gjør ChatTrait til fullverdig BlockShell-panel med BlockReceiver, fullskjerm-toggle, og responsivt design innenfor begrenset container.
- [x] 20.6 Panelrework — Kanban: gjør KanbanTrait til BlockShell-panel med drag-and-drop aksept fra andre paneler, fullskjerm, responsivt.
- [x] 20.7 Panelrework — Kalender: gjør CalendarTrait til BlockShell-panel med drop-aksept for scheduling, fullskjerm, responsivt.
- [x] 20.8 Panelrework — Editor/Artikkelverktøy: gjør artikkelverktøy til BlockShell-panel med source_material mottak fra andre paneler. Ref: `docs/features/artikkelverktoy.md`.
- [x] 20.9 Panelrework — Studio: gjør StudioTrait til BlockShell-panel med drop-aksept for lydfiler, fullskjerm, responsivt.

## Fase 21: CLI-verktøy — Unix-filosofi

Ref: `docs/retninger/unix_filosofi.md`. Bryt ut prosessering fra maskinrommet til
standalone CLI-verktøy i `tools/`. Maskinrommet kaller dem fra jobbkøen, Claude
kaller dem direkte. Samme verktøy, to brukere.

### Prosessering (erstatter jobbkø-handlere)

- [x] 21.1 `synops-transcribe`: Whisper-transkribering. Input: `--cas-hash <hash> --model <model> [--initial-prompt <tekst>]`. Output: JSON med segmenter. Skriver segmenter til PG, oppdaterer node metadata. Erstatter `transcribe.rs`.
- [x] 21.2 `synops-audio`: FFmpeg-prosessering. Input: `--cas-hash <hash> --edl <json>`. Output: ny CAS-hash. Erstatter `audio.rs`. Inkluder parametervalidering (fase 17.2–17.3).
- [x] 21.3 `synops-render`: Tera HTML-rendering. Input: `--node-id <uuid> --theme <tema>`. Output: CAS-hash for rendret HTML. Erstatter `publishing.rs`.
- [x] 21.4 `synops-rss`: RSS/Atom-generering. Input: `--collection-id <uuid>`. Output: XML til stdout. Erstatter `rss.rs`.
- [x] 21.5 `synops-tts`: Tekst-til-tale. Input: `--text <tekst> --voice <stemme>`. Output: CAS-hash for lydfil. Erstatter `tts.rs`.
- [x] 21.6 `synops-summarize`: AI-oppsummering. Input: `--communication-id <uuid>`. Output: sammendrag som tekst. Erstatter `summarize.rs`.
- [x] 21.7 `synops-suggest-edges`: AI-foreslåtte edges. Input: `--node-id <uuid>`. Output: JSON med forslag (target, edge_type, confidence). Erstatter `ai_edges.rs`.
- [x] 21.8 `synops-respond`: Claude chat-svar. Input: `--communication-id <uuid> --message-id <uuid>`. Output: svartekst. Erstatter `agent.rs` sin prosessering (auth/ratelimit forblir i maskinrommet).
- [x] 21.9 `synops-prune`: Opprydding av gamle noder. Input: `--dry-run` for forhåndsvisning. Erstatter `pruning.rs`.

### Oppslag (Claude-verktøy)

- [x] 21.10 `synops-context`: Hent kontekst for en samtale. Input: `--communication-id <uuid>`. Output: markdown med spec, deltakere, historikk, relaterte noder.
- [x] 21.11 `synops-search`: Fulltekstsøk i grafen. Input: `<query> [--kind <node_kind>] [--limit N]`. Output: matchende noder med utdrag.
- [x] 21.12 `synops-tasks`: Parse tasks.md og vis status. Input: `[--phase N] [--status todo|done|blocked]`. Output: formatert oppgaveliste.
- [x] 21.13 `synops-feature-status`: Sjekk feature-status. Input: `<feature_key>`. Output: spec-sammendrag, oppgavestatus, nylige commits, ubesvart feedback.
- [x] 21.14 `synops-node`: Hent/vis en node med edges. Input: `<uuid> [--depth N] [--format json|md]`. Output: node-data med edges.

### Infrastruktur

- [x] 21.15 Jobbkø-dispatcher: endre maskinrommets jobbkø-handlere til å spawne CLI-verktøy (`Command::new("synops-X")`) i stedet for inline-kode. Stdout → jobbresultat, stderr → feillogg, exitkode → status.
- [x] 21.16 Felles lib: `synops-common` crate med PG-tilkobling, CAS-helpers, og node/edge-typer. Deles mellom alle CLI-verktøy for å unngå duplisering.

## Fase 12: Herding

- [x] 12.1 Observerbarhet: strukturert logging, metrikker (request latency, queue depth, AI cost).
- [x] 12.2 Backup: PG-dump rutine.
- [x] 12.3 Feilhåndtering: retry med backoff i skrivestien, dead letter queue for feilede PG-skrivinger.
- [x] 12.4 Ytelse: profiler PG-spørringer, optimaliser node_access-oppdatering.

## Fase 22: SpacetimeDB-migrering — PG LISTEN/NOTIFY

Ref: `docs/retninger/datalaget.md` (revidert mars 2026). Faser ut SpacetimeDB
til fordel for PG LISTEN/NOTIFY + WebSocket i portvokteren. Én datakilde,
ingen synk-kompleksitet.

- [x] 22.1 WebSocket-lag i portvokteren: implementer PG LISTEN/NOTIFY-lytter og WebSocket-endepunkt. Legg til PG-triggers (`notify_node_change`, `notify_edge_change`) for nodes og edges. Frontend kobler til begge (STDB + nytt WS) i parallell for verifisering.
- [x] 22.2 Frontend-migrering: erstatt SpacetimeDB-klient med vanlig WebSocket til portvokteren. Erstatt STDB-stores med reaktive stores som lytter på WebSocket. Verifiser all sanntidsfunksjonalitet (chat, kanban, kalender, mixer, canvas).
- [x] 22.3 Fjern STDB-skrivestien: portvokteren slutter å skrive til SpacetimeDB. All skriving går kun til PG. NOTIFY-triggere er eneste push-mekanisme. Verifiser at ingenting avhenger av STDB-data.
- [x] 22.4 Fjern SpacetimeDB: stopp Docker-container, fjern STDB-modul, fjern STDB-klient fra portvokteren og frontend, fjern synkroniseringskode, oppdater docs og CLAUDE.md.
- [x] 22.5 Opprydding: arkiver STDB-relaterte erfaringsdocs, oppdater alle docs-referanser, fjern Docker-konfig for SpacetimeDB, fjern SpacetimeDB-loven fra feedback-memories.

## Fase 23: Validering — test og kvalitetssikring per fase

Hver oppgave spinner opp en ny sesjon med frisk kontekst som gjennomgår
implementeringen fra den angitte fasen. Sesjonen tester, leser kode,
verifiserer mot spec, og sjekker at ting faktisk fungerer. Ved funn:
fiks direkte hvis det er småting, eller opprett nye work_items (tasks)
med spesifikasjon for det som trenger en dedikert sesjon.

- [x] 23.1 Valider fase 1–2 (infra + maskinrommet): PG-skjema, indekser, auth-middleware, intensjoner, STDB-klient (nå erstattet av WS). Verifiser at skjema matcher docs, at auth fungerer, at skrivestien er konsistent.
- [x] 23.2 Valider fase 3–4 (frontend + tilgang): SvelteKit-oppsett, OIDC-flow, sanntid, mottaksflate, TipTap-editor, node_access-matrise, team-transitivitet, visibility-filtrering.
- [x] 23.3 Valider fase 5–8 (kommunikasjon + CAS + lyd + aliaser): chat-loop, kontekst-arv, CAS-hashing/deduplisering, Whisper-pipeline, segmenttabell, SRT-eksport, alias-identitet.
- [x] 23.4 Valider fase 9–10 (visninger + AI): kanban drag-and-drop, kalender, dagbok, kunnskapsgraf, LiteLLM-ruting, AI-foreslåtte edges, oppsummering, TTS.
- [x] 23.5 Valider fase 11 (produksjon): LiveKit-oppsett, sanntidslyd, pruning-logikk, podcast-RSS.
- [x] 23.6 Valider fase 13–14 (traits + publisering): trait-validering, pakkevelger, Tera-templates, HTML-rendering, forside, slot-håndtering, redaksjonell flyt, planlagt publisering, A/B-testing.
- [x] 23.7 Valider fase 15–16 (admin + lydmixer): systemvarsler, graceful shutdown, jobbkø-oversikt, ressursstyring, serverhelse, Web Audio mixer, delt kontroll, sound pads, EQ, stemmeeffekter.
- [x] 23.8 Valider fase 17–18 (lydstudio-utbedring + AI-verktøy): responsivt layout, FFmpeg-validering, fade/silence, AI-presets, direction-logikk, drag-and-drop integrasjon.
- [x] 23.9 Valider fase 19–20 (arbeidsflaten + universell overføring): canvas pan/zoom, BlockShell, layout-persistering, snarveier, transfer service, alle panelreworks (chat, kanban, kalender, editor, studio).
- [x] 23.10 Valider fase 21 (CLI-verktøy): kjør hvert synops-*-verktøy, verifiser --help, --payload-json, output-format, feilhåndtering, synops-common integrasjon.
- [x] 23.11 Valider fase 22 (STDB-migrering): WebSocket-sanntid fungerer, PG LISTEN/NOTIFY-triggere, ingen STDB-rester i aktiv kode/konfig.

## Fase 24: Orkestrering — trigger-drevne automatiseringer

Ref: `docs/concepts/orkestrering.md`. Orkestreringsnoder (`node_kind: 'orchestration'`)
med fritekst-instruksjoner som boten utfører via function calling. Strukturerte triggere,
automatisk eskalering av intelligens ved feil, kompilering av velprøvde mønstre.

- [x] 24.1 Orchestration node-type: legg til `orchestration` i maskinrommets node-validering. Metadata-skjema: `trigger` (event + conditions), `executor`, `intelligence`, `effort`, `compiled`, `pipeline`. Valider trigger-events mot kjent liste.
- [x] 24.2 Trigger-evaluering i portvokteren: ved node/edge-events, sjekk om noen `orchestration`-noder matcher triggeren. Effektiv lookup (indeksert på `metadata.trigger.event`). Ingen LLM-kall for trigger-matching.
- [x] 24.3 Script-kompilator: parser menneskelig scriptspråk ("transkriber lydfilen (stor modell)") og kompilerer til tekniske CLI-kall. Matcher verb mot `cli_tool`-noders `aliases`, argumenter mot `args_hints`, variabler fra trigger-kontekst. Rust-stil kompileringsfeil med forslag.
- [x] 24.4 cli_tool alias-metadata: utvid alle `cli_tool`-noder med `aliases` (norske verb) og `args_hints` (menneskelige argumenter → CLI-flagg). Seed for alle eksisterende verktøy.
- [x] 24.5 Script-executor: vaktmesteren parser kompilert script og eksekverer steg sekvensielt via generisk dispatch. VED_FEIL-håndtering. Logger i `orchestration_log`.
- [x] 24.6 Orchestration UI: editor med tre visninger (Enkel/Teknisk/Kompilert) som tabber. Sanntids kompileringsfeil. Trigger-velger, "Test kjøring"-knapp, kjørehistorikk. Ref: `docs/concepts/orkestrering.md`.
- [x] 24.7 AI-assistert oppretting: `synops-ai` med auto-generert systemprompt (fra cli_tool-noder) foreslår script fra fritekst-beskrivelse. Vaktmesteren validerer. Eventually-modus: lagre som work_item for Claude Code.
- [x] 24.8 Kaskade: `triggers`-edge mellom orkestreringer. Output fra én trigger neste. Syklusdeteksjon for å unngå uendelige loops.
- [x] 24.9 Seed-orkestreringer: opprett standard-orkestreringer for podcast-pipeline, publiseringsflyt, og AI-beriking basert på eksisterende hardkodet logikk i vaktmesteren. Skrives i menneskelig scriptspråk.

## Fase 25: Web Clipper — `synops-clip`

Ref: `docs/proposals/web_clipper.md`. CLI-verktøy som henter URL, parser med
Readability, og oppretter innholdsnode med AI-beriking. Brukes av @bot i chat
("les denne artikkelen"), orkestreringer, og fremtidig browser-extension.

- [x] 25.1 `synops-clip` CLI: hent URL, parse med Readability (mozilla/readability via JS eller Rust-port), returner ren tekst + metadata (tittel, forfatter, dato, ingress). Fallback til headless browser (Playwright) for JS-rendrede sider. Detekter betalingsmur (kort/avkuttet innhold, "logg inn for å lese", kjente paywall-mønstre) — returner `"paywall": true` og tilgjengelig innhold (ingress/utdrag). Output: JSON med `title`, `author`, `date`, `content`, `url`, `paywall`.
- [x] 25.2 Node-opprettelse: `synops-clip --write` oppretter `content`-node med artikkelinnhold, `metadata.source_url`, og `tagged`-edge "clipped". AI-oppsummering via LiteLLM. `mentions`-edges til gjenkjente entiteter i kunnskapsgrafen.
- [x] 25.3 @bot-integrasjon: bruker limer inn URL i chat → boten gjenkjenner URL, kaller `synops-clip`, presenterer oppsummering i chatten, oppretter node i bakgrunnen. Ved paywall: "Denne artikkelen er bak betalingsmur. Jeg fikk med tittel og ingress — lim inn innholdet om du vil dele resten."
- [x] 25.4 Orkestrering-støtte: `synops-clip` tilgjengelig som verktøy i orkestreringer. F.eks. "Clip alle URL-er som deles i #Redaksjonen og oppsummer dem".

## Fase 26: Epost — send og motta via synops.no

Vaktmesteren kan sende epost (msmtp) og motta epost (Postfix → synops-mail).
Brukernavn@domene ruter til brukerens innboks. Alle domener (synops.no,
sidelinja.org, vegard.info) ruter til samme bruker basert på username.

- [x] 26.1 Username i auth_identities: legg til `username`-kolonne, populer fra Authentik `preferred_username` ved login. Unik constraint. Oppdater auth-callback i SvelteKit til å lagre username.
- [?] 26.2 msmtp oppsett: konfigurer utgående epost via SMTP-relay. Avsender: `vaktmester@synops.no`. Tilgjengelig som `synops-mail --send --to <epost> --subject <emne>` CLI-verktøy.
  > Spørsmål: Trenger SMTP-relay credentials. Hetzner-relay (mail.your-server.de:587) krever autentisering, og port 25 er blokkert utgående. Verktøyet (`synops-mail`) og msmtp-config er ferdig — mangler kun brukernavn/passord i `/srv/synops/config/msmtp/msmtprc`. Alternativer: (1) Hetzner Robot-konto credentials, (2) ekstern SMTP-relay (Brevo, Mailgun, etc.), (3) annen tilnærming?
  > Kontekst: Alt er implementert og testet (CLI kompilerer, msmtp kobler til relay, auth feiler pga placeholder-credentials). Trenger bare ekte SMTP-bruker/passord.
- [ ] 26.3 MX-records: sett opp MX for synops.no, sidelinja.org, vegard.info som peker til serveren.
- [ ] 26.4 Postfix minimal: installer Postfix som lokal MTA kun for mottak. Ingen relay, ingen kø for utgående. Pipe innkommende epost til `synops-mail --receive`.
- [ ] 26.5 `synops-mail --receive`: Rust CLI som leser raw epost fra stdin. Sjekk 1: avsender-epost matcher `auth_identities.email`? Sjekk 2: innhold starter med "Kjære vaktmester" (eller konfigurerbar frase)? Begge må matche. Opprett `content`-node i brukerens innboks med epostinnholdet. Alt annet → /dev/null, ingen bounce.
- [ ] 26.6 Domene-alias: `vegard@synops.no`, `vegard@sidelinja.org`, `vegard@vegard.info` ruter alle til samme bruker via username-oppslag i PG. Domenet er irrelevant.
- [ ] 26.7 Utgående varsler: vaktmesteren kan sende epost-varsler til brukere (ny oppgave tildelt, innsendt artikkel godkjent, etc.) via `synops-mail --send`. Konfigurerbart per bruker i metadata.preferences.

## Fase 27: Tankekart — radial grafvisning

Ref: `docs/features/tankekart.md`. Tankekart-panel som viser noder og edges
i radial layout med en rotnode i sentrum. Ingen ny backend — ren frontend-
visning av eksisterende grafdata.

- [ ] 27.1 MindMap Svelte-komponent: radial/tree-layout av noder rundt en rotnode. Hent relaterte noder (1-2 hopp) via WebSocket. d3-hierarchy eller trigonometri for layout. Pan/zoom via canvas-primitiv. Klikk node = ny rot, dobbeltklikk = åpne i editor.
- [ ] 27.2 BlockShell-panel: MindMap som BlockShell-panel i arbeidsflaten med fullskjerm, resize, drag-handle. Rotnode fra kontekst-header. Responsivt.
- [ ] 27.3 MindMap-trait: `mindmap`-trait for samlingsnoder. Vises i trait-velger ved opprettelse. Konfigurasjon: default dybde (1-3 hopp), layout-stil (radial/tree).

## Fase 28: Manglende CLI-verktøy + AI-rutingskontroll

Verktøy som mangler i verktøykassen, pluss admin-styring av hvilken
modell som brukes til hva.

### synops-ai: lettvekts LLM-kall

- [ ] 28.1 `synops-ai` CLI: direkte LLM-kall via LiteLLM. Input: `--prompt <tekst> [--model <alias>] [--system <systemprompt>]`. Output: tekst til stdout. Ingen fillesing, ingen verktøy, bare prompt inn/ut. Bruker `ai_job_routing`-tabellen for å bestemme modell hvis `--model` ikke er satt. Logger i `ai_usage_log`.
- [ ] 28.2 AI-rutingskontroll i admin: utvid admin-UI (fase 15.4) med konfigurasjon av hvilken modell som brukes per kontekst. Tabellen `ai_job_routing` mapper `(job_type, context)` → `model_alias`. Kontekster: `orchestration_script`, `orchestration_dream`, `bot_chat`, `bot_triage`, `summarize`, `suggest_edges`, `classify`. Admin kan endre uten redeploy.
- [ ] 28.3 Kostnadstak per bruker/samling: `ai_budget`-felt i metadata for brukere og samlinger. `synops-ai` sjekker budsjett mot `ai_usage_log` aggregat før kall. Ved overskridelse: returner feilmelding, opprett work_item.

### Øvrige manglende verktøy

- [ ] 28.4 `synops-notify`: send varsel via epost (synops-mail), WebSocket-push, eller begge. Input: `--to <node_id> --message <tekst> [--channel email|ws|both]`. Brukes av orkestreringer og vaktmesteren.
- [ ] 28.5 `synops-validate`: sjekk at en node matcher forventet skjema for sin node_kind. Input: `--node-id <uuid>`. Output: liste av avvik. Brukes av valideringsfasen og som pre-commit sjekk.
- [ ] 28.6 `synops-backup`: PG-dump + CAS-filiste + metadata-snapshot. Input: `[--full | --incremental]`. Output: backup-sti. Erstatter cron-scriptet fra 12.2.
- [ ] 28.7 `synops-health`: sjekk status for alle tjenester (PG, Caddy, vaktmesteren, LiteLLM, Whisper, LiveKit). Output: JSON med status per tjeneste. Brukes av admin-dashboard og overvåking.

## Fase 29: Universell input — alle modaliteter blir noder

Ref: `docs/features/universell_input.md`. Utvid input-primitiven til å dekke
alle relevante modaliteter. Alt ender som noder. Modaliteten er transport,
noden er det som lever videre.

### Skjermklipp
- [ ] 29.1 Skjermklipp-input: paste screenshot fra clipboard i chat/editor → upload til CAS → media-node. Frontend detekterer bilde-paste (ClipboardEvent). Valgfri AI-beskrivelse via synops-ai ("beskriv dette bildet"). Metadata: `{ "source": "screenshot", "description": "..." }`.

### RSS/Feed-abonnement
- [ ] 29.2 `synops-feed` CLI: abonner på RSS/Atom-feed. Input: `--url <feed-url> --collection-id <uuid> [--interval 30m]`. Poller feed, oppretter `content`-node for nye entries med `metadata.source_url` og `tagged`-edge "feed". AI-oppsummering valgfritt. Paywall-deteksjon gjenbrukt fra synops-clip.
- [ ] 29.3 Feed-orkestrering: standard-orkestrering "Overvåk RSS-feed" som bruker synops-feed. Konfigurerbar per samling. Nye artikler havner i innboks eller direkte i en kanal.

### Webhook (universell ekstern input)
- [ ] 29.4 Webhook-endepunkt i vaktmesteren: `POST /api/webhook/<token>` → opprett node fra JSON-body. Hvert webhook har et unikt token (UUID) knyttet til en `webhook`-node med `belongs_to`-edge til målsamling. Validerer token, oppretter `content`-node med payload i metadata.
- [ ] 29.5 Webhook-admin: UI for å opprette/administrere webhooks. Vis token, mål-samling, siste hendelser, aktivitet-logg. Regenerer token. Deaktiver/slett.
- [ ] 29.6 Webhook-templates: forhåndsdefinerte mappinger for kjente tjenester (GitHub → commits/issues, Slack → meldinger, CI/CD → build-status). Template mapper JSON-felt til node title/content/metadata.

### Video
- [ ] 29.7 Video-opptak i frontend: webcam/skjermopptak via MediaRecorder API → upload til CAS → media-node. Start/stopp-knapp i input-komponenten. Maks varighet konfigurerbar.
- [ ] 29.8 Video-prosessering: `synops-video` CLI for transcode (H.264), thumbnail-generering, og varighet-uttrekk. Input: `--cas-hash <hash>`. Output: ny CAS-hash (trancodet) + thumbnail CAS-hash.

### Geolokasjon
- [ ] 29.9 Lokasjon-input: "Del posisjon"-knapp i input-komponenten → Geolocation API → node med `metadata.location: { "lat": 59.91, "lon": 10.75 }`. Kart-visning i node-detaljer (Leaflet/OpenStreetMap). Valgfritt: reverse geocoding via Nominatim for adresse.

### Håndskrift/tegning
- [ ] 29.10 Tegne-input: enkel canvas-basert tegneflate i input-komponenten. Eksporter som PNG → CAS → media-node. Ikke whiteboard (det er et eget verktøy) — dette er "rask skisse som input", som en post-it.

### Kalender-import
- [ ] 29.11 ICS-import: `synops-calendar` CLI som parser ICS-fil og oppretter noder med `scheduled`-edges. Input: `--file <ics> --collection-id <uuid>`. Duplikatdeteksjon via UID. Oppdatering ved re-import.
- [ ] 29.12 CalDAV-abonnement: abonner på ekstern CalDAV-kalender (Google, Outlook). Poller periodisk, synkroniserer endringer. Som RSS-feed men for kalenderhendelser.

## Fase 30: Podcast-hosting — komplett, uten ekstern avhengighet

Ref: `docs/features/podcast_hosting.md`. Bygg komplett podcast-hosting i Synops.
Ingen castopod, ingen ekstern tjeneste. Import fra eksisterende podcast med
prøveimport-flyt.

### RSS og metadata
- [ ] 30.1 iTunes/Podcasting 2.0 RSS-tags: utvid synops-rss med `<itunes:*>` og `<podcast:*>` namespace. Tags fra samlingens podcast-trait metadata (author, category, explicit, language). Podcast:transcript og podcast:chapters fra eksisterende edges.
- [ ] 30.2 Podcast-trait metadata: utvid podcast-trait med iTunes-felt (itunes_category, itunes_author, explicit, language, redirect_feed). Admin-UI for å redigere.

### Statistikk
- [ ] 30.3 `synops-stats` CLI: parse Caddy access-logger for /media/*-requests. Aggreger nedlastinger per episode per dag. IAB-regler: filtrer bots (user-agent), unik IP per 24t. Output: JSON med episode_id, date, downloads, unique_listeners. `--write` lagrer i PG.
- [ ] 30.4 Statistikk-dashboard: vis nedlastinger per episode, trend over tid, topp-episoder, klienter (Apple/Spotify/andre), geografi. Integrert i admin-panelet.

### Embed-spiller
- [ ] 30.5 Podcast-spiller komponent: Svelte-komponent med artwork, tittel, play/pause, progress, waveform, kapittelmerkering. Responsiv. Serveres som iframe-embed: `synops.no/pub/<slug>/<episode>/player`.

### Import
- [ ] 30.6 `synops-import-podcast` CLI: importer eksisterende podcast fra RSS-feed. Parse metadata, last ned lydfiler/artwork/transkripsjoner til CAS, opprett noder med edges. Duplikatdeteksjon via `<guid>`. `--dry-run` for forhåndsvisning. Idempotent: kjør flere ganger, bare nye episoder importeres.
- [ ] 30.7 Prøveimport-flyt i frontend: "Importer podcast"-wizard i admin. Steg 1: lim inn RSS-URL, vis forhåndsvisning av episoder. Steg 2: importer (kan ta tid for mange episoder). Steg 3: sjekk resultat, sammenlign feeds. Steg 4: re-importer nye episoder når klar. Steg 5: aktiver 301-redirect på gammel host.

### Eksport og redirect
- [ ] 30.8 Feed-redirect: `redirect_feed`-felt i podcast-trait. Når satt: Caddy returnerer 301 for feed-URL. Brukeren kan alltid flytte bort. Admin-UI med én-klikks aktivering og advarsel.
