commit 631b17ae05c4ebe307f2643cc13fec8eb60c53b2 Author: faligam Date: Sun Sep 6 06:26:03 2026 +0000 FE-0: Projektgrundgerüst (SvelteKit, Tailwind, Docker, Caddy) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9e5f514 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +.git +.env +.env.* +*.md \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..317fa6d --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +# URL des NSCT Backend API +NSCT_API_BASE_URL=http://localhost:8080 + +# Optional: Caddy Domain für externen Zugriff +CADDY_DOMAIN= + +# Default Theme +DEFAULT_THEME=dark \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fb4097d --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +.svelte-kit +build +.env +.env.local +*.log +.DS_Store \ No newline at end of file diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..e77d31e --- /dev/null +++ b/Caddyfile @@ -0,0 +1,12 @@ +: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 + header { + X-Content-Type-Options nosniff + X-Frame-Options DENY + X-XSS-Protection "1; mode=block" + } +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..101cfc3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# Build stage +FROM node:22-alpine AS builder +WORKDIR /app +COPY package.json . +RUN npm ci +COPY . . +ENV NODE_ENV=production +RUN npm run build + +# Production stage +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 package.json + +# Non-root user +RUN addgroup -S nsct && adduser -S nsct -G nsct +USER nsct + +EXPOSE 3000 +ENV HOST=0.0.0.0 +ENV PORT=3000 + +CMD ["node", "build"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..de0b18e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,38 @@ +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" + networks: + - nsct-network + + 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 + networks: + - nsct-network + depends_on: + - web + +networks: + nsct-network: + driver: bridge + +volumes: + caddy_data: + caddy_config: \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..0b04bff --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "nsct-web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.0", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^4.0.0", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "tailwindcss": "^4.0.0", + "typescript": "^5.0.0", + "vite": "^6.0.0" + }, + "dependencies": { + "svelte-markdown": "^0.5.0" + } +} \ No newline at end of file diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..5c45a3f --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; \ No newline at end of file diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000..f173aa4 --- /dev/null +++ b/src/app.css @@ -0,0 +1 @@ +@import 'tailwindcss'; \ No newline at end of file diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..e320afd --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,14 @@ +/// +/// + +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; \ No newline at end of file diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..6784153 --- /dev/null +++ b/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + \ No newline at end of file diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..b41e9ee --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,60 @@ +const API_BASE_URL = import.meta.env.NSCT_API_BASE_URL || 'http://localhost:8080'; + +interface ApiConfig { + apiKey?: string; +} + +function headers(config?: ApiConfig): Record { + const h: Record = { + 'Content-Type': 'application/json' + }; + if (config?.apiKey) { + h['X-API-Key'] = config.apiKey; + } + return h; +} + +export async function apiPost( + path: string, + body?: unknown, + config?: ApiConfig +): Promise { + const res = await fetch(`${API_BASE_URL}${path}`, { + method: 'POST', + headers: headers(config), + body: body ? JSON.stringify(body) : undefined + }); + if (!res.ok) { + throw new Error(`API Error ${res.status}: ${res.statusText}`); + } + return res.json(); +} + +export async function apiGet( + path: string, + config?: ApiConfig +): Promise { + const res = await fetch(`${API_BASE_URL}${path}`, { + headers: headers(config) + }); + if (!res.ok) { + throw new Error(`API Error ${res.status}: ${res.statusText}`); + } + return res.json(); +} + +export async function apiDelete( + path: string, + config?: ApiConfig +): Promise { + const res = await fetch(`${API_BASE_URL}${path}`, { + method: 'DELETE', + headers: headers(config) + }); + if (!res.ok) { + throw new Error(`API Error ${res.status}: ${res.statusText}`); + } + return res.json(); +} + +export { API_BASE_URL }; \ No newline at end of file diff --git a/src/lib/stores.ts b/src/lib/stores.ts new file mode 100644 index 0000000..cc266de --- /dev/null +++ b/src/lib/stores.ts @@ -0,0 +1,13 @@ +import { writable } from 'svelte/store'; + +// Theme: 'dark' | 'light' +export const theme = writable<'dark' | 'light'>('dark'); + +// User: null oder { apiKey, username? } +export const user = writable<{ apiKey: string; username?: string } | null>(null); + +// Sidebar: ist offen oder geschlossen (mobile) +export const sidebarOpen = writable(false); + +// Session IDs der letzten Researches +export const recentResearchIds = writable([]); \ No newline at end of file diff --git a/src/lib/theme.ts b/src/lib/theme.ts new file mode 100644 index 0000000..8558830 --- /dev/null +++ b/src/lib/theme.ts @@ -0,0 +1,20 @@ +import { theme } from '$lib/stores'; + +export function initTheme() { + const stored = localStorage.getItem('nsct-theme'); + if (stored === 'dark' || stored === 'light') { + theme.set(stored); + } else { + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + theme.set(prefersDark ? 'dark' : 'light'); + } +} + +export function toggleTheme() { + theme.update(t => t === 'dark' ? 'light' : 'dark'); +} + +export function applyTheme(t: 'dark' | 'light') { + document.documentElement.classList.toggle('dark', t === 'dark'); + localStorage.setItem('nsct-theme', t); +} \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..d41435e --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,41 @@ +export interface ResearchStatus { + id: string; + status: string; + progress?: number; + error?: string; +} + +export interface ResearchSummary { + id: string; + query: string; + status: string; + created_at: string; + completed_at?: string; +} + +export interface SourceInfo { + id: string; + url: string; + title: string; + domain?: string; + source_type?: string; + independence_score?: number; +} + +export interface ClaimInfo { + id: string; + claim: string; + claim_type: string; + confidence: number; + source_id?: string; + evidence_span?: string; +} + +export interface EvidenceScore { + source_independence: number; + primary_source_proximity: number; + cross_source_support: number; + contradiction_level: number; + evidence_directness: number; + date_relevance: number; +} \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..5dcb7d3 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,17 @@ + + + + NSCT – Neutral Search Crawler + + + + \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..92d97fb --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,49 @@ + + +
+
+

