From 3d7c619461c7488555aed262d41621064254a129 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 13 Sep 2026 16:12:29 +0200 Subject: [PATCH] feat: 20 MB on an upload, and delete one where they are listed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A source PDF is chunked, vectorised and then read by a model a chunk at a time, so a 500 MB upload is not a big file — it is an hour of work nobody asked for and a bucket that grows for ever. The cap is 20 MB, in the config default, in backend/.env which was overriding it at 524288000, and in nginx, which was letting 500M through to be refused by the application afterwards. A backup of .env is beside it. And a document is deleted where the documents are listed. The endpoint has always existed and removes the file, the vector collection and the row — but reaching it meant opening the document first, which is a page you go to in order to extract from it, not somewhere you visit to tidy up. Delete, then Delete it or Keep, in the workbench list. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN --- backend/.env.bak-160955 | 62 +++++++++++++++++++++++++++++++ backend/app/config.py | 6 ++- frontend/nginx.conf | 2 +- frontend/src/pages/ToolsPage.css | 3 ++ frontend/src/pages/ToolsPage.jsx | 28 ++++++++++++++ frontend/src/pages/UploadPage.jsx | 4 +- 6 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 backend/.env.bak-160955 diff --git a/backend/.env.bak-160955 b/backend/.env.bak-160955 new file mode 100644 index 0000000..cfef892 --- /dev/null +++ b/backend/.env.bak-160955 @@ -0,0 +1,62 @@ +DATABASE_URL=postgresql://pedquiz:pedquiz_secret@postgres:5432/pedquiz +SECRET_KEY=a787624f6ff8331ba39b0c759fc2ae21dd06898062d81cc9433abd3206cd8f7c +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=1440 + +REDIS_URL=redis://redis:6379/0 + +LITELLM_MODEL=openrouter-claude-haiku-4.5 +LITELLM_API_KEY=sk-Lu70lYqqWh863YdMJohviA +LITELLM_API_BASE=https://llm.danvics.com +LITELLM_EMBEDDING_MODEL=openrouter-bge-m3 +LITELLM_RERANK_MODEL=cohere-rerank-v4.0-pro + +#OPENAI_API_KEY=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 +#ELEVENLABS_API_KEY=sk_4d43f4cabd67c42850adf8117f8b21b6e24418c1e2dd7491 +#GOOGLE_TTS_API_KEY= +LOCAL_SPEECH_GATEWAY_URL=http://local-speech-gateway:8110 +AWS_ACCESS_KEY_ID=AKIAZP6UAHW7JS5GC6DK +AWS_SECRET_ACCESS_KEY=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 +AWS_REGION=us-east-1 +AWS_BEDROCK_REGION=us-east-1 + +CHROMA_PERSIST_DIR=/app/chroma_data + +MAIL_USERNAME=danvics.com +MAIL_PASSWORD=YFZ2L8F06YUYBfBR +MAIL_FROM=noreply@pedshub.com +MAIL_PORT=587 +MAIL_SERVER=mail.smtp2go.com +MAIL_STARTTLS=true +MAIL_SSL_TLS=false + +UPLOAD_DIR=/app/uploads +MAX_UPLOAD_SIZE=524288000 +APP_URL=https://pedshub.com +# Blank until the hCaptcha secret is issued; the challenge is skipped while it is. +CAP_SECRET_KEY=sk-55R5UwjN96vkTaZMv5pbM5rNrxIGfoJCztKRndFNJtk +CAP_SITE_KEY=863be850d3 +CAP_API_URL=http://cap:3000 +ADMIN_EMAIL=dan@danvics.com + +# Media storage. 'local' keeps the container volume; 's3' uses MinIO. +# Reads fall back to the volume either way, so existing uploads keep working. +STORAGE_BACKEND=s3 +S3_ENDPOINT_URL=http://quiz-minio:9000 +S3_ACCESS_KEY=pedshub +S3_SECRET_KEY=B4162G4nVvPsHeka4JLBP-GKvXiP2Lmm +S3_BUCKET=pedshub-media + +# Clinical library index (Milvus, ped-ai stack). Read-only: articles are written +# from what it retrieves, and nothing here ever writes to it. +CLINICAL_MILVUS_URI=http://milvus:19530 +CLINICAL_MILVUS_TOKEN=clinical_query:9950b7d40fd318fd7ecb658a68b40df68f5337ee11c2ac85 +CLINICAL_MILVUS_COLLECTION=mcp_bge_m3_1024 +OIDC_PROVIDER_URL=https://sso.pedshub.com/application/o/pedshub/ +OIDC_CLIENT_ID=YY8v0cH523GCHTfK76d88IlAiuWiQSxHb1QiJXaM +OIDC_CLIENT_SECRET=cYVU3WtW1On6hWWsWeehUlOGvlAV5OWz0Sysk0VwF5EnB6Trm97FCsifKcpe7a0D67BIKqdq8B31nQCbkt4dZvdaZ3tUYZxPR5OlLWizbB51iJfUKSvPpfPb6TeRr4Bx +OIDC_SCOPES=openid email profile +OIDC_PROVIDER_NAME=PedsHub SSO +OIDC_ROLE_CLAIM=groups +OIDC_ADMIN_GROUPS=pedshub-admins +OIDC_MODERATOR_GROUPS=pedshub-moderators diff --git a/backend/app/config.py b/backend/app/config.py index 89da3ab..f6dcff1 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -58,7 +58,11 @@ class Settings(BaseSettings): S3_SECRET_KEY: str = "" S3_BUCKET: str = "pedshub-media" S3_REGION: str = "us-east-1" - MAX_UPLOAD_SIZE: int = 524288000 # 500MB + # 20 MB. A source PDF is chunked, vectorised and then read by a model a + # chunk at a time, so a 500 MB upload is not a big file — it is an hour of + # work nobody asked for and a bucket that grows for ever. Anything larger + # is split before it comes in. + MAX_UPLOAD_SIZE: int = 20971520 # 20MB # hCaptcha. Leave the secret blank to disable the challenge entirely. # Cap, self-hosted beside us. The secret verifies a solve and never leaves diff --git a/frontend/nginx.conf b/frontend/nginx.conf index 593da31..61a4222 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -48,7 +48,7 @@ server { proxy_set_header X-Forwarded-Proto $scheme; # Large file uploads - client_max_body_size 500M; + client_max_body_size 24M; # 20 MB of PDF plus the multipart wrapper proxy_request_buffering off; proxy_read_timeout 600s; } diff --git a/frontend/src/pages/ToolsPage.css b/frontend/src/pages/ToolsPage.css index c15affe..d96c85a 100644 --- a/frontend/src/pages/ToolsPage.css +++ b/frontend/src/pages/ToolsPage.css @@ -66,3 +66,6 @@ font-size: .85rem; font-weight: 600; color: var(--primary); text-decoration: none; } .tools-jobs-link:hover { text-decoration: underline; } + +/* Delete, and the two buttons it turns into. */ +.tools-doc-confirm { display: inline-flex; gap: 6px; } diff --git a/frontend/src/pages/ToolsPage.jsx b/frontend/src/pages/ToolsPage.jsx index bb4c8cb..d52e510 100644 --- a/frontend/src/pages/ToolsPage.jsx +++ b/frontend/src/pages/ToolsPage.jsx @@ -31,6 +31,19 @@ export default function ToolsPage() { const [open, setOpen] = useState(null) const [showDone, setShowDone] = useState(false) const [labsOpen, setLabsOpen] = useState(false) + //: Which document is asking to be confirmed. A PDF is the heaviest thing + //: anybody uploads and the one most often uploaded twice by mistake. + const [removing, setRemoving] = useState(null) + + const remove = async (id) => { + setRemoving(null) + try { + await api.delete(`/documents/${id}`) + setDocuments(rows => rows.filter(row => row.id !== id)) + } catch { + setError('Could not delete that document') + } + } const load = useCallback(() => { Promise.all([ @@ -99,6 +112,21 @@ export default function ToolsPage() { {doc.status} + {/* Where the documents are listed is where one is thrown away. + Deleting meant opening the document first, which is a page + you go to in order to extract from it — not somewhere you + visit to tidy up. */} + {removing === doc.id ? ( + + + + + ) : ( + + )} ))} diff --git a/frontend/src/pages/UploadPage.jsx b/frontend/src/pages/UploadPage.jsx index 2f263f7..0c80878 100644 --- a/frontend/src/pages/UploadPage.jsx +++ b/frontend/src/pages/UploadPage.jsx @@ -54,7 +54,7 @@ export default function UploadPage() {

Upload PDF Document

- Upload a PDF file (up to 500MB) to generate interactive quizzes. + Upload a PDF (up to 20 MB) to extract questions from. Larger books are split before they come in — a source is read a chunk at a time, so size is work rather than storage.

{error &&
{error}
} @@ -76,7 +76,7 @@ export default function UploadPage() {
PDF
Click or drag a PDF file here
-
Supports files up to 500MB
+
Up to 20 MB
)}