feat: 20 MB on an upload, and delete one where they are listed
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
This commit is contained in:
parent
8836993ca1
commit
3d7c619461
6 changed files with 101 additions and 4 deletions
62
backend/.env.bak-160955
Normal file
62
backend/.env.bak-160955
Normal file
|
|
@ -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=QJ3GmEdB5wK09MqVj+rSKXK5GKAA0G+DBva9EAZP
|
||||
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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</small>
|
||||
</Link>
|
||||
<span className={`badge badge-${doc.status}`}>{doc.status}</span>
|
||||
{/* 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 ? (
|
||||
<span className="tools-doc-confirm">
|
||||
<button type="button" className="btn btn-danger btn-sm"
|
||||
onClick={() => remove(doc.id)}>Delete it</button>
|
||||
<button type="button" className="btn btn-secondary btn-sm"
|
||||
onClick={() => setRemoving(null)}>Keep</button>
|
||||
</span>
|
||||
) : (
|
||||
<button type="button" className="btn btn-secondary btn-sm"
|
||||
onClick={() => setRemoving(doc.id)}>Delete</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export default function UploadPage() {
|
|||
<div className="card">
|
||||
<h2>Upload PDF Document</h2>
|
||||
<p style={{ color: 'var(--text-muted)', marginBottom: 20, fontSize: '0.9rem' }}>
|
||||
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.
|
||||
</p>
|
||||
|
||||
{error && <div className="alert alert-error">{error}</div>}
|
||||
|
|
@ -76,7 +76,7 @@ export default function UploadPage() {
|
|||
<div>
|
||||
<div style={{ fontSize: '2rem', marginBottom: 8 }}>PDF</div>
|
||||
<div>Click or drag a PDF file here</div>
|
||||
<div style={{ color: 'var(--text-subtle)', fontSize: '0.85rem', marginTop: 4 }}>Supports files up to 500MB</div>
|
||||
<div style={{ color: 'var(--text-subtle)', fontSize: '0.85rem', marginTop: 4 }}>Up to 20 MB</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue