diff --git a/ADMIN-Frontend.md b/ADMIN-Frontend.md new file mode 100644 index 0000000..0652814 --- /dev/null +++ b/ADMIN-Frontend.md @@ -0,0 +1,873 @@ +# 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 `` | +| **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 + +```bash +git clone https://github.com//NSCT-FrontEnd.git +cd NSCT-FrontEnd +``` + +### Umgebungsvariablen konfigurieren + +```bash +# .env kopieren und anpassen +cp .env.example .env +``` + +Minimale `.env`-Konfiguration: + +```env +# 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 + +```bash +docker compose up -d +``` + +### Health-Check + +```bash +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) + +```env +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) + +```env +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) + +```env +NSCT_API_BASE_URL=http://host.docker.internal:8080 +CADDY_DOMAIN= +DEFAULT_THEME=dark +``` + +#### Produktionsumgebung (HTTPS) + +```env +NSCT_API_BASE_URL=http://nsct-api:8080 +CADDY_DOMAIN=nsct.example.com +DEFAULT_THEME=dark +``` + +#### Light-Mode (z.B. für bestimmte Nutzergruppen) + +```env +NSCT_API_BASE_URL=http://nsct-api:8080 +CADDY_DOMAIN=nsct.example.com +DEFAULT_THEME=light +``` + +--- + +## 5. Docker Compose Konfiguration + +### Services + +#### `web` — NSCT-Frontend + +```yaml +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 + +```yaml + 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 + +```yaml +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 + +```dockerfile +# 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 + +```typescript +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 + +```javascript +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 ``) +- **Custom Farben:** `nsct-*`-Präfix für konsistentes Theming + +### SvelteKit Konfiguration + +```javascript +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 + +```bash +# 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 +# .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()` | + +```typescript +// 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` | + +```bash +# 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" +``` + +### Share-Links funktionieren nicht + +**Symptome:** Share-Token-URL (z.B. `/share/`) 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 | + +```bash +# 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 | + +```bash +# 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. + +```typescript +// 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 | `.env` → `DEFAULT_THEME` prüfen | + +```bash +# 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 + +```bash +# 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 + +```bash +# 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: + +```bash +# 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`](https://github.com/mholt/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 + +```bash +# Änderungen committen +git add . +git commit -m "Prepare deployment: " +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-web` → `healthy` + +### Rollback + +```bash +# 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: + → 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/ + → X-API-Key: + → Status prüfen: COMPLETED → pollingActive.set(false) + +5. Share-Link generieren + → Share-Token vom Backend erhalten + → URL: /share/ + → Ö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 │ │ +│ └───────────────┘ └───────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` \ No newline at end of file