f564ecfbc7
Shell: - One shared NexusLayout for ALL routes (dashboard + pages): compact 68px icon rail with hover-expand overlay, replaces both old sidebars + topbars - Single flat nav source (railNav) — same menu everywhere incl. Settings - Removed dead shell components (AppSidebar, AppHeader, Topbar, NavGroup, NavItem, ModuleView) and dead nav routes; App.vue is now just RouterView Live updates: - Fix SSE loop in DashboardController: PeriodicTimer.WaitForNextTickAsync and ChannelReader.ReadAsync were re-invoked while pending — every published update threw InvalidOperationException and killed ALL live streams - liveSync store: treat graceful stream close as disconnect (was stuck connected=true with polling stopped -> page frozen until manual reload), fast first retry, heartbeat watchdog (65s), reconnect on online/visibility - One app-wide SSE connection owned by the layout instead of per-view connect/disconnect churn; removed duplicate live-sync.ts store Performance: - Board endpoint: drop nested childTasks duplication (counts stay) — payload 104KB -> 65KB; SSE snapshots shrink equally - nginx: gzip for JSON/JS/CSS (board 18KB, bundle 95KB over the wire); text/event-stream excluded to keep SSE unbuffered Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
81 lines
2.8 KiB
Nginx Configuration File
81 lines
2.8 KiB
Nginx Configuration File
server {
|
|
listen 80;
|
|
server_name _;
|
|
|
|
root /usr/share/nginx/html;
|
|
index index.html;
|
|
|
|
# Kompression für Bundle + API-JSON (Board-Payload ~100 KB → wenige KB).
|
|
# text/event-stream bewusst NICHT in gzip_types: gzip würde den SSE-Stream puffern.
|
|
gzip on;
|
|
gzip_comp_level 5;
|
|
gzip_min_length 1024;
|
|
gzip_vary on;
|
|
gzip_proxied any;
|
|
gzip_types application/json application/javascript text/css text/javascript image/svg+xml;
|
|
|
|
add_header Content-Security-Policy "default-src 'self'; connect-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
|
|
add_header Referrer-Policy "no-referrer" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-Frame-Options "DENY" always;
|
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
|
|
|
# Gehashte Assets: 1 Jahr cachen (immutable wg. Content-Hash im Dateinamen)
|
|
location /assets/ {
|
|
expires 1y;
|
|
add_header Cache-Control "public, immutable";
|
|
}
|
|
|
|
# SPA-Entry nie cachen
|
|
location = /index.html {
|
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
|
add_header Pragma "no-cache";
|
|
add_header Expires "0";
|
|
}
|
|
|
|
# Bridge-Endpunkte (Agent-zu-Backend): separater Pfad ohne CSP-Einschränkungen
|
|
# für Gateway-Calls. Kein Caching, kein Buffering.
|
|
location /api/bridge/ {
|
|
proxy_pass http://api:8080;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header X-Agent-Id $http_x_agent_id;
|
|
proxy_buffering off;
|
|
proxy_read_timeout 120s;
|
|
}
|
|
|
|
# Dashboard SSE stream: single dedicated non-buffered block.
|
|
location = /api/dashboard/live {
|
|
proxy_pass http://api:8080;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Connection "";
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_buffering off;
|
|
proxy_cache off;
|
|
proxy_read_timeout 1h;
|
|
proxy_send_timeout 1h;
|
|
add_header Cache-Control "no-cache, no-store, must-revalidate" always;
|
|
add_header X-Accel-Buffering no always;
|
|
}
|
|
|
|
location /api/ {
|
|
proxy_pass http://api:8080;
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
}
|
|
|
|
location /health {
|
|
proxy_pass http://api:8080/health;
|
|
}
|
|
|
|
location / {
|
|
try_files $uri $uri/ /index.html;
|
|
}
|
|
}
|
|
|