Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/baocoder288203_learn-now-nodejs.json

1 line
34 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "baocoder288203/learn-now-nodejs", "files": {"README.md": {"7fe4b149c501e8a76a93c28cec2404e0a65af23c": {"t": 1785746715.670407, "n": 7633, "h": "6746a5038270e0eb", "c": "# Learn Now API (TOEIC Practice Backend)\n\nExpress + Prisma (SQLite) API for the TOEIC practice frontend.\n\n## Prerequisites\n\n- Node.js 20+\n- `ALIBABA_API_KEY` (Alibaba Model Studio / Qwen) and/or `OPENAI_API_KEY` / `GEMINI_API_KEY` for admin AI import\n- AWS S3 bucket + IAM credentials (required for admin file upload)\n\n## Setup\n\n```bash\nnpm install\n# Chỉnh learn-now-nodejs/.env — set ALIBABA_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY cho admin import\n\nnpx prisma db push\nnpm run dev\n```\n\nAPI runs at **http://localhost:4000** (default).\n\n## Seed accounts (empty database)\n\n| Role | Email | Password |\n|---------|------------------|-----------|\n| Student | user@toeic.com | user123 |\n| Admin | admin@toeic.com | admin123 |\n\n## Scripts\n\n| Command | Description |\n|-------------------|--------------------------|\n| `npm run dev` | Dev server with hot reload |\n| `npm run build` | Bundle to `dist/index.js` |\n| `npm start` | Run production build |\n| `npm run db:push` | Apply Prisma schema to DB |\n\n## Environment\n\n| Variable | Description |\n|------------------------|--------------------------------------|\n| `PORT` | API port (default `4000`) |\n| `CORS_ORIGIN` | Comma-separated frontend URLs |\n| `AI_PROVIDER` | `auto` (default), `alibaba`, `deepseek`, `openai`, or `gemini` |\n| `AI_PROVIDER_ORDER` | Comma-separated fallback order; default `alibaba,deepseek,openai,gemini` |\n| `DEEPSEEK_API_KEY` | [DeepSeek API](https://api-docs.deepseek.com/) (OpenAI-compatible) |\n| `DEEPSEEK_BASE_URL` | Default `https://api.deepseek.com` |\n| `DEEPSEEK_MODEL` | Default `deepseek-v4-flash`; fallback `deepseek-v4-pro` via `DEEPSEEK_MODEL_FALLBACKS` |\n| `DEEPSEEK_MAX_OUTPUT_TOKENS` | Default `8192` |\n| `ALIBABA_API_KEY` | Alibaba Cloud Model Studio (Qwen), OpenAI-compatible endpoint |\n| `ALIBABA_BASE_URL` | Workspace compatible-mode URL (from Model Studio console) |\n| `ALIBABA_MODEL` | Default `qwen-plus`; vision: `ALIBABA_VISION_MODEL` (`qwen-vl-plus`) |\n| `OPENAI_API_KEY` | OpenAI for TOEIC import (fallback in `auto`) |\n| `OPENAI_MODEL` | Optional; default `gpt-4o-mini` |\n| `OPENAI_MODEL_FALLBACKS` | Comma-separated; default `gpt-4o,gpt-4.1` |\n| `OPENAI_MAX_OUTPUT_TOKENS` | Optional; default `65536` |\n| `GEMINI_API_KEY` | Google Gemini (fallback in `auto`) |\n| `JWT_SECRET` | Access token signing |\n| `JWT_REFRESH_SECRET` | Refresh token signing |\n| `AWS_ACCESS_KEY_ID` | IAM access key for S3 |\n| `AWS_SECRET_ACCESS_KEY`| IAM secret key for S3 |\n| `AWS_REGION` | S3 region (e.g. `ap-southeast-1`) |\n| `S3_BUCKET_NAME` | Target bucket name |\n| `GEMINI_MODEL` | Optional; default `gemini-2.5-flash` |\n| `GEMINI_MODEL_FALLBACKS` | Comma-separated fallback models when quota/503; default `gemini-2.0-flash,gemini-2.0-flash-lite` |\n| `GEMINI_MAX_OUTPUT_TOKENS` | Optional; default `65536` |\n| `AUTO_IMPORT_THRESHOLD`| Optional; min confidence (0–1) to skip admin review; default `0.85` |\n| `PYMUPDF_URL` | PyMuPDF sidecar (`http://pymupdf:8081` in Docker Compose) |\n| `USE_PYMUPDF_PIPELINE` | `true` (default) — rule-based TOEIC import qua PyMuPDF |\n| `PYMUPDF_TIMEOUT_MS` | Optional; timeout gọi sidecar (default `120000`) |\n| ~~`MARKITDOWN_URL`~~ | *(deprecated)* MarkItDown sidecar — không dùng nữa |\n| ~~`MARKITDOWN_TIMEOUT_MS`~~ | *(deprecated)* |\n| `AI_ENABLE_STREAMING` | `true` (default) — stream JSON; on truncate, handoff partial sang provider kế |\n| `HEADROOM_BASE_URL` | Headroom proxy (`http://headroom:8787` trong Compose; `http://127.0.0.1:8787` nếu API dev + proxy Docker). Để trống hoặc `HEADROOM_ENABLED=false` = tắt nén |\n| `HEADROOM_MIN_CHARS` | Chỉ nén prompt text ≥ N ký tự (default `2000`). Vision (KEY RC ảnh) không nén |\n| `ALIBABA_MAX_OUTPUT_TOKENS` | Default `8192` (tránh lỗi Qwen max_tokens) |\n| `OPENAI_MAX_OUTPUT_TOKENS` | Default `16384` |\n| `GEMINI_MAX_OUTPUT_TOKENS` | Default `8192` |\n\n### Import pipeline checkpoint + resume\n\nTOEIC import lưu tiến độ từng bước trong `IngestionDraft.pipelineState` (extract → RC key → Listening 1–4 → Reading 5–7 → save). Khi job `FAILED`, gọi lại chỉ các bước chưa `done` — không parse lại Part đã xong.\n\n```bash\n# Tiếp tục job sau khi hết quota AI\nPOST /api/admin/import-jobs/:jobId/resume\n```\n\nLog: `[Pipeline] step=parse_listening_2 status=skip` (đã checkpoint) hoặc `status=run`.\n\n### Docker Compose (API + Postgres + Redis + PyMuPDF + Headroom)\n\n```bash\ndocker compose up -d --build\n```\n\n| Service | Port | Mô tả |\n|---------|------|--------|\n| `api` | 4000 | Node.js API |\n| `pymupdf` | 8081 | Python sidecar — layout/text/clip PDF |\n| `headroom` | 8787 | Context compression proxy — nén prompt text trước Alibaba/OpenAI |\n| `db` | 5432 | PostgreSQL |\n| `redis` | 6379 | Redis |\n| ~~`markitdown`~~ | ~~8080~~ | *(deprecated)* Python MarkItDown sidecar |\n\nChỉ chạy sidecar (dev local, API bằng `npm run dev`):\n\n```bash\ndocker compose up -d pymupdf headroom\n# .env: PYMUPDF_URL=http://localhost:8081\n# .env: HEADROOM_BASE_URL=http://127.0.0.1:8787\n```\n\n<!-- MarkItDown deprecated:\ndocker compose up -d markitdown headroom\n# .env: MARKITDOWN_URL=http://localhost:8080\n-->\n\nHoặc proxy Headroom riêng: `docker run -d --name headroom -p 8787:8787 ghcr.io/chopratejas/headroom:latest`\n\n<!-- MarkItDown deprecated: set MARKITDOWN_URL= và bỏ depends_on markitdown -->\n\n\nUpload paths are derived from `Test.examType` (`TOEIC` → `toeic`, `IELTS` → `ielts`):\n\n| File | Key pattern |\n|------|-------------|\n| Exam PDF | `exams/{exam}/{testId}/exam.pdf` |\n| KEY LC PDF | `answers/{exam}/{testId}/key-lc.pdf` |\n| KEY RC PDF | `images/{exam}/{testId}/key-rc.pdf` |\n| Listening MP3 | `audio/{exam}/{testId}/listening.mp3` |\n| Intake (multi-file job, pre-classify) | `intake/{exam}/{testId}/{timestamp}-{name}{ext}` |\n\n`UploadedFile.filePath` stores the S3 key. Listening audio is served via presigned URLs when loading a test.\n\nDocker Compose passes the four `AWS_*` / `S3_BUCKET_NAME` variables from the host `.env` into the `api` service.\n\nSingle multi-file import flow:\n\n1. `POST /api/admin/tests/:testId/import-jobs` with multipart `files[]`\n2. Poll `GET /api/admin/import-jobs/:jobId`\n3. If `REVIEW_REQUIRED`, submit roles via `POST /api/admin/import-jobs/:jobId/review-submit`\n\nJob statuses: `QUEUED` → `EXTRACTING` → `CLASSIFYING` → (`REVIEW_REQUIRED` | `IMPORTING`) → `DONE` | `FAILED`.\n\nUpload rule for import-jobs: send files in one request with **PDF + MP3 only**.\n- At least 2 PDFs (exam + answer key docs)\n- At least 1 MP3 (listening audio for Part 1-4)\n- If you have image answer sheets, convert them to PDF before uploading.\n\n### E2E checklist (manual)\n\n1. `docker compose up -d` (postgres/sqlite per project, `api`)\n2. Admin login → create TOEIC test → select test in import panel\n3. Upload in one batch: exam PDF + key PDFs + MP3 audio (Part 1-4)\n4. Poll until `REVIEW_REQUIRED` or `DONE`\n5. If review: assign roles → Confirm & Import → poll until `DONE`\n6. Open test as student: 200 questions, listening audio plays (presigned URL)\n7. Check API logs for `jobId` on failures\n\n## Frontend\n\nPoint the React app at this API:\n\n- Dev: Vite proxy `/api` → `http://localhost:4000`, or set `VITE_API_URL=http://localhost:4000`\n- Prod: set `VITE_API_URL` to your deployed API URL and configure `CORS_ORIGIN`\n\n## Production\n\n```bash\nnpm run build\nNODE_ENV=production npm start\n```\n.\n"}, "a01689c5c72bc1cb79e2cc2b7fce8ab459ead732": {"t": 1785767224.6718392, "n": 7633, "h": "6746a5038270e0eb", "c": "# Learn Now API (TOEIC Practice Backend)\n\nExpress + Prisma (SQLite) API for the TOEIC practice frontend.\n\n## Prerequisites\n\n- Node.js 20+\n- `ALIBABA_API_KEY` (Alibaba Model Studio / Qwen) and/or `OPENAI_API_KEY` / `GEMINI_API_KEY` for admin AI import\n- AWS S3 bucket + IAM credentials (required for admin file upload)\n\n## Setup\n\n```bash\nnpm install\n# Chỉnh learn-now-nodejs/.env — set ALIBABA_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY cho admin import\n\nnpx prisma db push\nnpm run dev\n```\n\nAPI runs at **http://localhost:4000** (default).\n\n## Seed accounts (empty database)\n\n| Role | Email | Password |\n|---------|------------------|-----------|\n| Student | user@toeic.com | user123 |\n| Admin | admin@toeic.com | admin123 |\n\n## Scripts\n\n| Command | Description |\n|-------------------|--------------------------|\n| `npm run dev` | Dev server with hot reload |\n| `npm run build` | Bundle to `dist/index.js` |\n| `npm start` | Run production build |\n| `npm run db:push` | Apply Prisma schema to DB |\n\n## Environment\n\n| Variable | Description |\n|------------------------|--------------------------------------|\n| `PORT` | API port (default `4000`) |\n| `CORS_ORIGIN` | Comma-separated frontend URLs |\n| `AI_PROVIDER` | `auto` (default), `alibaba`, `deepseek`, `openai`, or `gemini` |\n| `AI_PROVIDER_ORDER` | Comma-separated fallback order; default `alibaba,deepseek,openai,gemini` |\n| `DEEPSEEK_API_KEY` | [DeepSeek API](https://api-docs.deepseek.com/) (OpenAI-compatible) |\n| `DEEPSEEK_BASE_URL` | Default `https://api.deepseek.com` |\n| `DEEPSEEK_MODEL` | Default `deepseek-v4-flash`; fallback `deepseek-v4-pro` via `DEEPSEEK_MODEL_FALLBACKS` |\n| `DEEPSEEK_MAX_OUTPUT_TOKENS` | Default `8192` |\n| `ALIBABA_API_KEY` | Alibaba Cloud Model Studio (Qwen), OpenAI-compatible endpoint |\n| `ALIBABA_BASE_URL` | Workspace compatible-mode URL (from Model Studio console) |\n| `ALIBABA_MODEL` | Default `qwen-plus`; vision: `ALIBABA_VISION_MODEL` (`qwen-vl-plus`) |\n| `OPENAI_API_KEY` | OpenAI for TOEIC import (fallback in `auto`) |\n| `OPENAI_MODEL` | Optional; default `gpt-4o-mini` |\n| `OPENAI_MODEL_FALLBACKS` | Comma-separated; default `gpt-4o,gpt-4.1` |\n| `OPENAI_MAX_OUTPUT_TOKENS` | Optional; default `65536` |\n| `GEMINI_API_KEY` | Google Gemini (fallback in `auto`) |\n| `JWT_SECRET` | Access token signing |\n| `JWT_REFRESH_SECRET` | Refresh token signing |\n| `AWS_ACCESS_KEY_ID` | IAM access key for S3 |\n| `AWS_SECRET_ACCESS_KEY`| IAM secret key for S3 |\n| `AWS_REGION` | S3 region (e.g. `ap-southeast-1`) |\n| `S3_BUCKET_NAME` | Target bucket name |\n| `GEMINI_MODEL` | Optional; default `gemini-2.5-flash` |\n| `GEMINI_MODEL_FALLBACKS` | Comma-separated fallback models when quota/503; default `gemini-2.0-flash,gemini-2.0-flash-lite` |\n| `GEMINI_MAX_OUTPUT_TOKENS` | Optional; default `65536` |\n| `AUTO_IMPORT_THRESHOLD`| Optional; min confidence (0–1) to skip admin review; default `0.85` |\n| `PYMUPDF_URL` | PyMuPDF sidecar (`http://pymupdf:8081` in Docker Compose) |\n| `USE_PYMUPDF_PIPELINE` | `true` (default) — rule-based TOEIC import qua PyMuPDF |\n| `PYMUPDF_TIMEOUT_MS` | Optional; timeout gọi sidecar (default `120000`) |\n| ~~`MARKITDOWN_URL`~~ | *(deprecated)* MarkItDown sidecar — không dùng nữa |\n| ~~`MARKITDOWN_TIMEOUT_MS`~~ | *(deprecated)* |\n| `AI_ENABLE_STREAMING` | `true` (default) — stream JSON; on truncate, handoff partial sang provider kế |\n| `HEADROOM_BASE_URL` | Headroom proxy (`http://headroom:8787` trong Compose; `http://127.0.0.1:8787` nếu API dev + proxy Docker). Để trống hoặc `HEADROOM_ENABLED=false` = tắt nén |\n| `HEADROOM_MIN_CHARS` | Chỉ nén prompt text ≥ N ký tự (default `2000`). Vision (KEY RC ảnh) không nén |\n| `ALIBABA_MAX_OUTPUT_TOKENS` | Default `8192` (tránh lỗi Qwen max_tokens) |\n| `OPENAI_MAX_OUTPUT_TOKENS` | Default `16384` |\n| `GEMINI_MAX_OUTPUT_TOKENS` | Default `8192` |\n\n### Import pipeline checkpoint + resume\n\nTOEIC import lưu tiến độ từng bước trong `IngestionDraft.pipelineState` (extract → RC key → Listening 1–4 → Reading 5–7 → save). Khi job `FAILED`, gọi lại chỉ các bước chưa `done` — không parse lại Part đã xong.\n\n```bash\n# Tiếp tục job sau khi hết quota AI\nPOST /api/admin/import-jobs/:jobId/resume\n```\n\nLog: `[Pipeline] step=parse_listening_2 status=skip` (đã checkpoint) hoặc `status=run`.\n\n### Docker Compose (API + Postgres + Redis + PyMuPDF + Headroom)\n\n```bash\ndocker compose up -d --build\n```\n\n| Service | Port | Mô tả |\n|---------|------|--------|\n| `api` | 4000 | Node.js API |\n| `pymupdf` | 8081 | Python sidecar — layout/text/clip PDF |\n| `headroom` | 8787 | Context compression proxy — nén prompt text trước Alibaba/OpenAI |\n| `db` | 5432 | PostgreSQL |\n| `redis` | 6379 | Redis |\n| ~~`markitdown`~~ | ~~8080~~ | *(deprecated)* Python MarkItDown sidecar |\n\nChỉ chạy sidecar (dev local, API bằng `npm run dev`):\n\n```bash\ndocker compose up -d pymupdf headroom\n# .env: PYMUPDF_URL=http://localhost:8081\n# .env: HEADROOM_BASE_URL=http://127.0.0.1:8787\n```\n\n<!-- MarkItDown deprecated:\ndocker compose up -d markitdown headroom\n# .env: MARKITDOWN_URL=http://localhost:8080\n-->\n\nHoặc proxy Headroom riêng: `docker run -d --name headroom -p 8787:8787 ghcr.io/chopratejas/headroom:latest`\n\n<!-- MarkItDown deprecated: set MARKITDOWN_URL= và bỏ depends_on markitdown -->\n\n\nUpload paths are derived from `Test.examType` (`TOEIC` → `toeic`, `IELTS` → `ielts`):\n\n| File | Key pattern |\n|------|-------------|\n| Exam PDF | `exams/{exam}/{testId}/exam.pdf` |\n| KEY LC PDF | `answers/{exam}/{testId}/key-lc.pdf` |\n| KEY RC PDF | `images/{exam}/{testId}/key-rc.pdf` |\n| Listening MP3 | `audio/{exam}/{testId}/listening.mp3` |\n| Intake (multi-file job, pre-classify) | `intake/{exam}/{testId}/{timestamp}-{name}{ext}` |\n\n`UploadedFile.filePath` stores the S3 key. Listening audio is served via presigned URLs when loading a test.\n\nDocker Compose passes the four `AWS_*` / `S3_BUCKET_NAME` variables from the host `.env` into the `api` service.\n\nSingle multi-file import flow:\n\n1. `POST /api/admin/tests/:testId/import-jobs` with multipart `files[]`\n2. Poll `GET /api/admin/import-jobs/:jobId`\n3. If `REVIEW_REQUIRED`, submit roles via `POST /api/admin/import-jobs/:jobId/review-submit`\n\nJob statuses: `QUEUED` → `EXTRACTING` → `CLASSIFYING` → (`REVIEW_REQUIRED` | `IMPORTING`) → `DONE` | `FAILED`.\n\nUpload rule for import-jobs: send files in one request with **PDF + MP3 only**.\n- At least 2 PDFs (exam + answer key docs)\n- At least 1 MP3 (listening audio for Part 1-4)\n- If you have image answer sheets, convert them to PDF before uploading.\n\n### E2E checklist (manual)\n\n1. `docker compose up -d` (postgres/sqlite per project, `api`)\n2. Admin login → create TOEIC test → select test in import panel\n3. Upload in one batch: exam PDF + key PDFs + MP3 audio (Part 1-4)\n4. Poll until `REVIEW_REQUIRED` or `DONE`\n5. If review: assign roles → Confirm & Import → poll until `DONE`\n6. Open test as student: 200 questions, listening audio plays (presigned URL)\n7. Check API logs for `jobId` on failures\n\n## Frontend\n\nPoint the React app at this API:\n\n- Dev: Vite proxy `/api` → `http://localhost:4000`, or set `VITE_API_URL=http://localhost:4000`\n- Prod: set `VITE_API_URL` to your deployed API URL and configure `CORS_ORIGIN`\n\n## Production\n\n```bash\nnpm run build\nNODE_ENV=production npm start\n```\n.\n"}, "83904fc9b489d41e34acb628cff66f605dd012a1": {"t": 1785767226.9813328, "n": 7393, "h": "8006432094735a86", "c": "# Learn Now API (TOEIC Practice Backend)\n\nExpress + Prisma (SQLite) API for the TOEIC practice frontend.\n\n## Prerequisites\n\n- Node.js 20+\n- `ALIBABA_API_KEY` (Alibaba Model Studio / Qwen) and/or `OPENAI_API_KEY` / `GEMINI_API_KEY` for admin AI import\n- AWS S3 bucket + IAM credentials (required for admin file upload)\n\n## Setup\n\n```bash\nnpm install\n# Chỉnh learn-now-nodejs/.env — set ALIBABA_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY cho admin import\n\nnpx prisma db push\nnpm run dev\n```\n\nAPI runs at **http://localhost:4000** (default).\n\n## Seed accounts (empty database)\n\n| Role | Email | Password |\n|---------|------------------|-----------|\n| Student | user@toeic.com | user123 |\n| Admin | admin@toeic.com | admin123 |\n\n## Scripts\n\n| Command | Description |\n|-------------------|--------------------------|\n| `npm run dev` | Dev server with hot reload |\n| `npm run build` | Bundle to `dist/index.js` |\n| `npm start` | Run production build |\n| `npm run db:push` | Apply Prisma schema to DB |\n\n## Environment\n\n| Variable | Description |\n|------------------------|--------------------------------------|\n| `PORT` | API port (default `4000`) |\n| `CORS_ORIGIN` | Comma-separated frontend URLs |\n| `AI_PROVIDER` | `auto` (default), `alibaba`, `deepseek`, `openai`, or `gemini` |\n| `AI_PROVIDER_ORDER` | Comma-separated fallback order; default `alibaba,deepseek,openai,gemini` |\n| `DEEPSEEK_API_KEY` | [DeepSeek API](https://api-docs.deepseek.com/) (OpenAI-compatible) |\n| `DEEPSEEK_BASE_URL` | Default `https://api.deepseek.com` |\n| `DEEPSEEK_MODEL` | Default `deepseek-v4-flash`; fallback `deepseek-v4-pro` via `DEEPSEEK_MODEL_FALLBACKS` |\n| `DEEPSEEK_MAX_OUTPUT_TOKENS` | Default `8192` |\n| `ALIBABA_API_KEY` | Alibaba Cloud Model Studio (Qwen), OpenAI-compatible endpoint |\n| `ALIBABA_BASE_URL` | Workspace compatible-mode URL (from Model Studio console) |\n| `ALIBABA_MODEL` | Default `qwen-plus`; vision: `ALIBABA_VISION_MODEL` (`qwen-vl-plus`) |\n| `OPENAI_API_KEY` | OpenAI for TOEIC import (fallback in `auto`) |\n| `OPENAI_MODEL` | Optional; default `gpt-4o-mini` |\n| `OPENAI_MODEL_FALLBACKS` | Comma-separated; default `gpt-4o,gpt-4.1` |\n| `OPENAI_MAX_OUTPUT_TOKENS` | Optional; default `65536` |\n| `GEMINI_API_KEY` | Google Gemini (fallback in `auto`) |\n| `JWT_SECRET` | Access token signing |\n| `JWT_REFRESH_SECRET` | Refresh token signing |\n| `AWS_ACCESS_KEY_ID` | IAM access key for S3 |\n| `AWS_SECRET_ACCESS_KEY`| IAM secret key for S3 |\n| `AWS_REGION` | S3 region (e.g. `ap-southeast-1`) |\n| `S3_BUCKET_NAME` | Target bucket name |\n| `GEMINI_MODEL` | Optional; default `gemini-2.5-flash` |\n| `GEMINI_MODEL_FALLBACKS` | Comma-separated fallback models when quota/503; default `gemini-2.0-flash,gemini-2.0-flash-lite` |\n| `GEMINI_MAX_OUTPUT_TOKENS` | Optional; default `65536` |\n| `AUTO_IMPORT_THRESHOLD`| Optional; min confidence (0–1) to skip admin review; default `0.85` |\n| `MARKITDOWN_URL` | MarkItDown sidecar URL (`http://markitdown:8080` in Docker Compose; `http://localhost:8080` if API chạy ngoài Docker). Để trống = chỉ dùng pdf-parse |\n| `MARKITDOWN_TIMEOUT_MS` | Optional; timeout gọi sidecar (default `120000`) |\n| `AI_ENABLE_STREAMING` | `true` (default) — stream JSON; on truncate, handoff partial sang provider kế |\n| `HEADROOM_BASE_URL` | Headroom proxy (`http://headroom:8787` trong Compose; `http://127.0.0.1:8787` nếu API dev + proxy Docker). Để trống hoặc `HEADROOM_ENABLED=false` = tắt nén |\n| `HEADROOM_MIN_CHARS` | Chỉ nén prompt text ≥ N ký tự (default `2000`). Vision (KEY RC ảnh) không nén |\n| `ALIBABA_MAX_OUTPUT_TOKENS` | Default `8192` (tránh lỗi Qwen max_tokens) |\n| `OPENAI_MAX_OUTPUT_TOKENS` | Default `16384` |\n| `GEMINI_MAX_OUTPUT_TOKENS` | Default `8192` |\n\n### Import pipeline checkpoint + resume\n\nTOEIC import lưu tiến độ từng bước trong `IngestionDraft.pipelineState` (extract → RC key → Listening 1–4 → Reading 5–7 → save). Khi job `FAILED`, gọi lại chỉ các bước chưa `done` — không parse lại Part đã xong.\n\n```bash\n# Tiếp tục job sau khi hết quota AI\nPOST /api/admin/import-jobs/:jobId/resume\n```\n\nLog: `[Pipeline] step=parse_listening_2 status=skip` (đã checkpoint) hoặc `status=run`.\n\n### Docker Compose (API + Postgres + Redis + MarkItDown + Headroom)\n\n```bash\ndocker compose up -d --build\n```\n\n| Service | Port | Mô tả |\n|---------|------|--------|\n| `api` | 4000 | Node.js API |\n| `markitdown` | 8080 | Python sidecar — `POST /convert`, `GET /health` |\n| `headroom` | 8787 | Context compression proxy — nén prompt text trước Alibaba/OpenAI |\n| `db` | 5432 | PostgreSQL |\n| `redis` | 6379 | Redis |\n\nChỉ chạy sidecar (dev local, API bằng `npm run dev`):\n\n```bash\ndocker compose up -d markitdown headroom\n# .env: MARKITDOWN_URL=http://localhost:8080\n# .env: HEADROOM_BASE_URL=http://127.0.0.1:8787\n```\n\nHoặc proxy Headroom riêng: `docker run -d --name headroom -p 8787:8787 ghcr.io/chopratejas/headroom:latest`\n\nTắt MarkItDown (chỉ pdf-parse): trong `.env` set `MARKITDOWN_URL=` (rỗng) và bỏ `depends_on` markitdown hoặc không start service `markitdown`.\n\n\nUpload paths are derived from `Test.examType` (`TOEIC` → `toeic`, `IELTS` → `ielts`):\n\n| File | Key pattern |\n|------|-------------|\n| Exam PDF | `exams/{exam}/{testId}/exam.pdf` |\n| KEY LC PDF | `answers/{exam}/{testId}/key-lc.pdf` |\n| KEY RC PDF | `images/{exam}/{testId}/key-rc.pdf` |\n| Listening MP3 | `audio/{exam}/{testId}/listening.mp3` |\n| Intake (multi-file job, pre-classify) | `intake/{exam}/{testId}/{timestamp}-{name}{ext}` |\n\n`UploadedFile.filePath` stores the S3 key. Listening audio is served via presigned URLs when loading a test.\n\nDocker Compose passes the four `AWS_*` / `S3_BUCKET_NAME` variables from the host `.env` into the `api` service.\n\nSingle multi-file import flow:\n\n1. `POST /api/admin/tests/:testId/import-jobs` with multipart `files[]`\n2. Poll `GET /api/admin/import-jobs/:jobId`\n3. If `REVIEW_REQUIRED`, submit roles via `POST /api/admin/import-jobs/:jobId/review-submit`\n\nJob statuses: `QUEUED` → `EXTRACTING` → `CLASSIFYING` → (`REVIEW_REQUIRED` | `IMPORTING`) → `DONE` | `FAILED`.\n\nUpload rule for import-jobs: send files in one request with **PDF + MP3 only**.\n- At least 2 PDFs (exam + answer key docs)\n- At least 1 MP3 (listening audio for Part 1-4)\n- If you have image answer sheets, convert them to PDF before uploading.\n\n### E2E checklist (manual)\n\n1. `docker compose up -d` (postgres/sqlite per project, `api`)\n2. Admin login → create TOEIC test → select test in import panel\n3. Upload in one batch: exam PDF + key PDFs + MP3 audio (Part 1-4)\n4. Poll until `REVIEW_REQUIRED` or `DONE`\n5. If review: assign roles → Confirm & Import → poll until `DONE`\n6. Open test as student: 200 questions, listening audio plays (presigned URL)\n7. Check API logs for `jobId` on failures\n\n## Frontend\n\nPoint the React app at this API:\n\n- Dev: Vite proxy `/api` → `http://localhost:4000`, or set `VITE_API_URL=http://localhost:4000`\n- Prod: set `VITE_API_URL` to your deployed API URL and configure `CORS_ORIGIN`\n\n## Production\n\n```bash\nnpm run build\nNODE_ENV=production npm start\n```\n.\n"}, "aadd4f598af7552b775adbf74189dae73126f440": {"t": 1785767228.4310558, "n": 4106, "h": "26ac6844285a6523", "c": "# Learn Now API (TOEIC Practice Backend)\n\nExpress + Prisma (SQLite) API for the TOEIC practice frontend.\n\n## Prerequisites\n\n- Node.js 20+\n- `GEMINI_API_KEY` (required for admin AI import)\n- AWS S3 bucket + IAM credentials (required for admin file upload)\n\n## Setup\n\n```bash\nnpm install\ncp .env.example .env\n# Edit .env — set GEMINI_API_KEY if using admin import\n\nnpx prisma db push\nnpm run dev\n```\n\nAPI runs at **http://localhost:4000** (default).\n\n## Seed accounts (empty database)\n\n| Role | Email | Password |\n|---------|------------------|-----------|\n| Student | user@toeic.com | user123 |\n| Admin | admin@toeic.com | admin123 |\n\n## Scripts\n\n| Command | Description |\n|-------------------|--------------------------|\n| `npm run dev` | Dev server with hot reload |\n| `npm run build` | Bundle to `dist/index.js` |\n| `npm start` | Run production build |\n| `npm run db:push` | Apply Prisma schema to DB |\n\n## Environment\n\n| Variable | Description |\n|------------------------|--------------------------------------|\n| `PORT` | API port (default `4000`) |\n| `CORS_ORIGIN` | Comma-separated frontend URLs |\n| `GEMINI_API_KEY` | Google Gemini for OCR import |\n| `JWT_SECRET` | Access token signing |\n| `JWT_REFRESH_SECRET` | Refresh token signing |\n| `AWS_ACCESS_KEY_ID` | IAM access key for S3 |\n| `AWS_SECRET_ACCESS_KEY`| IAM secret key for S3 |\n| `AWS_REGION` | S3 region (e.g. `ap-southeast-1`) |\n| `S3_BUCKET_NAME` | Target bucket name |\n| `GEMINI_MODEL` | Optional; default `gemini-2.5-flash` |\n| `GEMINI_MODEL_FALLBACKS` | Comma-separated fallback models when quota/503; default `gemini-2.0-flash,gemini-2.0-flash-lite` |\n| `GEMINI_MAX_OUTPUT_TOKENS` | Optional; default `65536` |\n| `AUTO_IMPORT_THRESHOLD`| Optional; min confidence (0–1) to skip admin review; default `0.85` |\n\n### S3 object key layout\n\nUpload paths are derived from `Test.examType` (`TOEIC` → `toeic`, `IELTS` → `ielts`):\n\n| File | Key pattern |\n|------|-------------|\n| Exam PDF | `exams/{exam}/{testId}/exam.pdf` |\n| KEY LC PDF | `answers/{exam}/{testId}/key-lc.pdf` |\n| KEY RC PDF | `images/{exam}/{testId}/key-rc.pdf` |\n| Listening MP3 | `audio/{exam}/{testId}/listening.mp3` |\n| Intake (multi-file job, pre-classify) | `intake/{exam}/{testId}/{timestamp}-{name}{ext}` |\n\n`UploadedFile.filePath` stores the S3 key. Listening audio is served via presigned URLs when loading a test.\n\nDocker Compose passes the four `AWS_*` / `S3_BUCKET_NAME` variables from the host `.env` into the `api` service.\n\nSingle multi-file import flow:\n\n1. `POST /api/admin/tests/:testId/import-jobs` with multipart `files[]`\n2. Poll `GET /api/admin/import-jobs/:jobId`\n3. If `REVIEW_REQUIRED`, submit roles via `POST /api/admin/import-jobs/:jobId/review-submit`\n\nJob statuses: `QUEUED` → `EXTRACTING` → `CLASSIFYING` → (`REVIEW_REQUIRED` | `IMPORTING`) → `DONE` | `FAILED`.\n\nUpload rule for import-jobs: send files in one request with **PDF + MP3 only**.\n- At least 2 PDFs (exam + answer key docs)\n- At least 1 MP3 (listening audio for Part 1-4)\n- If you have image answer sheets, convert them to PDF before uploading.\n\n### E2E checklist (manual)\n\n1. `docker compose up -d` (postgres/sqlite per project, `api`)\n2. Admin login → create TOEIC test → select test in import panel\n3. Upload in one batch: exam PDF + key PDFs + MP3 audio (Part 1-4)\n4. Poll until `REVIEW_REQUIRED` or `DONE`\n5. If review: assign roles → Confirm & Import → poll until `DONE`\n6. Open test as student: 200 questions, listening audio plays (presigned URL)\n7. Check API logs for `jobId` on failures\n\n## Frontend\n\nPoint the React app at this API:\n\n- Dev: Vite proxy `/api` → `http://localhost:4000`, or set `VITE_API_URL=http://localhost:4000`\n- Prod: set `VITE_API_URL` to your deployed API URL and configure `CORS_ORIGIN`\n\n## Production\n\n```bash\nnpm run build\nNODE_ENV=production npm start\n```\n.\n"}, "c3377474662f4606fab6c8a2b51b34582f6883d0": {"t": 1785767230.2031598, "n": 4104, "h": "ec5b88b499485964", "c": "# Learn Now API (TOEIC Practice Backend)\n\nExpress + Prisma (SQLite) API for the TOEIC practice frontend.\n\n## Prerequisites\n\n- Node.js 20+\n- `GEMINI_API_KEY` (required for admin AI import)\n- AWS S3 bucket + IAM credentials (required for admin file upload)\n\n## Setup\n\n```bash\nnpm install\ncp .env.example .env\n# Edit .env — set GEMINI_API_KEY if using admin import\n\nnpx prisma db push\nnpm run dev\n```\n\nAPI runs at **http://localhost:4000** (default).\n\n## Seed accounts (empty database)\n\n| Role | Email | Password |\n|---------|------------------|-----------|\n| Student | user@toeic.com | user123 |\n| Admin | admin@toeic.com | admin123 |\n\n## Scripts\n\n| Command | Description |\n|-------------------|--------------------------|\n| `npm run dev` | Dev server with hot reload |\n| `npm run build` | Bundle to `dist/index.js` |\n| `npm start` | Run production build |\n| `npm run db:push` | Apply Prisma schema to DB |\n\n## Environment\n\n| Variable | Description |\n|------------------------|--------------------------------------|\n| `PORT` | API port (default `4000`) |\n| `CORS_ORIGIN` | Comma-separated frontend URLs |\n| `GEMINI_API_KEY` | Google Gemini for OCR import |\n| `JWT_SECRET` | Access token signing |\n| `JWT_REFRESH_SECRET` | Refresh token signing |\n| `AWS_ACCESS_KEY_ID` | IAM access key for S3 |\n| `AWS_SECRET_ACCESS_KEY`| IAM secret key for S3 |\n| `AWS_REGION` | S3 region (e.g. `ap-southeast-1`) |\n| `S3_BUCKET_NAME` | Target bucket name |\n| `GEMINI_MODEL` | Optional; default `gemini-2.5-flash` |\n| `GEMINI_MODEL_FALLBACKS` | Comma-separated fallback models when quota/503; default `gemini-2.0-flash,gemini-2.0-flash-lite` |\n| `GEMINI_MAX_OUTPUT_TOKENS` | Optional; default `65536` |\n| `AUTO_IMPORT_THRESHOLD`| Optional; min confidence (0–1) to skip admin review; default `0.85` |\n\n### S3 object key layout\n\nUpload paths are derived from `Test.examType` (`TOEIC` → `toeic`, `IELTS` → `ielts`):\n\n| File | Key pattern |\n|------|-------------|\n| Exam PDF | `exams/{exam}/{testId}/exam.pdf` |\n| KEY LC PDF | `answers/{exam}/{testId}/key-lc.pdf` |\n| KEY RC PDF | `images/{exam}/{testId}/key-rc.pdf` |\n| Listening MP3 | `audio/{exam}/{testId}/listening.mp3` |\n| Intake (multi-file job, pre-classify) | `intake/{exam}/{testId}/{timestamp}-{name}{ext}` |\n\n`UploadedFile.filePath` stores the S3 key. Listening audio is served via presigned URLs when loading a test.\n\nDocker Compose passes the four `AWS_*` / `S3_BUCKET_NAME` variables from the host `.env` into the `api` service.\n\nSingle multi-file import flow:\n\n1. `POST /api/admin/tests/:testId/import-jobs` with multipart `files[]`\n2. Poll `GET /api/admin/import-jobs/:jobId`\n3. If `REVIEW_REQUIRED`, submit roles via `POST /api/admin/import-jobs/:jobId/review-submit`\n\nJob statuses: `QUEUED` → `EXTRACTING` → `CLASSIFYING` → (`REVIEW_REQUIRED` | `IMPORTING`) → `DONE` | `FAILED`.\n\nUpload rule for import-jobs: send files in one request with **PDF + MP3 only**.\n- At least 2 PDFs (exam + answer key docs)\n- At least 1 MP3 (listening audio for Part 1-4)\n- If you have image answer sheets, convert them to PDF before uploading.\n\n### E2E checklist (manual)\n\n1. `docker compose up -d` (postgres/sqlite per project, `api`)\n2. Admin login → create TOEIC test → select test in import panel\n3. Upload in one batch: exam PDF + key PDFs + MP3 audio (Part 1-4)\n4. Poll until `REVIEW_REQUIRED` or `DONE`\n5. If review: assign roles → Confirm & Import → poll until `DONE`\n6. Open test as student: 200 questions, listening audio plays (presigned URL)\n7. Check API logs for `jobId` on failures\n\n## Frontend\n\nPoint the React app at this API:\n\n- Dev: Vite proxy `/api` → `http://localhost:4000`, or set `VITE_API_URL=http://localhost:4000`\n- Prod: set `VITE_API_URL` to your deployed API URL and configure `CORS_ORIGIN`\n\n## Production\n\n```bash\nnpm run build\nNODE_ENV=production npm start\n```\n"}, "d5b4f182538891dea0d9fc4e563160c9d2fdc020": {"t": 1785767231.4823887, "n": 1680, "h": "efaf5d2e6665307c", "c": "# Learn Now API (TOEIC Practice Backend)\n\nExpress + Prisma (SQLite) API for the TOEIC practice frontend.\n\n## Prerequisites\n\n- Node.js 20+\n- `GEMINI_API_KEY` (required only for admin AI import)\n\n## Setup\n\n```bash\nnpm install\ncp .env.example .env\n# Edit .env — set GEMINI_API_KEY if using admin import\n\nnpx prisma db push\nnpm run dev\n```\n\nAPI runs at **http://localhost:4000** (default).\n\n## Seed accounts (empty database)\n\n| Role | Email | Password |\n|---------|------------------|-----------|\n| Student | user@toeic.com | user123 |\n| Admin | admin@toeic.com | admin123 |\n\n## Scripts\n\n| Command | Description |\n|-------------------|--------------------------|\n| `npm run dev` | Dev server with hot reload |\n| `npm run build` | Bundle to `dist/index.js` |\n| `npm start` | Run production build |\n| `npm run db:push` | Apply Prisma schema to DB |\n\n## Environment\n\n| Variable | Description |\n|------------------------|--------------------------------------|\n| `PORT` | API port (default `4000`) |\n| `CORS_ORIGIN` | Comma-separated frontend URLs |\n| `GEMINI_API_KEY` | Google Gemini for OCR import |\n| `JWT_SECRET` | Access token signing |\n| `JWT_REFRESH_SECRET` | Refresh token signing |\n\n## Frontend\n\nPoint the React app at this API:\n\n- Dev: Vite proxy `/api` → `http://localhost:4000`, or set `VITE_API_URL=http://localhost:4000`\n- Prod: set `VITE_API_URL` to your deployed API URL and configure `CORS_ORIGIN`\n\n## Production\n\n```bash\nnpm run build\nNODE_ENV=production npm start\n```\n"}}}}