Files
NSCT-FrontEnd/ADMIN-Frontend.md
2026-09-06 08:46:16 +00:00

28 KiB

NSCT-Frontend — Admin Handbook

1. Übersicht

Das NSCT-Frontend ist eine Single-Page-Application (SPA), die als statische Assets gebaut und über einen Caddy Reverse-Proxy ausgeliefert wird. Die App ist in SvelteKit + TailwindCSS + TypeScript implementiert und bietet eine Dark/Light-Oberfläche zur Interaktion mit dem NSCT-Backend.

Architektur

Browser → Caddy (HTTPS/Reverse-Proxy) → Web-Container (Statische Assets) → NSCT-Backend (API)
  • Build: SvelteKit mit @sveltejs/adapter-static → alle Assets landen in build/
  • Runtime: Node 22 Alpine, portiert auf 3000
  • Proxy: Caddy übernimmt TLS, Security Headers, Compression
  • Auth: API-Key-basiert, persistiert im localStorage des Browsers
  • UI-Pattern: Svelte 5 Stores (writable), TailwindCSS utility-first, Svelte 5 Runes

Features

Feature Beschreibung
Dark/Light Theme Klassen-basiertes Theming über dark-Class auf <html>
API-Key-Auth Login mit API-Key, persistent im localStorage
Share-Links Token-basierte freischaltbare Research-Detailansichten
Sidebar-Historie Liste letzter Researches mit Polling-Status
Markdown-Reports Vollständige Berichte via svelte-markdown
Error-Banner Zentrales Error-System (ContradictionBanner, ErrorBanner)
Polling Automatisches Refetching mit konfigurierbarem Intervall

Deployment

Multi-Stage Docker Build (Node 22 Alpine) → statische Assets → Caddy Reverse-Proxy.


2. Schnellstart

Repository klonen

git clone https://github.com/<org>/NSCT-FrontEnd.git
cd NSCT-FrontEnd

Umgebungsvariablen konfigurieren

# .env kopieren und anpassen
cp .env.example .env

Minimale .env-Konfiguration:

# URL des NSCT Backend API (Docker-Netzwerk)
NSCT_API_BASE_URL=http://nsct-api:8080

# Optional: Domain für HTTPS-Zugriff über Caddy
CADDY_DOMAIN=nsct.example.com

# Standard-Theme: dark | light
DEFAULT_THEME=dark

Stack starten

docker compose up -d

Health-Check

curl http://localhost:3000/health

Erwartete Antwort: HTTP 200 mit JSON-Payload.

Browser öffnen

https://nsct.example.com

3. Caddy + HTTPS Konfiguration

Caddyfile-Struktur

Die Caddyfile (Basis: Caddyfile) definiert zwei Block-Typen:

  1. HTTPS-Block (:443) — Hauptblock mit Security Headers, Compression, Reverse-Proxy
  2. HTTP-Fallback (:80) — Kein HTTPS, Basis-Header (wenn keine Domain gesetzt)
# Block 1: HTTPS (immer aktiv)
:443 {
    reverse_proxy web:3000

    header { ... }          # Security Headers
    encode gzip zstd         # Compression
    log { format json }     # Structured JSON Logs
}

# Block 2: HTTP (ohne HTTPS)
:80 {
    reverse_proxy web:3000
    header { ... }
    encode gzip zstd
}

Let's Encrypt (automatisch)

CADDY_DOMAIN=nsct.example.com

Caddy erstellt automatisch ein TLS-Zertifikat über ACME/Let's Encrypt. Der Container muss port 443 erreichen können und die DNS-Auflösung für nsct.example.com muss auf den Server zeigen.

Zertifikate werden persistiert im Volume caddy_data gespeichert (Pfad: /data/caddy/certificates).

HTTP-Fallback (ohne Domain)

Ohne CADDY_DOMAIN läuft Caddy nur auf Port 80 (:80 Block). Keine TLS, kein Redirect.

Security Headers

Header Wert Zweck
X-Content-Type-Options nosniff Verhindert MIME-Type-Sniffing
X-Frame-Options DENY Keine Einbettung in iframes
X-XSS-Protection 1; mode=block XSS-Protection (legacy)
Content-Security-Policy Default-Self + inline-src Resource-Whitelisting
Referrer-Policy no-referrer-when-downgrade Referrer-Kontrolle
Permissions-Policy camera=(), microphone=(), geolocation=() Feature-Blocking
X-Permitted-Cross-Domain-Policies none Cross-Domain-Restriction
X-DNS-Prefetch-Control off DNS-Prefetch deaktivieren

Compression

encode gzip zstd
gzip 1    # Minimaler Kompressionslevel

Rate-Limiting (optional)

NSCT_RATE_LIMIT=100/second

Wenn gesetzt, aktiviert Caddy ein Rate-Limiting. Konfiguration erfolgt via Caddy-Rate-limit-Plugin (nicht im Standard-Image enthalten). Für Produktionsbetrieb empfohlen: dediziertes Rate-Limiting über einen separaten Reverse-Proxy (z.B. Traefik/HAProxy).

DNS-Konfiguration

Für externen Zugriff muss die Domain auf den öffentlichen Server-IP-Adresse verweisen:

nsct.example.com    A    203.0.113.1

Port 80 und 443 müssen im Firewall-Regelwerk freigeschaltet sein.


4. Environment-Variablen

Web-Container (web service)

Variable Beschreibung Default Beispiel
NSCT_API_BASE_URL URL des NSCT-Backend-API http://nsct-api:8080 http://nsct-api:8080
DEFAULT_THEME Start-Theme dark light
NODE_ENV Laufzeit-Modus production (fest im Dockerfile)
HOST Bind-Adresse 0.0.0.0 (fest im Dockerfile)
PORT Port 3000 (fest im Dockerfile)

Caddy-Container (caddy service)

Variable Beschreibung Default Beispiel
CADDY_DOMAIN Domain für HTTPS / Zertifikat leer nsct.example.com
NSCT_RATE_LIMIT Rate-Limiting (optional) leer 100/second

Konfiguration am Beispiel

Lokale Entwicklung (HTTP only)

NSCT_API_BASE_URL=http://host.docker.internal:8080
CADDY_DOMAIN=
DEFAULT_THEME=dark

Produktionsumgebung (HTTPS)

NSCT_API_BASE_URL=http://nsct-api:8080
CADDY_DOMAIN=nsct.example.com
DEFAULT_THEME=dark

Light-Mode (z.B. für bestimmte Nutzergruppen)

NSCT_API_BASE_URL=http://nsct-api:8080
CADDY_DOMAIN=nsct.example.com
DEFAULT_THEME=light

5. Docker Compose Konfiguration

Services

web — NSCT-Frontend

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: nsct-web
    restart: unless-stopped
    environment:
      - NSCT_API_BASE_URL=http://nsct-api:8080
      - DEFAULT_THEME=dark
    ports:
      - "3000:3000"    # Direktzugriff (intern)
    networks:
      - nsct-network
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1.0'
        reservations:
          memory: 128M
          cpus: '0.25'

Erklärung:

  • build.context: ./ — Dockerfile und Quellcode im selben Verzeichnis
  • container_name: Festgelegt für docker compose exec / logs
  • restart: unless-stopped: Startet automatisch nach Crash/Reboot
  • ports: 3000:3000: Exponiert für Debugging/Health-Check (Caddy sollte im Produktivbetrieb den einzigen入口 haben)
  • healthcheck: wget prüft /health-Endpoint

caddy — Reverse-Proxy

  caddy:
    image: caddy:2.8-alpine
    container_name: nsct-caddy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    environment:
      - CADDY_DOMAIN=${CADDY_DOMAIN:-}
      - NSCT_RATE_LIMIT=${NSCT_RATE_LIMIT:-}
    networks:
      - nsct-network
    depends_on:
      - web
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: '0.5'
        reservations:
          memory: 64M
          cpus: '0.1'

