- 注册 / 新建用户 / 批量建用户:先算密码哈希,再同步地查重和插入, 同名并发请求返回 400 而不是撞唯一约束变成 500 - 上传任务素材改用 INSERT ... ON CONFLICT DO UPDATE - CLAUDE.md 补充同步阻塞、行内大文本列、索引、类型差异等注意事项 - 忽略根目录的 db.sqlite3(线上数据备份) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.7 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
WebPreview is a web-based tutorial/challenge platform where users view tutorials and submit HTML/CSS/JS code for challenges. It has a Bun backend (server/) and a Vue 3 frontend (client/) in one repository, deployed via Docker Compose with Caddy as reverse proxy.
The backend was rewritten from Django in 2026-10 to cut memory use (~25 MB vs. 1–2 GB). It keeps Django's exact database schema (table names, column types, UTC YYYY-MM-DD HH:MM:SS.ffffff timestamps, 32-char hex UUIDs, pbkdf2_sha256$... password hashes) so old data loads unchanged. The Django code is in git history before that change.
Architecture
-
server/— Bun + Hono HTTP API, Bun's native WebSocket,bun:sqlite(single SQLite file, WAL). No Redis, no Postgres.src/index.ts—Bun.serve: WebSocket upgrades go tows.ts, everything else to the Hono appsrc/app.ts— mounts routers under/api/<prefix>, CORS, Origin check on writes, session middleware, error →{ detail }JSON (ninja-compatible; validation errors are 422)src/db.ts— schema (CREATE TABLE IF NOT EXISTS, same DDL as Django),all/get/run/txhelpers using$nameparams, time (now(),iso()) and UUID (uuidHex(),uuidOut()) conversionssrc/auth.ts— Django-compatible password hashing, sessions in tableauth_session(cookiesessionid),requireUser/requireAdmin/requireSuper(401 / 403)src/routes/—account,tasks(tutorial / challenge / creation),files(task assets + image upload),submission,showcase(mounted under/submission/showcase),promptsrc/services/—score.ts(rating weights super×0.5, admin×0.3, normal×0.2, then Bayesian smoothing with C=3 and zones),gradebook.ts,llm.ts(OpenAI-compatible streaming via fetch; DeepSeek default, ARK for doubao models),classifier.ts(Bloom-level L1–L6 prompt tagging, runs in background),cascade.ts(Django's CASCADE deletes, done by hand),pyround.ts(pyRound/pySumreproduce Python'sround()andsum()so scores match the old system bit-for-bit)src/ws.ts—/ws/prompt/<task_id>/(code generation chat) and/ws/guidance/<task_id>/(prompt coaching); messages per connection are processed serially; on disconnect the pending user message is deleted and the LLM request abortedscripts/— one-off CLIs:migrate-from-postgres.ts,create-superuser.ts,classify-prompts.ts,recalculate-scores.ts- Roles:
super,admin,normal. Task model is multi-table:task_task+ one oftask_tutorial/task_challenge/task_creation(task_ptr_id)
-
client/— Vue 3 + TypeScript SPA built with Rsbuild- UI framework: Naive UI (auto-imported via
unplugin-vue-components) src/api.ts— Axios-based API client; 401 clears login and goes home, 403 alerts and goes homesrc/router.ts— Vue Router with auth guardssrc/store/— Reactive state (plain Vue refs, not Vuex/Pinia);prompt.ts/guidance.tsown the WebSocketssrc/pages/,src/components/— user management usescomponents/dashboard/UserFormModal.vue(there is no Django Admin anymore)
- UI framework: Naive UI (auto-imported via
-
Caddyfile— serves SPA frombuild/and/media, proxies/api*and/ws*to the backend -
docker-compose.yaml—webpreview-web(Caddy) andwebpreview-api(Bun);webpreview-db(Postgres) exists only under themigrateprofile for the one-time data migration -
.github/workflows/deploy.yaml— on push tomain: build client, rsync to serverbuild/, thengit pull && docker compose up -d --buildon the server
Development Commands
./dev.sh # backend (bun --watch) + frontend together
Server (from server/)
bun run dev # dev server on :8000 with auto-reload
bun test # API + unit tests (in-memory SQLite, LLM disabled)
bun run typecheck
bun scripts/create-superuser.ts <username> <password>
bun scripts/classify-prompts.ts [--task-id N] [--force] [--dry-run]
bun scripts/recalculate-scores.ts [--dry-run]
Config comes from server/.env (see server/.env.example); dev DB is server/data/db.sqlite3, uploads go to the repo-root media/.
Client (from client/)
npm run start # Dev server (Rsbuild) on :3000
npm run build # Production build
npm run fmt # Format with Prettier
Frontend env vars used at build time: PUBLIC_BASE_URL, PUBLIC_WS_URL, PUBLIC_WEB_URL (dev proxy target for /media and /ws).
Docker (from project root)
docker compose up -d --build
docker compose run --rm webpreview-api bun scripts/create-superuser.ts <username> <password>
Key Technical Details
- Schema changes: there is no migration framework. Edit the DDL in
src/db.tsfor new tables; for altering existing tables add an idempotent step guarded byPRAGMA user_version - SQL style: raw SQL with named params (
$id),strict: trueso params are passed without the$prefix ({ id }). Wrap multi-statement writes intx(). Deleting tasks/submissions must go throughservices/cascade.ts - Queries block the whole server:
bun:sqliteis synchronous in a single process, so a slow query stalls every HTTP request and WebSocket. Request-path queries must take milliseconds; batch work goes inscripts/. Neverawaitinsidetx()(it would commit early). Code with noawaitruns atomically, so read-check-write needs no locking; races only happen across anawait - Wide rows: unlike Postgres (TOAST), SQLite keeps
html/css/jsandprompt_message.content/code_*inline, and Django's column order putsuser_id,task_id,flag,zone,raw_scoreafter them, so reading those columns from the table reads the whole code. List/scan queries must not select the big columns: page on indexes first (selectrowids withORDER BY ... LIMIT), then fetch full rows for just that page (seeGET /submission/,random-for-rating) - Indexes: SQLite uses one index per table for AND conditions (no BitmapAnd), so multi-column filters need a composite index (equality columns first, then range/sort). Hot scans on
submission_submissionmust be covered bysubmission_submission_slim; comparerowidrather thanid(idis a separate index). The planner only has stats viaPRAGMA optimize(run on open and hourly), and may pick a poor single-column index — check new queries withEXPLAIN QUERY PLAN(noSCANof big tables, ideallyCOVERING INDEX); a unary+(+s.zone = $zone) stops a column's index from being used.CREATE INDEX IF NOT EXISTSmatches by name only, so a changed index needs a new name (drop the old one in auser_versionstep) - Types: booleans are
0/1; timestamps are text compared as strings, so only write values fromnow()(neverCURRENT_TIMESTAMP/datetime('now')); text sorts by code point;ORDER BY ... ASCputs NULLs first (Postgres: last), so addNULLS LASTwhere it matters; noILIKE/DISTINCT ON - API compatibility: response shapes, status codes and trailing-slash tolerance (
strict: false) mirror the old django-ninja API, so the client needed no changes besides user management. Paginated endpoints takepage/page_size(default 10, max 100) and return{ items, count } - Task
displayfield: integer used as URL slug for tutorials/challenges/creations (unique per task type), not the DBid - Submission / conversation IDs: UUIDs in the API, 32-char hex in the DB
- Language/locale is Chinese; timestamps are stored in UTC and returned as ISO strings ending in
Z - Client build output is served by Caddy from
build/in production