Files
NSCT-FrontEnd/ADMIN-Frontend.md

873 lines
28 KiB
Markdown

# 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
```bash
git clone https://df918b20ee2da2f8dd9bdbdb42ee0c6f9da@git.frerkc.de/opencode/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 `<html>`)
- **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/<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 |
```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: <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-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: <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 │ │
│ └───────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```