pdf-quiz-generator/frontend/nginx.conf
Daniel 16aed6b6b0 feat: Cap on its own host, hints per topic, and an objective is asked for
Cap moved from /cap/ under this app to cap.pedshub.com, so anything else
on this machine can use the same instance. Caddy terminates it, the
backend keeps verifying over the compose network rather than going out
and back, and the widget endpoint is configuration rather than a path
baked into the component. Verified: a challenge is issued on the
subdomain, and a token that was never issued is still refused.

"Correct using hints" is now a per-topic figure. The knowledge profile's
accuracy bar was two-tone because /study-tools/recommendations carried
only `answered` and `correct`; the hint count existed lifetime-wide but
never per topic, and inferring one from the other would have been a
different set of answers drawn as though it were this one. The column
was already on attempt_answers, so it is a group-by, and the bar is
three-tone as the reference has it.

And the objective is asked for. It decides which questions exist, how
relevance is weighted, and what readiness measures against — and it was
possible to sit a whole board paper without ever being asked, because no
objective quietly means the entire bank. That is a reasonable default and
a poor thing to arrive at by accident. Five of six accounts here had
never set one.

It can be declined: "everything" is a real answer, and trapping somebody
behind a modal because a list failed to load would be worse than the gap
it closes. Declining is still a choice made, which is the point.

Also in this commit, from the exam-player work: Show answer in study mode
that reveals without recording an answer, review keyed on the attempt
being closed rather than every question being answered — a block that
timed out with nothing answered is over too — and the exam top and bottom
bars. That work found something worth knowing: the exam player is *served*
questions with no correct answer and no explanation, so review cannot
un-hide what it never had, and the player refetches the marked version
once the attempt closes. Nothing is revealed while a block is running.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
2026-09-12 06:24:23 +02:00

80 lines
3.1 KiB
Nginx Configuration File

server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob:; media-src 'self' blob:; connect-src 'self' https://cap.pedshub.com; font-src 'self' https://fonts.gstatic.com; frame-src 'self' https://cap.pedshub.com;" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# API proxy to backend
location /api/ {
resolver 127.0.0.11 valid=10s;
set $backend http://backend:8000;
proxy_pass $backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Large file uploads
client_max_body_size 500M;
proxy_request_buffering off;
proxy_read_timeout 600s;
}
# SCORM content — no frame-blocking headers, allow scripts
location /uploads/scorm/ {
resolver 127.0.0.11 valid=10s;
set $backend http://backend:8000;
proxy_pass $backend;
proxy_set_header Host $host;
proxy_hide_header X-Frame-Options;
proxy_force_ranges on; # Pinned backend FileResponse does not implement byte ranges.
# Local add_header prevents inheriting the app CSP; preserve backend SVG sandbox.
add_header X-Content-Type-Options "nosniff" always;
}
# Uploaded images proxy to backend
location /uploads/ {
resolver 127.0.0.11 valid=10s;
set $backend http://backend:8000;
proxy_pass $backend;
proxy_set_header Host $host;
# Backend owns private/no-store and Vary; never cache protected media.
proxy_force_ranges on;
add_header X-Content-Type-Options "nosniff" always;
}
# Hashed build assets are immutable — a new build gets a new filename.
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable" always;
add_header X-Content-Type-Options "nosniff" always;
}
# index.html must never be cached, or a deploy keeps serving the old
# bundle references and the app appears unchanged after a release.
location = /index.html {
add_header Cache-Control "no-cache, must-revalidate" always;
add_header X-Content-Type-Options "nosniff" always;
}
# SPA fallback
location / {
add_header Cache-Control "no-cache, must-revalidate" always;
try_files $uri $uri/ /index.html;
}
}