Official website for a medical / biomedical research lab, with a reserved architecture for mounting interpretable research prediction models as WebApps.
Privacy: This project does not process real patient-level or personally identifiable data. Model pages are for research and education only and never produce diagnostic or treatment instructions.
| Layer | Technology |
|---|---|
| Frontend | Next.js 14 (App Router) · TypeScript · Tailwind CSS |
| Backend | FastAPI · Pydantic · Uvicorn (Phase-1 skeleton) |
| Content | JSON files under frontend/data/ (no CMS / DB in Phase-1) |
| Deploy | Docker Compose + Nginx reverse proxy |
labweb/
├── frontend/ # Next.js app (pages, components, JSON data, design system)
├── backend/ # FastAPI skeleton (health, model catalog, mock predict)
├── nginx/lab.conf # reverse proxy: / -> frontend, /api/ -> backend, /apps/ reserved
├── docker-compose.yml
├── .env.example # copy to .env (no real secrets committed)
├── deploy.sh # minimal server deploy helper
├── AGENTS.md # rules for AI coding tools working in this repo
└── README.md
cd frontend
npm install
npm run dev # http://localhost:3000
npm run lint
npm run buildcd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000 # http://localhost:8000/docscp .env.example .env # edit if needed
docker compose config # validate compose file
docker compose up -d --build
# Site: http://localhost/
# Backend health (proxied through nginx): http://localhost/health
# Model catalog API: http://localhost/api/models- Install Docker + the Compose plugin on the server.
- Copy the repo to the server (
git cloneorscp). cp .env.example .envand setALLOWED_ORIGINSto your domain../deploy.sh(builds images, startsfrontend+backend+nginx, then probes health).- Point your domain's DNS at the server; open port 80 in the security group.
- HTTPS (recommended next step): add a TLS server block to
nginx/lab.confand issue a certificate with certbot / Let's Encrypt.
Server sizing (4 GB RAM / 8 vCPU / 70 GB SSD) is comfortable for this stack.
deploy.shruns, in order: acurlavailability check →git pull --ff-only(skipped automatically if the folder is not a git checkout) →docker compose build→docker compose up -d→docker compose ps→ three post-deploy health probes against/,/health, and/api/models. It never runsdocker system prune, never edits an existing.env, and never configures HTTPS.- Backend healthcheck: the
backendservice has a Dockerhealthcheckusing the Python standard library (no curl/wget in the slim image). The health path is/health. After deploy,docker compose psshould report backend ashealthy. - nginx gzip is enabled for text-like assets (HTML/CSS/JS/JSON/XML/SVG).
/apps/returns503by design — it is a placeholder until model WebApps (Streamlit / Gradio) are mounted. A 503 there is the expected result for now.- HTTPS is not enabled yet. Configure TLS separately only after the domain is pointed at the server (see step 6).
- Do not expose ports 3000 (frontend) or 8000 (backend) to the public internet.
They are only
exposed on the internal Docker network and reached through nginx. Only nginx publishes a host port (80). - In the Tencent Cloud security group, open only 22 (SSH) and 80 (HTTP) to start. Open 443 only after HTTPS/TLS is configured.
- On first deploy, confirm
backendshowshealthyindocker compose psbefore considering the rollout complete. - If
/apps/returns 503, that is the expected placeholder behavior before any model service is connected — not an error.
docker compose config # validate compose syntax
./deploy.sh # build + up + ps + health probes
docker compose ps # backend should be "healthy"
docker compose exec nginx nginx -t # validate nginx config (incl. gzip)
curl -fsS http://localhost/ # frontend (200)
curl -fsS http://localhost/health # backend {"status":"ok",...}
curl -fsS http://localhost/api/models # model catalog JSON
curl -s -H "Accept-Encoding: gzip" -I http://localhost/ | grep -i content-encoding # gzip activeAll editorial content lives in JSON — edit and redeploy:
| Page | File |
|---|---|
| Members | frontend/data/members.json |
| Alumni | frontend/data/alumni.json |
| Publications | frontend/data/publications.json |
| Models (cards) | frontend/data/models.json |
| News | frontend/data/news.json |
Lab identity (name, institution, email, address, nav) lives in
frontend/lib/site.ts. Research directions live in the same file (researchAreas).
The design system is centralized:
- Color / spacing / type tokens →
frontend/tailwind.config.ts - CSS variables + base styles →
frontend/app/globals.css - Reusable UI primitives →
frontend/components/ui/
Map Figma tokens onto these, and components re-skin without page rewrites.
A model card always lives in frontend/data/models.json (id, route /models/<id>,
status, version, disclaimer); the dynamic route frontend/app/models/[modelId]/page.tsx
renders it. How the tool itself runs depends on its type:
For a self-contained client-side app (e.g. a Vite/React SPA that computes in the browser), vendor its build into the site and embed it via an iframe — no backend, no extra container.
- Build + vendor with the one-command helper (rebuilds with the correct base
/apps/<id>/and copiesdist/intofrontend/public/apps/<id>/):The frontend container then serves it atscripts/sync-model.sh <model-id> <source-spa-dir> # e.g. scripts/sync-model.sh plan-c /Users/hezhu/projects/LatentIRI/cdss
/apps/<id>/(nginx routes/apps/*to the frontend via the catch-all — no nginx/compose change needed). - Register the embed in
frontend/app/models/[modelId]/page.tsx:(Kept in code, not inconst MODEL_EMBEDS = { '<model-id>': '/apps/<model-id>/index.html', /* ... */ };
models.json, to preserve the data-structure lock.) The detail page shows the disclaimer + an iframe with an "open in new tab" link. - Add the card to
frontend/data/models.json(setstatustoAvailable) and commitfrontend/public/apps/<id>/together with the edits above.
Reference implementation: PLAN-C Compass at /models/plan-c → /apps/plan-c/.
To update a tool later, just re-run scripts/sync-model.sh <id> <dir> and commit.
For server-side inference, drop the artifact in backend/model_artifacts/, add a
service + schema + route (see backend/README.md), and keep the research-use
disclaimer and version fields in every response. The detail page's reserved section
skeleton (overview / inputs / output / interpretation / performance / citation) is
already in place for non-embedded models.
Reference implementation: Kawasaki_IVIG at /models/kawasaki-ivig. The locked
scikit-learn pipeline lives in backend/model_artifacts/kawasaki_ivig/; inference is
backend/app/services/kawasaki_ivig.py + schemas/kawasaki.py + the
POST /api/predict/kawasaki-ivig route; the bilingual form is
frontend/components/models/KawasakiPredictForm.tsx (registered in the detail page's
MODEL_NATIVE_FORMS) driven by frontend/lib/kawasakiFields.ts. Note: scikit-learn
is pinned exactly to the version that serialized the joblib (_sklearn_version),
or the artifact will not load.
Run as their own container and add a dedicated upstream + location /apps/<id>/
block in nginx/lab.conf pointing at it.
See AGENTS.md for the collaboration rules (do-not-touch list,
frontend/backend conventions, and medical-model safety constraints).