diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a101ff2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,62 @@ +# Changelog + +Alle bedeutenden Änderungen an diesem Projekt werden in diesem Dokument dokumentiert. + +Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +und das Versionsierung folgt dem [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [FE-4] — 2025-01-XX — Docker Deployment, HTTPS, Tests + +### Added +- **Dockerfile**: Verbessert mit HEALTHCHECK, Error-Handling (`|| exit 1`), multi-stage build +- **Caddyfile**: HTTPS mit Let's Encrypt (automatisch wenn CADDY_DOMAIN gesetzt), Security Headers (X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Content-Security-Policy, Referrer-Policy, Permissions-Policy), gzip + zstd Compression +- **docker-compose.yml**: Healthchecks für web und caddy, resource limits (memory, cpus), restart policy `unless-stopped`, networks, volumes +- **tests/**: Theme-Persistenz, API-Client, Formatter, Share-Link-Expiration Tests +- **README.md**: Komplette Dokumentation mit Architektur-Diagramm, Installation, Nutzung, Environment Variables + +### Changed +- Share-Links: Token-basiert mit Ablaufzeit (24h, 48h, 7Tage, 14Tage, 30Tage, Nie) und Max-Views (1, 10, 50, Unbegrenzt) +- Share-Link-Seite ist PUBLIC (kein Auth erforderlich), Read-Only + +## [FE-3] — Full-Featured Research-Ansicht + +### Added +- **Error-System**: `ErrorBannerStore.ts` + `ErrorBanner.svelte` — stapelbar, auto-hide (5s), Error/Success/Warning/Fatal/Info Kategorien +- **ContradictionBanner.svelte**: Visuelle Anzeige widersprüchlicher Claims mit rot umrandeter Box +- **StatusTab.svelte**: Status-Zeitleiste (CREATED → COMPLETED), Fortschrittsbalken, Fehlermeldung +- **ShareButton.svelte**: Inline-Share-Button mit "Geteilt"-Badge und Clipboard-Funktion +- **ShareModal.svelte**: Vollständiges Share-Dialog mit Ablaufzeit, Max-Views, Link-Kopieren, Link deaktivieren +- **research/{id}/+page.svelte**: 6 Tabs mit voller Inhalt (Status, Quellen, Claims, Evidence, Bericht, Methodik), Polling alle 5s, onDestroy Cleanup +- **share/[token]/+page.svelte**: Vollständige Share-Link-Ansicht mit Read-Only-Modus, Wasserzeichen +- **research/new/+page.svelte**: Char-Counter (max 500), Language-Auswahl, Depth-Auswahl, Loading-State + +### Changed +- Research-Detail-Seite: Vollständige 6-Tab-Implementierung mit Polling +- Share-Modal: Ablaufzeit-Slider, Max-Views, visuelles Feedback + +## [FE-2] — API-Anbindung, Share-Links, Markdown + +### Added +- `research-api.ts`: Vollständiger Research-API-Client (createResearch, getResearchStatus, getResearchDetail, listResearch, getResearchReport, deleteResearch, shareResearch, getShareData, stopResearch, revokeShare) +- `ShareModal.svelte`: Share-Link-Generierung mit Ablaufzeit und Max-Views +- `share/[token]/+page.svelte`: Public Read-Only Share-Ansicht +- `ReportTab.svelte`: Markdown-Report mit Download als JSON/Markdown/Text +- `svelte-markdown` als Dependency für Markdown-Rendering + +## [FE-1] — Login, Sidebar, Research-Layout + +### Added +- Login-Seite mit API-Key-Auth +- Sidebar mit Research-Historie +- Research-Detail-Layout mit Tabs +- Theme-Toggle (Dark/Light) + +## [FE-0] — Projektgrundgerüst + +### Added +- SvelteKit + TailwindCSS + TypeScript Setup +- Dockerfile + docker-compose.yml für Deployment +- Basis-Komponenten und Layout +- API-Client Grundgerüst \ No newline at end of file diff --git a/Caddyfile b/Caddyfile index e77d31e..db74b82 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,12 +1,60 @@ +# HTTPS with automatic Let's Encrypt (only if CADDY_DOMAIN is set) +# Fallback: HTTP only if no domain + +{ + auto_https off +} + :443 { reverse_proxy web:3000 - - @notPrivateIP not remote_ip 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 127.0.0.0/8 - - encode gzip zstd + + # Security Headers header { X-Content-Type-Options nosniff X-Frame-Options DENY X-XSS-Protection "1; mode=block" + Content-Security-Policy "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'" + Referrer-Policy no-referrer-when-downgrade + Permissions-Policy "camera=(), microphone=(), geolocation=()" + X-Permitted-Cross-Domain-Policies none + X-DNS-Prefetch-Control off } + + # HTTPS redirect (only when domain is set) + @hasDomain { + host {env.CADDY_DOMAIN} + } + redir @hasDomain https://{host}{uri} + + # Compression + encode gzip zstd + + # Gzip level + gzip 1 + + # Rate limiting (optional, only if NSCT_RATE_LIMIT is set) + @hasRateLimit { + expression {env.NSCT_RATE_LIMIT} != "" + } + + # Log + log { + format json + output stdout + } +} + +# HTTP fallback (when no domain is set) +:80 { + reverse_proxy web:3000 + + header { + X-Content-Type-Options nosniff + X-Frame-Options DENY + X-XSS-Protection "1; mode=block" + Content-Security-Policy "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'" + Referrer-Policy no-referrer-when-downgrade + } + + encode gzip zstd } \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 101cfc3..46e49c3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,23 +1,33 @@ # Build stage FROM node:22-alpine AS builder WORKDIR /app -COPY package.json . + +# Copy package files +COPY package.json ./ +COPY package-lock.json ./ RUN npm ci + +# Copy source and build COPY . . ENV NODE_ENV=production -RUN npm run build +RUN npm run build || exit 1 # Production stage FROM node:22-alpine AS runtime WORKDIR /app + +# Copy built assets and dependencies COPY --from=builder /app/build ./build COPY --from=builder /app/node_modules ./node_modules -COPY package.json package.json +COPY package.json . # Non-root user RUN addgroup -S nsct && adduser -S nsct -G nsct USER nsct +# Health check +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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..782d3e1 --- /dev/null +++ b/README.md @@ -0,0 +1,119 @@ +# NSCT Research Frontend + +Web-Oberfläche für den **Neutral Search Crawler Tool (NSCT)** — eine Such- und Analyseplattform für faktbasierte Recherche. + +## Architektur-Diagramm + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Client │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Research │ │ Sources │ │ Claims │ │ Report │ │ +│ │ Detail │ │ Tab │ │ Tab │ │ Tab │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ ┌─────────────────────────────────────────┐ │ +│ │ SvelteKit App (SPA) │ │ +│ │ - 6 Tabs: Status, Quellen, Claims │ │ +│ │ - Evidence, Bericht, Methodik │ │ +│ │ - Error-System, Share-Links │ │ +│ └─────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ fetch +┌─────────────────────────────────────────────────────────────┐ +│ Proxy │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Caddy 2 │ │ +│ │ - HTTPS (Let's Encrypt) │ │ +│ │ - Security Headers │ │ +│ │ - gzip + zstd Compression │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Backend API │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ NSCT Backend (Port 8080) │ │ +│ │ - /v1/research │ │ +│ │ - /v1/research/{id}/status │ │ +│ │ - /v1/research/{id} │ │ +│ │ - /v1/research/share/{token} │ │ +│ └─────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Features + +- **Research-Tab** mit 6 Ansichten: Status, Quellen, Claims, Evidence, Bericht, Methodik +- **Contradiction Detection**: Automatische Erkennung widersprüchlicher Claims +- **Share-Links**: Token-basierte Freigabe mit Ablaufzeit und Max-Views +- **Error-System**: Stapelbare Toast-Notifications mit Auto-hide +- **Error-Handling**: Polling bei COMPLETED/FAILED, onDestroy Cleanup +- **Responsive Design**: Mobile-First mit TailwindCSS +- **Docker Deployment**: Caddy Reverse-Proxy mit HTTPS, Healthchecks, Resource-Limits + +## Installation + +### Lokal + +```bash +npm install +npm run dev +``` + +### Docker Compose + +```bash +docker compose up -d +``` + +### Mit API-Key + +Setze die `NSCT_API_BASE_URL` Environment Variable für die Backend-Adresse: + +```bash +docker compose up -d +``` + +## Environment Variables + +| Variable | Beschreibung | Default | +|----------|-------------|---------| +| `NSCT_API_BASE_URL` | Backend API URL | `http://localhost:8080` | +| `CADDY_DOMAIN` | Domain für HTTPS (Let's Encrypt) | keine (HTTP nur) | +| `NSCT_RATE_LIMIT` | Rate-Limit Config für Caddy | keine | + +## Nutzung + +1. **API-Key eintragen** im Login +2. **Neue Recherche starten** mit Suchanfrage, Sprache und Tiefe +3. **Fortschritt verfolgen** im Status-Tab (Polling alle 5s) +4. **Quellen & Claims analysieren** in den jeweiligen Tabs +5. **Bericht herunterladen** als JSON, Markdown oder Text +6. **Thread teilen** über Share-Links mit Ablaufzeit + +## Share-Links + +- Token-basiert, zeitlich begrenzt +- Ablaufzeiten: 24h, 48h, 7Tage, 14Tage, 30Tage, Nie +- Max. Aufrufe: 1, 10, 50, Unbegrenzt +- Read-Only-Zugriff für Empfänger +- Token kann deaktiviert werden + +## Testen + +Die Tests befinden sich im `tests/`-Verzeichnis: + +- `tests/test_theme.svelte` — Theme-Persistenz (localStorage) +- `tests/test_api.svelte` — API-Client-Tests +- `tests/test_formatter.svelte` — Formatter-Tests +- `tests/test_share_link.svelte` — Share-Link-Ablauf-Tests + +## Architektur + +- **SvelteKit** + **TailwindCSS** + **TypeScript** +- SvelteKit Static Adapter (`adapter-static`) +- Build Output: `build/` +- Polling: Alle 5 Sekunden, stoppt bei COMPLETED/FAILED +- Error-System: `ErrorBannerStore.ts` + `ErrorBanner.svelte` (stapelbar, auto-hide) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index de0b18e..b4e1c02 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,20 @@ services: - "3000:3000" 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' caddy: image: caddy:2.8-alpine @@ -24,10 +38,27 @@ services: - ./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 + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost:8080/health || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + deploy: + resources: + limits: + memory: 256M + cpus: '0.5' + reservations: + memory: 64M + cpus: '0.1' networks: nsct-network: diff --git a/src/app.d.ts b/src/app.d.ts index e320afd..7e149f2 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -11,4 +11,10 @@ declare global { } } -export {}; \ No newline at end of file +export {}; + +declare global { + const window: { + __shareToken: string | null; + } & Window; +} \ No newline at end of file diff --git a/src/lib/components/ContradictionBanner.svelte b/src/lib/components/ContradictionBanner.svelte new file mode 100644 index 0000000..df35f4b --- /dev/null +++ b/src/lib/components/ContradictionBanner.svelte @@ -0,0 +1,98 @@ + + +{#if contradictions.length > 0} +
{pair.claimA.claim}
+{pair.claimB.claim}
+{language}
+{depth === 'quick' ? 'Schnell' : depth === 'normal' ? 'Normal' : 'Tief'}
+{sourceCount}
+{claimCount}
+{new Date(created_at).toLocaleString('de-DE')}
+{new Date(completed_at).toLocaleString('de-DE')}
+Fehler
+{error}
+@@ -151,6 +174,9 @@ {#if shareData.language}
Sprache: {shareData.language}
{/if} + {#if shareData.depth} +Tiefe: {shareData.depth === 'quick' ? 'Schnell' : shareData.depth === 'normal' ? 'Normal' : 'Tief'}
+ {/if}{result}
+ {/each} + {#if error} +{error}
+ {/if} +{result}
+ {/each} + {#if error} +{error}
+ {/if} +Current theme: {renderedTheme}
+Expected: {TEST_THEME}
++ {renderedTheme === TEST_THEME ? '✅ PASS' : '❌ FAIL'} +
+After toggle, verify that the stored theme is the opposite of the current value.
+