NSCT

+

Neutral Search Crawler Tool

+ +
+

Anmelden

+ e.key === 'Enter' && login()} + /> + + {#if error} +

{error}

+ {/if} +
+ + +
+
\ No newline at end of file diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..2c416e6 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,22 @@ +import adapter from '@sveltejs/adapter-static'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +const config = { + kit: { + adapter: adapter({ + pages: 'build', + assets: 'build', + fallback: undefined, + precompress: false, + strict: true + }), + alias: { + $components: 'src/components', + $lib: 'src/lib', + $assets: 'src/assets' + } + }, + preprocess: vitePreprocess() +}; + +export default config; \ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..f2d634c --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,33 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{svelte,js,ts,html}'], + darkMode: 'class', // Class-based theming (dark/light toggle) + theme: { + extend: { + colors: { + // Dark mode colors + 'nsct-dark-bg': '#0f0f23', + 'nsct-dark-surface': '#1a1a2e', + 'nsct-dark-border': '#2d2d44', + 'nsct-dark-text': '#e0e0e0', + 'nsct-dark-text-secondary': '#a0a0b0', + 'nsct-dark-hover': '#2a2a3e', + // Light mode colors + 'nsct-light-bg': '#ffffff', + 'nsct-light-surface': '#f8f9fa', + 'nsct-light-border': '#e0e0e0', + 'nsct-light-text': '#1a1a1a', + 'nsct-light-text-secondary': '#666666', + 'nsct-light-hover': '#f0f0f0', + // Accent + 'nsct-accent': '#6366f1', + 'nsct-accent-hover': '#4f46e5', + 'nsct-success': '#22c55e', + 'nsct-warning': '#f59e0b', + 'nsct-error': '#ef4444', + 'nsct-info': '#3b82f6' + } + } + }, + plugins: [] +}; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..80a321b --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..bebff56 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,18 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +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/, '') + } + } + } +}); \ No newline at end of file