Amin Mousavi b4aff39797 feat(backend): assemble resolved extraction from voice intents
Composes the tooth, span, prosthesis, catalog and date resolvers into the
payload the review sheet renders.

Connected spans expand: "a bridge from 14 to 16" selects 15, which was never
spoken. Overlapping spans merge into one bridge, group teeth sort along the
arch (16-15-14, and 11 beside 21 across the midline), and a span collapsing to
a single tooth degrades to a single group without losing that tooth — there is
no such thing as a one-tooth bridge. A cross-arch span is impossible and is
reported rather than guessed at.

Prosthesis expands a default across the selection then applies per-tooth
overrides, because "همه زیرکونیا، ۲۶ پی‌اف‌ام" is how clinicians actually speak.
Completeness is computed here so an unshippable map surfaces at review rather
than failing later at dispatch.

Everything the model names is checked against the catalog we supplied it, and
anything rejected is reported rather than dropped — a hallucinated lab id must
not look identical to "no lab was spoken", since silence and a wrong lab lead
to very different corrective actions.

Also fixed, from review of this commit:
- an empty prosthesis object no longer fabricates an "incomplete, cannot ship"
  warning on a plain restoration
- an override naming a tooth outside the selection now reports
  tooth_not_selected rather than malformed; the clinician was understood, the
  tooth just is not on this detail
- a due object with no `kind` is treated as no deadline rather than a blank
  "heard but lost" row; an unrecognised kind is still flagged, and named

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 17:41:41 +03:30

Dyolink

Monorepo: NestJS backend (backend/), Next.js frontend (frontend/), Docker stack under infrastructure/..

Local development: see backend/README.md and frontend/README.md.

Cursor AI: project conventions for agents are in AGENTS.md, .cursor/rules/, and .cursor/skills/.


