# 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://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 # Erreichbarer Host:Port des Backend-Rechners (kein Docker-Netzwerk nötig) NSCT_API_UPSTREAM=backend.example.com: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 | |---|---|---|---| | `PORT` | Port des statischen Webservers | `3000` | (fest im Caddyfile) | ### 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` | | `NSCT_API_UPSTREAM` | Host:Port des Backend-Rechners | `host.docker.internal:8080` | `backend.example.com:8080` | ### Konfiguration am Beispiel #### Lokale Entwicklung (HTTP only) ```env NSCT_API_UPSTREAM=host.docker.internal:8080 CADDY_DOMAIN= DEFAULT_THEME=dark ``` #### Produktionsumgebung (HTTPS) ```env NSCT_API_UPSTREAM=backend.example.com:8080 CADDY_DOMAIN=nsct.example.com DEFAULT_THEME=dark ``` #### Light-Mode (z.B. für bestimmte Nutzergruppen) ```env NSCT_API_UPSTREAM=backend.example.com: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 an den Same-Origin-Pfad `/api`; Caddy leitet ihn an `NSCT_API_UPSTREAM` weiter. Frontend und Backend können daher auf getrennten Rechnern laufen, ohne dass der Browser CORS zum Backend benötigt. ### 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 │ │ │ └───────────────┘ └───────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ ```