Admin Handbook: Frontend (NSCT Web)
This commit is contained in:
873
ADMIN-Frontend.md
Normal file
873
ADMIN-Frontend.md
Normal file
@@ -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 `<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://github.com/<org>/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 │ │
|
||||||
|
│ └───────────────┘ └───────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user