273 Commits

Author SHA1 Message Date
19b5671b0d improvement: Frontend Dockerfile updated so that aseests in public folder loads on the website.
All checks were successful
Production — tag build, push, deploy / build-and-push (push) Successful in 4m51s
Production — tag build, push, deploy / deploy (push) Successful in 2m47s
v1.0.10
2026-09-06 14:20:28 +03:30
9f6eacd0fb Merge branch 'master' into improvements/subscription-v1
All checks were successful
Production — tag build, push, deploy / build-and-push (push) Successful in 4m22s
Production — tag build, push, deploy / deploy (push) Successful in 17m31s
v1.0.9
2026-09-05 23:07:49 +03:30
52c4480c5d Merge pull request 'Fix Ci/Cd: Get Node another way — try Gitea cache, then Arvan / AWS / GCR, and only then Docker Hub; once pulled, store it on Gitea for next time.' (#74) from feature/ci-cd into master
All checks were successful
Registry — build, push, deploy / build-and-push (push) Successful in 43s
Registry — build, push, deploy / deploy (push) Successful in 1m25s
Reviewed-on: #74
Reviewed-by: admin <admin@localhost>
2026-09-05 23:00:29 +03:30
1ca8e6178e improvement: Nothing done is related to subscriptions!!! 2026-09-05 20:44:16 +03:30
6c7e0574b9 fix: omit ExtraSources when NODE_IMAGE_SOURCE is empty
All checks were successful
Production — tag build, push, deploy / build-and-push (push) Successful in 5m17s
Production — tag build, push, deploy / deploy (push) Successful in 13m34s
v1.0.8
2026-09-05 20:18:06 +03:30
feafc3a219 fix: pass empty ExtraSources when NODE_IMAGE_SOURCE is unset 2026-09-05 20:11:18 +03:30
8fa856a980 fix: pull node:20-alpine from Arvan/ECR when Docker Hub is blocked 2026-09-05 19:59:45 +03:30
005e0b0394 fix: parse ci-resolve-node-image.ps1 on Windows PowerShell 5.1 2026-09-05 19:48:46 +03:30
c1846f8e22 fix: mirror node:20-alpine to Gitea when Docker Hub TLS times out
BuildKit was failing on registry-1.docker.io even when a local base
image existed. CI now prefers a Gitea-hosted NODE_IMAGE and retries
docker build.
2026-09-05 19:12:37 +03:30
663eafa3e8 fix: fail Gitea deploy when Docker image push does not succeed
Windows PowerShell ignored docker exit codes, so build-and-push looked
green while :sha tags were never pushed and deploy pulled missing images.
2026-09-05 19:03:06 +03:30
5cd3436d0c Merge pull request 'improvement: icons added to prosthesis types catalog.' (#73) from improvements/prosthesis-catalog-icons into master
Some checks failed
Registry — build, push, deploy / build-and-push (push) Successful in 41s
Registry — build, push, deploy / deploy (push) Failing after 10s
Reviewed-on: #73
Reviewed-by: aminmsvi <amn.mousavi@gmail.com>
2026-09-05 13:45:25 +03:30
5d3597f973 improvement: icons added to prosthesis types catalog.
Some checks failed
Production — tag build, push, deploy / build-and-push (push) Failing after 23s
Production — tag build, push, deploy / deploy (push) Has been skipped
v1.0.7
2026-09-05 01:29:53 +03:30
4d7a4b390f Merge pull request 'improvement/prosthesis-types-overhaul' (#72) from improvement/prosthesis-types-overhaul into master
Some checks failed
Production — tag build, push, deploy / build-and-push (push) Successful in 4m18s
Production — tag build, push, deploy / deploy (push) Successful in 12m48s
Registry — build, push, deploy / build-and-push (push) Successful in 36s
Registry — build, push, deploy / deploy (push) Failing after 11s
Reviewed-on: #72
Reviewed-by: rameen <rameen.naghdi@gmail.com>
v1.0.6
2026-09-04 20:23:58 +03:30
e57fe2d959 Merge branch 'master' into improvement/prosthesis-types-overhaul 2026-09-04 17:07:59 +03:30
72f885d9dd improvement: all confirmed suggestions implemented 2026-09-04 17:04:48 +03:30
7f92e735fb improvement: Some improvements done. some bugs fixed. 2026-09-02 17:24:01 +03:30
c23785fbf4 Merge pull request 'feature/ci-cd' (#71) from feature/ci-cd into master
All checks were successful
Registry — build, push, deploy / build-and-push (push) Successful in 1m8s
Registry — build, push, deploy / deploy (push) Successful in 1m39s
Reviewed-on: #71
Reviewed-by: admin <admin@localhost>
2026-09-02 11:05:18 +03:30
a3c14a18c1 improvement: new prosthesis type data structure implemented and finally working! 2026-09-01 21:03:04 +03:30
60be656cf1 Allow Gitea registry pushes through gitea.wixur.ir: nginx 50M body limit 413s Docker layers.
All checks were successful
Production — tag build, push, deploy / build-and-push (push) Successful in 47s
Production — tag build, push, deploy / deploy (push) Successful in 14m17s
v1.0.5
2026-09-01 12:40:26 +03:30
6465af11ee Expose AdminJS on nudentic.ir: nginx only proxied /api, so /admin never reached Nest. 2026-09-01 11:49:14 +03:30
dc71c73ced Merge pull request 'feature/ci-cd' (#70) from feature/ci-cd into master
All checks were successful
Registry — build, push, deploy / build-and-push (push) Successful in 58s
Registry — build, push, deploy / deploy (push) Successful in 1m27s
Reviewed-on: #70
Reviewed-by: admin <admin@localhost>
2026-09-01 09:56:02 +03:30
19364ad5c2 resolve failed handshake connection with https://wixur.ir:3000
All checks were successful
Production — tag build, push, deploy / build-and-push (push) Successful in 42s
Production — tag build, push, deploy / deploy (push) Successful in 21s
v1.0.1 v1.0.3 v1.0.4
2026-08-31 10:04:30 +03:30
50eda34e8e The app is now wired to GlitchTip with the Sentry SDKs
Some checks failed
Production — tag build, push, deploy / build-and-push (push) Successful in 1h13m36s
Production — tag build, push, deploy / deploy (push) Failing after 1m1s
v1.0.2
2026-08-31 07:34:07 +03:30
573e5e0886 auto deploy on production server now finalized ready for smoke test.
All checks were successful
Production — tag build, push, deploy / build-and-push (push) Successful in 4m23s
Production — tag build, push, deploy / deploy (push) Successful in 11m52s
v1.0.0
2026-08-29 16:25:27 +03:30
23c37519b5 windows ssl 2026-08-29 14:46:41 +03:30
2e2c910d22 Document staging HTTPS on wixur.ir (Windows nginx + Let's Encrypt).
Mobinnet holds public port 80, so staging is served on 443. Examples and the host nginx template now match FRONTEND_URL=https://wixur.ir.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 14:46:05 +03:30
c40e276b8a migrate from http://178.131.50.201:8088 to wixur.ir 2026-08-29 12:09:50 +03:30
d8efaa6f2f Merge pull request 'Fix voice extract timeout for production ASR+LLM pipeline' (#69) from feature/ci-cd into master
All checks were successful
Registry — build, push, deploy / build-and-push (push) Successful in 1m37s
Registry — build, push, deploy / deploy (push) Successful in 38s
Reviewed-on: http://host.docker.internal:3000/admin/dyolink/pulls/69
Reviewed-by: admin <admin@localhost>
2026-08-25 14:42:23 +03:30
eee2506adf ASR + LLM routinely exceeds the default 10s axios timeout; match slow upload endpoints. 2026-08-25 10:21:55 +03:30
9f5f696fb4 Merge pull request 'Bind staging Docker to 18088 so portproxy can own 8088.' (#68) from feature/ci-cd into master
All checks were successful
Registry — build, push, deploy / build-and-push (push) Successful in 17s
Registry — build, push, deploy / deploy (push) Successful in 45s
Reviewed-on: http://host.docker.internal:3000/admin/dyolink/pulls/68
Reviewed-by: admin <admin@localhost>
2026-08-24 12:14:51 +03:30
5ac3a4a708 Bind staging Docker to 18088 so portproxy can own 8088. 2026-08-24 12:06:13 +03:30
2a0f9dbfee Merge pull request 'feat(treatment): add voice detail entry' (#65) from feat/voice-treatment-entry into master
Some checks failed
Registry — build, push, deploy / build-and-push (push) Successful in 7m32s
Registry — build, push, deploy / deploy (push) Failing after 45s
Reviewed-on: http://host.docker.internal:3000/admin/dyolink/pulls/65
Reviewed-by: admin <admin@localhost>
2026-08-24 11:13:45 +03:30
dc9401e13f Merge pull request 'feature/ci-cd' (#67) from feature/ci-cd into master
Some checks failed
Registry — build, push, deploy / deploy (push) Has been cancelled
Registry — build, push, deploy / build-and-push (push) Has been cancelled
Reviewed-on: http://host.docker.internal:3000/admin/dyolink/pulls/67
Reviewed-by: admin <admin@localhost>
2026-08-24 11:09:38 +03:30
8c16a46740 docs: drop the spec's reference to the auto-fill exemption
origin/master removed the dispatch panel's remembered-prosthesis auto-fill
(f52ad6b), so the exemption this branch carried for it went away in the
rebase. The spec named the mechanism and a verification step for a feature
that no longer exists.

The rule it existed to enforce still stands and is worth keeping written
down — confirm fills exactly what the sheet previewed — so it stays, marked
as a constraint on whatever gets added next rather than a description of
something in the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:18:22 +03:30
dc10d8dbe3 docs: cut the comments that were not earning their place
I wrote 731 comment lines on this branch against 4,530 lines of code — 14%,
where the rest of the repo runs at 1.8%. CLAUDE.md asks for code that reads
like its surroundings, and this did not.

Removed by genre rather than by taste:

- restating the code, e.g. "JS getUTCDay() numbering: Sunday = 0" above the
  map that literally shows it, and a docblock on startOfWeek explaining that
  it returns the start of the week;
- narrating history — "this used to rebuild the whole map", "left the bar
  recording forever" — which the commit message and git blame already carry;
- saying the same thing in several places: the "cannot record is not a
  denied microphone" reason appeared three times in one file, and the
  "aborting stops a per-minute metered call" reason across three files. Each
  now lives once, where the behaviour it explains lives;
- defending decisions nobody would question, like why toLatinDigits is its
  own module;
- over-explaining defensive branches, three separate comments to distinguish
  null from missing-kind from unrecognised-kind.

What stays is what the code cannot say: the patient-right convention in
toFdi, whose failure mode is a valid code for the wrong tooth; the
"this"-vs-"next" week anchoring; StrictMode re-arming mountedRef; Safari
accepting no mimeType hint; and the invariants whose violation already cost
a bug — the body parser's middleware ordering and the dispatch panel's
auto-fill rules.

Comments only. The diff contains no non-comment line.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:14:15 +03:30
562ef2ae6e docs: bring the voice spec in line with the flow as built
The spec still described the flow as designed, not as it works after the
first live recordings.

§2 gains what confirm actually writes: the detail and its lab case are
persisted on confirm, because the autosave effect watches `details` only
and a lab draft left in component state loses the lab, the due date and the
prosthesis map on reload — while the detail survives, which is what makes
that loss look like a save. Plus the guard: a preview treatment comes back
instead when any detail is still untyped, and confirm skips the lab-case
save rather than posting an id the server has never seen.

§9 corrects three codes: VOICE_MIC_DENIED is now only a real permission
failure, VOICE_UNSUPPORTED_FORMAT covers every "this browser cannot record"
path, and both it and VOICE_CLIP_TOO_LONG are named on their DTO
constraints rather than falling through the shared map to an unrelated
message. §8 no longer claims there is no duration cap — there is, 2
minutes, decided before implementation.

§12 gains the checks these changes need, including the reload that catches
an unsaved lab case, and decisions 31-33 record the three rules the live
testing settled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:07:53 +03:30
8f2d3f97ba fix: stop voice entry posting an unsaved detail id, and name failures right
Review findings on this branch.

The lab-case save could be posted against a detail the server has never
seen. persistDraft returns a *preview* treatment instead of saving when any
detail lacks a treatment type — the blank one the workspace opens with is
enough — and a preview's detail id falls back to the client id. Recording
straight after opening a visit and confirming a result with a lab or due
date would send that id and fail the whole save. It now checks what came
back rather than the precondition, so it holds for every early return
persistDraft has.

stop() optional-chained into a no-op when the recorder was already gone,
leaving the bar recording forever with a live timer and only Cancel as a
way out.

Three "this browser cannot record" paths reported VOICE_MIC_DENIED — no
MediaRecorder at all, no container the API accepts, and a recorder that
throws after permission was already granted. Telling clinicians their
microphone was denied sends them hunting for a permission nothing asked
for; they now report VOICE_UNSUPPORTED_FORMAT.

The voice route's large-body match stripped every trailing slash while
Express ignores exactly one, so '/api/voice/extract//' bought a 10 MB
buffer for a request that then 404s.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:07:53 +03:30
bdf6da135b fix(frontend): stop the remembered prosthesis default rewriting the map
Dictating "12 روکش PFM, 13 روکش PFZ" previewed correctly and then landed in
the form as PFM on both teeth.

The stored data was never wrong — the dev database holds 12 → pfm_crown and
13 → pfz_crown with selectionGroupIds matching the detail's groups exactly,
and a page reload renders it correctly. The damage was live client state:
the dispatch panel's "last type used for this lab" default rebuilt the
*entire* map from one code, so a single row reading as unfilled destroyed
every type already set.

Two changes:

- It fills blanks now, and leaves every entry that already carries a type
  alone. The bulk "apply to all" select only pre-sets itself when the fill
  really did cover every tooth, instead of claiming one type while the rows
  below disagree.
- A lab case created by confirming a voice result is exempt from the
  default entirely. The review sheet is a contract: topping the case up
  with a type for a tooth the preview never showed makes the confirmation
  step a lie about what it was going to fill.

The exemption is tracked in workspace state rather than on LabCaseDraft
because a draft field is dropped by mapLabCaseDraftFromApi on the first
server round-trip — exactly the window this failure lives in.

isProsthesisMapComplete is deliberately untouched: its strict
selectionGroupId match succeeds on the real data, so loosening it would
have been a blind change to a working path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:07:53 +03:30
3fb7f02f43 docs: add CLAUDE.md for project guidance and conventions 2026-08-23 23:05:30 +03:30
f46ecbd771 docs: mark transcript salvage as specified but not built
The backend returns the transcript on VOICE_EXTRACT_FAILED and the client
never reads it, so dictation the clinic paid for is shipped in an error
body and dropped. The spec claimed the whole feature was implemented; it
now names the gap and the two ways out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
3d64414dd3 fix(backend): stop showing the clinician null, NaN and the wrong failure
Two ways a voice failure described itself wrongly.

describe() built the quoted-back text from fields that are all nullable on
the wire, and toVoiceIntent casts rather than checks — so a half-classified
deadline rendered as “null null” — not a usable date, and an offset with no
amount as “+NaN day”. Blank is already handled by the sheet; it now falls
back to that.

The DTO's constraints resolved to unrelated codes: maxLength fell through
to VALIDATION_FIELD_REQUIRED, so an oversized recording said a field was
missing, and isIn maps to VALIDATION_LANGUAGE_INVALID, so an unsupported
container said the language was invalid. Both now name their own code —
the validation factory already returns a message verbatim when it is itself
a known ErrorCode, so this needs no change to the shared mapping.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
6a4c0cb1bb fix(frontend): stop the level meter re-rendering the whole workspace
The meter wrote React state from a requestAnimationFrame loop, and the hook
lives in TreatmentWorkspace — so every frame re-rendered the details editor,
the FDI chart, the lab panel and the history rail. About 7,200 whole-tree
renders across a two-minute recording, while the user is dictating.

Now samples every frame but publishes at LEVEL_POLL_MS, the rate the
elapsed timer already used. Peaks between publishes are carried forward, so
the meter stays responsive to transients rather than sampling at 10 Hz.

Also adds the catch the start path never had: new MediaRecorder() and
recorder.start() both throw on some browsers, and by then the stream is
live. The rejection went unhandled, the UI sat at 'idle' showing nothing,
and the browser's recording indicator stayed lit until unmount.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
3911477e42 fix(frontend): persist the lab case a voice result creates
applyVoiceResult put the lab draft in state and stopped there. Every other
path that creates a LabCaseDraft — handleContinueToLab, handleLabCasesChange
— immediately runs persistDraft + persistLabCases, and the autosave effect
only watches `details`. So applying a voice result carrying a lab, a due
date and a prosthesis map, then reloading, kept the detail and silently
dropped all three: the surviving detail made it look like the save worked.

applyVoiceResult moves below persistDraft/persistLabCases so it can call
them, and writes detailsRef itself before persisting — persistDraft reads
that ref, and setDetails has not rendered by the time the save runs. The
ref is already written imperatively elsewhere for the same reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
62deff0523 fix(frontend): untick prosthesis when a picked tooth breaks its map
initialVoiceSelection deliberately never auto-ticks an incomplete
prosthesis map, because a detail with an untyped tooth cannot ship — it
fails at dispatch instead. Picking a candidate tooth walked straight
through that rule: the tick was seeded once, so a map that was complete at
extraction stayed ticked after a tooth with no prosthesis type joined it,
and Apply attached a map assertCompleteToothProsthesisMap rejects.

Recomputed on each pick, and only ever downwards — re-ticking is the
clinician's call, not a side effect of un-picking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
b7ee61433e docs: record the FDI-first tooth rule in the voice spec
The spec's tooth section described the design that the first live test
disproved — descriptive phrasing primary, bare numerals refused in en.
Rewrites §6 around the rule the product actually wants, records the chip
affordance in §7, and closes open item §11.5: a two-digit number is FDI in
all three locales, with the Universal-numbering trade-off written down
rather than left implied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
754efdee09 feat(frontend): let the clinician pick the tooth from the candidates
An under-specified tooth was a dead end: the sheet said what was missing
and the clinician had to leave and hunt for it on the chart. The readings
are enumerable, so the review sheet now renders them as chips — the one
interactive part of an otherwise read-only confirmation step.

A pick is folded into the result by withChosenTeeth() rather than tracked
alongside it, so the rows, the mini chart, the prosthesis warning and
applyVoiceResult all keep reading a single VoiceExtractionResult and none
of them has to know the chips exist. It unions rather than toggles: a
candidate can coincidentally be a tooth the recording already produced, and
tapping it must not deselect that one.

Two things that would otherwise make the chips look functional while
applying nothing: the teeth row is ticked on the first pick (it starts
unticked when the recording produced no teeth of its own), and the apply
count is now intersected with row availability so it cannot promise to
apply a row with nothing in it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
82fad4ac02 feat(backend): offer the candidate teeth for an unspecified quadrant
A tooth the resolver refuses to guess at is currently a dead end: the sheet
says the quadrant was missing and the clinician has to leave and find the
tooth on the chart. But the readings are enumerable — "دو" is one of four
teeth, "دو بالا" one of two — so unresolved items now carry them.

Narrowed by whatever was actually said, so this stays a choice offered to
the clinician rather than a guess made for them. Only tooth_missing_quadrant
carries candidates; a wrong position or a deciduous tooth has nothing to
choose between.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
52a7359b9a feat(backend): read a spoken tooth number as its FDI code
The prompt had this backwards. "Never output an FDI tooth code unless the
speaker used FDI notation. Prefer arch + side + position" pushed the model
to decompose speech into "upper / patient_right / six", so the clinician
effectively had to *describe* every tooth. Saying "دندون بیست و شش" — the
way a dentist actually dictates — was the unsupported path.

FDI is what clinicians speak, so the prompt now teaches the notation
instead of forbidding it: first digit = quadrant from the patient's own
point of view, second digit = position from the midline. arch/side/position
stays as the reading of a *described* tooth, where a single digit is a
position and the quadrant comes from words.

Two guards come with it, because bare numbers are now teeth: a single digit
alone still refuses to guess a quadrant, and dates, counts and quantities
are explicitly not teeth.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
c09698aea2 fix(backend): read a tooth code whatever script its digits are in
The extraction model transcribes Persian speech, so it can hand back "۲۶"
in Persian digits or "2 6" from a digit-by-digit dictation. Both were
compared literally against /^[1-8][1-8]$/, missed, and fell through to the
positional branch with no quadrant — where the tooth was reported as "not
understood". The clinician loses a tooth and is told the words were the
problem.

normalizeFdiCode() now runs at both the branch choice and the final
validation, so the two cannot disagree. toLatinDigits moves out of
jalali.ts into common/digits.ts: it was exported but unused in production,
and a tooth module reaching into the calendar module would read as an
accident.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 23:05:30 +03:30
efff258910 fix(backend): apply the large-body limit to every spelling Express routes
req.path was compared to the canonical '/api/voice/extract' only, but
Express routes case-insensitively and ignores a trailing slash by default.
'/api/voice/extract/' therefore reached the controller with the 100 kb
parser, and 413'd every recording past ~20 seconds — a failure that reads
as a broken microphone rather than a routing detail.

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