Erklärung:

  • image: Offizielles Caddy 2.8 Alpine-Image
  • ports: 80:80, 443:443: Öffentliche Ports
  • volumes: Caddyfile-Mapping, TLS-Zertifikate (caddy_data), Konfiguration (caddy_config)
  • depends_on: Web-Container muss vor Caddy starten

Volumes

Volume Pfad im Container Zweck
caddy_data /data TLS-Zertifikate (Let's Encrypt)
caddy_config /config Caddy-Konfiguration (ACME account)

Volumes sind persistent — Zertifikate überleben Container-Neustarts.

Networks

networks:
  nsct-network:
    driver: bridge

Bridge-Netzwerk für inter-Container-Kommunikation. Container erreichen sich über ihren Namen als hostname:

  • Web-Container erreicht Backend über nsct-api:8080
  • Caddy erreicht Web-Container über web:3000

Resource Limits

Service Memory Limit CPU Limit Memory Reservation CPU Reservation
web 512 MB 1.0 Core 128 MB 0.25 Core
caddy 256 MB 0.5 Core 64 MB 0.1 Core

6. Build-Prozess

Multi-Stage Docker Build

# Stage 1: Build
FROM node:22-alpine AS builder
WORKDIR /app
COPY package.json ./
COPY package-lock.json ./
RUN npm ci
COPY . .
ENV NODE_ENV=production
RUN npm run build

# Stage 2: Runtime
FROM node:22-alpine AS runtime
WORKDIR /app
COPY --from=builder /app/build ./build
COPY --from=builder /app/node_modules ./node_modules
COPY package.json .
RUN addgroup -S nsct && adduser -S nsct -G nsct
USER nsct
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 CMD wget -qO- http://localhost:3000/health || exit 1
EXPOSE 3000
ENV HOST=0.0.0.0
ENV PORT=3000
CMD ["node", "build"]

Stage 1 — Builder:

  • Node 22 Alpine als Build-Umgebung
  • npm ci für reproduzierbare Dependencies (lock-file-basiert)
  • npm run build compiliert SvelteKit → statische Assets in build/

Stage 2 — Runtime:

  • Node 22 Alpine (lean, ~70 MB)
  • Nur build/ und node_modules kopiert (kein Quellcode)
  • Non-root-User nsct (Sicherheit)
  • Health-Check integriert
  • Startet SvelteKit mit node build

Vite Konfiguration

export default defineConfig({
  plugins: [sveltekit()],
  server: {
    port: 3000,
    strictPort: true,
    host: '0.0.0.0',
    proxy: {
      '/api': {
        target: 'http://nsct-api:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
});
  • Port: 3000, strikte Bindung (strictPort: true)
  • Dev-Proxy: /api/* wird an http://nsct-api:8080 geroutet
  • Production: Keine Proxy-Config — API-Calls gehen direkt an NSCT_API_BASE_URL

TailwindCSS Konfiguration

export default {
  content: ['./src/**/*.{svelte,js,ts,html}'],
  darkMode: 'class',
  theme: {
    extend: {
      colors: {
        'nsct-dark-bg': '#0f0f23',
        'nsct-dark-surface': '#1a1a2e',
        'nsct-dark-border': '#2d2d44',
        'nsct-dark-text': '#e0e0e0',
        'nsct-light-bg': '#ffffff',
        'nsct-light-surface': '#f8f9fa',
        'nsct-light-text': '#1a1a1a',
        'nsct-accent': '#6366f1',
        'nsct-success': '#22c55e',
        'nsct-warning': '#f59e0b',
        'nsct-error': '#ef4444'
      }
    }
  }
};
  • Dark Mode: Klassen-basiert (class="dark" auf <html>)
  • Custom Farben: nsct-*-Präfix für konsistentes Theming

SvelteKit Konfiguration

import adapter from '@sveltejs/adapter-static';

export default {
  kit: {
    adapter: adapter({
      pages: 'build',
      assets: 'build',
      fallback: undefined,
      precompress: false,
      strict: true
    }),
    alias: {
      $components: 'src/components',
      $lib: 'src/lib',
      $assets: 'src/assets'
    }
  }
};
  • Adapter: static — alle Routes werden als HTML-Dateien gebaut
  • Output: build/ (Pages + Assets)
  • Alias: $lib/, $components/, $assets/ für Importe

Dependencies

Paket Version Rolle
svelte ^5.0.0 Framework
@sveltejs/kit ^2.0.0 Router / SSR/SSG
@sveltejs/adapter-static ^3.0.0 Static-Output-Adapter
@sveltejs/vite-plugin-svelte ^4.0.0 Vite-Integration
vite ^6.0.0 Build-Tool
tailwindcss ^4.0.0 CSS-Framework
postcss + autoprefixer ^8.4.0 CSS-Pipeline
typescript ^5.0.0 Typisierung
svelte-markdown ^0.5.0 Markdown-Rendring für Reports

Bauen

# Lokal bauen (für Tests)
npm run build

# Ausgabe: ./build/ (index.html, _app/, assets/)

# Dev-Server starten
npm run dev
# → http://localhost:3000

# Typ-Checks
npm run check
# → svelte-check über tsconfig

7. Security

Security Headers (Caddy)

Header Wert Bedeutung
X-Content-Type-Options nosniff Verhindert MIME-Type-Sniffing durch Browser
X-Frame-Options DENY Kein Embedding in Frames/iframes
X-XSS-Protection 1; mode=block Builtin XSS-Protection aktivieren
Content-Security-Policy Default-self + inline-src Ressourcen-Whitelist
Referrer-Policy no-referrer-when-downgrade Referrer nur bei upgrades senden
Permissions-Policy camera=(), microphone=(), geolocation=() Sensible Browser-Features blockieren

CSP-Details:

default-src 'self'
script-src 'self' 'unsafe-inline' 'unsafe-eval'
style-src 'self' 'unsafe-inline'
img-src 'self' data: blob:
font-src 'self' data:
connect-src 'self' https://*
frame-src 'none'
object-src 'none'
base-uri 'self'
form-action 'self'

Rate-Limiting

Optional über Umgebungsvariable NSCT_RATE_LIMIT. Im Caddyfile wird geprüft, ob die Variable gesetzt ist:

# .env
NSCT_RATE_LIMIT=100/second

Hinweis: Das offizielle caddy:2.8-alpine-Image enthält kein Rate-Limiting-Plugin. Für Produktions-Rate-Limiting empfiehlt sich ein vorgeschalteter Reverse-Proxy (Traefik, HAProxy, Nginx).

CORS

Das Frontend stellt keine Cross-Origin-Anfragen. Alle API-Calls gehen direkt an NSCT_API_BASE_URL (gleicher Container/Netzwerk). CORS ist kein Problem, da keine fremden Domains angesprochen werden.

API-Key-Speicherung

Eigenschaft Wert
Speicherort localStorage (Key: nsct-api-key)
Nicht gespeichert in Cookies, Sessions, Server
Persistenz Bleibt nach Browser-Neustart erhalten
Entfernen logout()removeApiKey()
// auth.ts — Schlüssel-Management
const API_KEY_STORAGE = 'nsct-api-key';
localStorage.getItem('nsct-api-key');   // Lesen
localStorage.setItem('nsct-api-key', key); // Schreiben
localStorage.removeItem('nsct-api-key'); // Entfernen

Sicherheitsbewertung: API-Key im localStorage ist für SPA-Architekturen üblich, bietet aber keine Protection vor XSS. Für höheres Security-Niveau wäre httpOnly-Cookie-Auth bevorzugt. Für interne Tools mit kontrollierter Umgebung ist localStorage akzeptabel.


8. Troubleshooting

Login schlägt fehl

Symptome: API-Key wird akzeptiert, aber keine Research-Daten geladen.

Ursachen und Lösungen:

Ursache Diagnose Lösung
Falsche NSCT_API_BASE_URL curl $NSCT_API_BASE_URL/health → fail .env anpassen, docker compose up -d
CORS-Probleme (Backend) Browser-Console: CORS error Backend CORS-Headers prüfen
API-Key falsch/ungültig 401/403 vom Backend API-Key validieren, neu generieren
Backend nicht erreichbar docker compose logs web → connection refused Backend-Container prüfen: docker ps
# Diagnose-Skript
docker compose logs web | grep -i error
curl -v http://localhost:3000/health
docker compose exec web wget -qO- http://nsct-api:8080/health || echo "Backend unreachable"

Symptome: Share-Token-URL (z.B. /share/<token>) zeigt 404 oder leere Seite.

Mögliche Ursachen:

Ursache Diagnose Lösung
Token abgelaufen Backend gibt 404/410 zurück Neues Share-Link generieren
Backend nicht erreichbar curl auf Backend fail Backend-Container prüfen
Caddy-Proxy blockiert Caddy logs zeigen 404 Caddyfile auf /share/-Route prüfen
Falsche NSCT_API_BASE_URL Frontend kann Share-Endpoint nicht reachen .env überprüfen
# Share-Token auf Gültigkeit prüfen
docker compose logs web | grep share

Markdown-Reports werden nicht angezeigt

Symptome: ReportTab zeigt leeren Bereich statt Report-Inhalt.

Ursache Diagnose Lösung
svelte-markdown nicht importiert Console error: Cannot find module svelte-markdown in package.json prüfen
Report-Feld leer Backend gibt report: null zurück Backend-Daten prüfen
Markdown-Syntax invalid Reports mit ### Headern Client-side Markdown-Renderer prüfen
# svelte-markdown prüfen
docker compose exec web ls node_modules/svelte-markdown/package.json

Polling stoppt nicht

Symptome: Fetch-Intervall läuft weiter, auch wenn Research COMPLETED oder FAILED.

Ursache: Das Polling-System prüft nicht auf End-Status (COMPLETED, FAILED).

Lösung: Prüfen Sie die Polling-Logik in src/lib/stores.ts und den entsprechenden Komponenten. Das Polling muss auf pollingActive-Store reagieren und bei COMPLETED/FAILED-Status den pollingActive-Store auf false setzen.

// Korrektur: Polling bei End-Status stoppen
if (status === 'COMPLETED' || status === 'FAILED') {
  pollingActive.set(false);
}

Theme nicht persistent

Symptome: Theme springt nach Seiten-Reload zurück zu Standard.

Ursache Diagnose Lösung
localStorage blockiert Browser-Extension blockiert LS Extension deaktivieren, testen
class="dark" nicht gesetzt app.html hat keine dark-class app.html Zeile 2: class="dark" prüfen
Theme-Store initialisiert falsch DEFAULT_THEME ignored .envDEFAULT_THEME prüfen
# Theme-Store initial prüfen
docker compose exec web node -e "console.log(require('fs').readFileSync('/dev/stdin','utf8'), process.env.DEFAULT_THEME)"

Error-Banner verstehen

Das Error-System besteht aus drei Komponenten:

Komponente Datei Zweck
ErrorBanner src/components/ErrorBanner.svelte Zeigt API/Network-Fehler an
ErrorBannerStore src/components/ErrorBannerStore.ts Zentrales Error-State
ContradictionBanner src/components/ContradictionBanner.svelte Zeigt Zielkonflikte in Research an

Error-Flow:

  1. API-Fehler wird gefangen → ErrorBannerStore schreiben
  2. ErrorBanner.svelte subscribt auf Store → rendert Banner
  3. Nutzer schließt Banner → Error wird zurückgesetzt

Fehlerkategorien:

Error-Typ Beispiel Lösung
Network Error TypeError: Failed to fetch Netzwerk/Backend prüfen
API Error API Error 500: Internal Server Error Backend-Logs prüfen
Auth Error API Error 401: Unauthorized API-Key neu eingeben
Share Error Token nicht gefunden Link prüfen/erneut generieren

9. Monitoring

Container-Logs

# Alle Logs (follow)
docker compose logs -f

# Nur Web-Container
docker compose logs -f web

# Nur Caddy-Container
docker compose logs -f caddy

# Logs mit Filter
docker compose logs web | grep -i error
docker compose logs caddy | grep -i "request"

Health-Check

# Web-Container health
curl -s http://localhost:3000/health

# Caddy health (über Proxy)
curl -s -o /dev/null -w "%{http_code}" https://nsct.example.com/health

# Docker health-Check-Status
docker inspect --format='{{.State.Health.Status}}' nsct-web
docker inspect --format='{{.State.Health.Status}}' nsct-caddy

Browser-Entwicklerwerkzeuge

Tab Inhalt
Network API-Calls inspizieren: URL, Status, Payload, Timing
Console JS-Errors, Warnings, Store-Updates
Application localStorage-Inhalt prüfen (nsct-api-key)
Performance Loading-Zeiten, Render-Performance

Wichtige Network-Filter:

  • XHR/Fetch: Alle API-Calls (GET, POST, DELETE)
  • WS/EventSource: Falls WebSocket-Polling verwendet wird
  • Status-Codes: 200 (OK), 401 (Auth), 404 (Not Found), 500+ (Server Error)

Prometheus/Metriken (falls aktiv)

Falls der NSCT-Backend oder Caddy Metriken exponieren:

# Caddy Metriken (Standard: :2019)
curl -s http://localhost:2019/metrics | head -20

# Web-Container Metriken
curl -s http://localhost:3000/metrics

Caddy 2.8 mit caddymetrics oder built-in prometheus-Directive.


10. Deployment-Checkliste

Pre-Deploy

  • .env-Datei überprüft: NSCT_API_BASE_URL, CADDY_DOMAIN, DEFAULT_THEME
  • DNS-Eintrag für CADDY_DOMAIN zeigt auf Server-IP
  • Ports 80 und 443 im Firewall/Security-Group freigeschaltet
  • Caddyfile auf Korrektheit geprüft (Syntax: docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile)
  • Backend-Container erreichbar: docker compose exec web wget -qO- http://nsct-api:8080/health
  • Git-Status sauber: git status → kein uncommitted Code

Deploy

# Änderungen committen
git add .
git commit -m "Prepare deployment: <description>"
git push origin main

# Stack deployen
docker compose up -d
docker compose up -d --build   # Bei Code-Änderungen

Post-Deploy

  • Health-Check erfolgreich: curl http://localhost:3000/health → 200
  • Caddy erreichbar: curl -s -o /dev/null -w "%{http_code}" https://nsct.example.com → 200
  • Browser-Test: App lädt, Theme korrekt, Login funktioniert
  • API-Key-Login test: Login mit gültigem Key → Dashboard sichtbar
  • Share-Link test: Neue Research starten → Share-Token generieren → Link im Browser öffnen
  • Polling-Test: Langsame Research starten → Polling-Updates in der Sidebar verfolgen
  • Error-Test: Falschen API-Key eingeben → Error-Banner erscheint
  • Logs prüfen: docker compose logs --tail=50 → keine Errors
  • Docker-Health-Status: docker inspect --format='{{.State.Health.Status}}' nsct-webhealthy

Rollback

# Current Image-ID merken
docker compose ps --format json | jq -r '.Image'

# Stack stoppen
docker compose down

# Alte Image-Version in docker-compose.yml eintragen (Tag fixen)
# docker-compose.yml → web.build → tag auf feste Version setzen

# Stack starten
docker compose up -d

Best Practice: Immer feste Image-Tags verwenden (image: nsct-web:1.2.3) statt latest.


11. Architektur-Diagramm

Hochlevel-Architektur

┌──────────────┐       HTTPS        ┌──────────────┐         HTTP         ┌──────────────┐
│   Browser    │ ────────────────► │   Caddy      │ ──────────────────► │   Web        │
│   (Client)   │  Port 80/443      │  (Reverse-   │    Port 3000        │   Container │
│              │                   │   Proxy)      │                     │   (Node 22) │
│ - UI/HTML    │                   │              │                     │  :3000 Static│
│ - JS/Bundle  │                   │ - TLS/HTTPS   │                     │  Assets    │
│ - localStorage│                   │ - Security   │                     │              │
│   (API-Key)  │                   │   Headers     │                     │              │
└──────────────┘                   │ - Gzip/Zstd   │                     └──────┬───────┘
                                   │ - Rate-Limit*  │                            │
                                   └──────────────┘                            │
                                                                      ┌────────▼───────┐
                                                                      │  NSCT Backend  │
                                                                      │  (API Server)  │
                                                                      │  :8080         │
                                                                      │                │
                                                                      │ - Research API │
                                                                      │ - Auth         │
                                                                      │ - PostgreSQL   │
                                                                      └────────────────┘

Datenfluss

1. API-Key Eingabe (Login)
   → localStorage.setItem('nsct-api-key', key)

2. API-Anfrage (z.B. Research erstellen)
   → localStorage.getItem('nsct-api-key')
   → POST ${NSCT_API_BASE_URL}/research
   → Header: X-API-Key: <key>
   → Body: { "query": "..." }

3. Antwort vom Backend
   → 200 OK: { id: "...", status: "RUNNING" }
   → Store: researchData.set(response)
   → Polling: pollingActive.set(true) → pollingInterval (5s)

4. Polling bis COMPLETED/FAILED
   → GET ${NSCT_API_BASE_URL}/research/<id>
   → X-API-Key: <key>
   → Status prüfen: COMPLETED → pollingActive.set(false)

5. Share-Link generieren
   → Share-Token vom Backend erhalten
   → URL: /share/<token>
   → Öffentlicher Zugang (kein API-Key nötig)

Kompletter Request-Flow (mit Caddy)

Browser                          Caddy (HTTPS)                    Web Container
─────────                       ───────────────                     ─────────────
GET /share/abc123  ──────────►  :443 reverse_proxy web:3000 ──────►  serve static
                                                                 200 OK: share.html
                                                                     + JS Bundle
                                                                      |
Browser  ◄─────────────────────────── ◄────────────────────────────  HTML + JS
     200 OK                             200 OK                      |
                                                                      |
                            (JS lädt Share-Daten)                     |
                                                                      |
GET /api/share/abc123 ──────────────────────────────────────────────► POST/GET NSCT-API
    X-API-Key: (none for share)                                              |
                                                                               |
     ◄──────────────────────────────────────────────────────────────────  JSON Response
    200 OK                                                                    

Storage-Layer

┌─────────────────────────────────────────────────────────────────────┐
│                         PostgreSQL                                  │
│  ┌───────────────┐  ┌───────────────┐  ┌──────────────────────────┐ │
│  │ researches    │  │ sources       │  │ claims / evidence        │ │
│  │ - id (PK)     │  │ - id (PK)     │  │ - id (PK)                │ │
│  │ - query       │  │ - url         │  │ - research_id (FK)       │ │
│  │ - status      │  │ - title       │  │ - claim text             │ │
│  │ - created_at  │  │ - domain      │  │ - evidence               │ │
│  │ - completed_at│  └───────────────┘  └──────────────────────────┘ │
│  └───────────────┘                                                   │
│  ┌───────────────┐  ┌───────────────┐                              │
│  │ users         │  │ shares        │                              │
│  │ - api_key     │  │ - token       │                              │
│  │ - username    │  │ - research_id │                              │
│  └───────────────┘  └───────────────┘                              │
└─────────────────────────────────────────────────────────────────────┘