Production deploy (Docker Hub + HTTPS + Let's Encrypt)

Full step-by-step guide: infrastructure/DEPLOY.md

Minimal server setup: install Docker, create .env + secrets/, docker login, run one script.

On server (once) In repo / Docker
DNS A record → server IP docker-compose.prod.yml, nginx, certbot
docker login (private Hub) Build & push images from dev machine
secrets/database.env, secrets/backend.env Examples: database.prod.env.example, backend.prod.env.example
infrastructure/.env (DOMAIN, LETSENCRYPT_EMAIL) deploy.prod.env.example

Dev machine — build frontend with the public domain baked in, push to Docker Hub:

./infrastructure/scripts/build-and-push-prod.sh wixur.ir latest

Server — from infrastructure/:

cp deploy.prod.env.example .env          # edit DOMAIN, paths
mkdir -p ../secrets && cp database.prod.env.example ../secrets/database.env
cp backend.prod.env.example ../secrets/backend.env   # set passwords + FRONTEND_URL
docker login
chmod +x scripts/*.sh
./scripts/deploy-prod.sh

SSL is issued automatically via Certbot (scripts/init-letsencrypt.sh). Nginx config is generated from DOMAIN in .env. When you move to another domain (e.g. dyolink.com), update .env + backend.env, re-run init-letsencrypt.sh, and rebuild the frontend image with the new URL.


Deploy on your own server (Docker + Gitea)

High level: build container images → push to a registry → server pulls images and runs Compose. Optionally Gitea Actions automates that on every merge to main / master.

1. One-time server preparation

  1. Install Docker and Docker Compose on the server.

  2. Run Gitea with the container registry enabled (same host/port you use for docker login, e.g. 178.131.50.201:3000).

  3. Copy the repo (or deploy only infrastructure/ + secrets). You need at least:

    • infrastructure/docker-compose.registry.yml
    • infrastructure/nginx/ configs referenced by that compose file
    • infrastructure/database/init.sql if used by your Postgres service
  4. Secrets on the server (never commit real values):

    • Copy infrastructure/database.staging.env.exampledatabase.staging.env (Postgres user/password/db).
    • Copy infrastructure/backend.staging.env.examplebackend.staging.env (e.g. DATABASE_URL, JWT, pointing at the compose Postgres service name).
    • Put both files in one directory on the server, e.g. /opt/dyolink/secrets/.
  5. Registry login from the server (same credentials you use for docker push):

    docker login <registry-host>:<port> -u <user>
    

    For HTTP registries, Docker may require insecure-registries on the daemon.

2. Manual deploy (build images elsewhere, run on server)

On your dev machine (after successful local builds):

$REG = "<registry-host>:<port>"
$OWN = "<registry-owner>"
$TAG = "manual"

docker build -t "${REG}/${OWN}/dyolink-backend:${TAG}" -t "${REG}/${OWN}/dyolink-backend:latest" ./backend

docker build `
  --build-arg NEXT_PUBLIC_API_URL="http://<your-public-ip>:<nginx-port>/api" `
  --build-arg NEXT_PUBLIC_APP_URL="http://<your-public-ip>:<nginx-port>" `
  --build-arg NEXT_PUBLIC_APP_NAME="Dyolink" `
  -t "${REG}/${OWN}/dyolink-frontend:${TAG}" `
  -t "${REG}/${OWN}/dyolink-frontend:latest" `
  ./frontend

docker push "${REG}/${OWN}/dyolink-backend:${TAG}"
docker push "${REG}/${OWN}/dyolink-backend:latest"
docker push "${REG}/${OWN}/dyolink-frontend:${TAG}"
docker push "${REG}/${OWN}/dyolink-frontend:latest"

On the server, from infrastructure/:

  1. Create deploy.registry.env (see infrastructure/deploy.registry.env.example):

    • REGISTRY_PREFIX=<host>:<port>/<owner> (no http://, no trailing slash)
    • IMAGE_TAG=latest or the tag you pushed
    • STAGING_HTTP_PORT=<host port> (e.g. 8088 — browser uses http://<ip>:8088)
  2. Set DEPLOY_SECRETS_DIR to the absolute path of the folder containing database.staging.env and backend.staging.env (you can export it in the shell or add it to deploy.registry.env if your Compose setup expects it).

  3. Pull and start:

    docker compose -f docker-compose.registry.yml --env-file deploy.registry.env pull backend frontend
    docker compose -f docker-compose.registry.yml --env-file deploy.registry.env up -d
    

The backend container runs prisma migrate deploy on startup (via entrypoint) when NODE_ENV=production, so schema updates apply after you deploy a new image that includes new migrations.

3. Automatic deploy (Gitea Actions)

Workflow file: .gitea/workflows/registry-build-deploy.yml.

Requirements:

  • Gitea Actions enabled for the repository.

  • A self-hosted runner (with Docker) registered to Gitea — the workflow uses runs-on: self-hosted.

  • Windows runners: the workflow uses PowerShell (not Bash). Giteas runner was failing with execvpe(/bin/bash) failed when Bash was routed through WSL without a real /bin/bash. If your runner is Linux, switch .gitea/workflows/registry-build-deploy.yml to defaults.run.shell: bash and use Bash syntax instead.

  • Repository → Actions → Variables (examples):

    • REGISTRY_HOST — e.g. 178.131.50.201:3000
    • REGISTRY_OWNER — image namespace (same as Docker image path after the host), e.g. admin
    • PUBLIC_BASE_URL — URL users open in the browser, e.g. http://178.131.50.201:8088 (no trailing slash)
    • DEPLOY_SECRETS_DIRabsolute path on the runner machine to the folder containing database.staging.env and backend.staging.env
    • Optional: STAGING_HTTP_PORT (defaults to 8088)
  • Repository → Actions → Secrets:

    • REGISTRY_USERNAME
    • REGISTRY_PASSWORD — access token with package read/write (or equivalent)

Trigger: push to main or master, or run the workflow manually (workflow_dispatch).

The pipeline clones from your Gitea instance, builds and pushes backend/frontend images, then on the runner runs docker compose pull and up -d using infrastructure/docker-compose.registry.yml.


Path Role
backend/Dockerfile API image
frontend/Dockerfile Web image
infrastructure/docker-compose.registry.yml Pull-only staging stack (registry images + nginx + postgres)
infrastructure/deploy.registry.env.example Template for deploy.registry.env
Description
The whole dockerized structure of the project repo
Readme 7.8 MiB
Languages
TypeScript 97.5%
Shell 1.1%
CSS 0.8%
JavaScript 0.4%
Dockerfile 0.2%