Commit Graph

295 Commits

Author SHA1 Message Date
21c78bb930 fix(treatment): never keep a bridge across a tooth that left it
A bridge is a contiguous span, but every operation that filtered teeth out of
a group kept the `connected` kind as long as two teeth remained. Reduce a
12-13-14 span to 12 and 14 and you get a "bridge" with no pontic — and worse,
both teeth keep one selectionGroupId, so the lab receives them as a single
unit and task generation builds work that cannot be made.

linkedEdgesFromGroups already guarded the drawn marks with areArchNeighbors,
so the chart looked right while the data was wrong.

Adds splitDisconnectedRuns, which breaks what remains into contiguous runs: a
run of two or more stays connected, a run of one becomes a single. The first
run keeps the original groupId so lab rows pointing at it stay valid, and
pruneToothProsthesisForGroups re-maps the rest.

Applied at all three sites that filter a group's teeth, which each carried
their own copy of the length check:
- groupsFromFlatTeeth, when teeth are no longer selected
- applyShiftRange, when a shift-range takes teeth from an existing group
- removeTeethFromGroups, the path pruneDetailTeethToJobs uses

toggleToothInGroups already split into runs by hand for the single-removal
case; this is the same rule, shared.

Tests: a new toothSelectionGroups.spec.ts (13) plus two for
removeTeethFromGroups. The middle-tooth cases fail without the fix; the
end-tooth and contiguous cases pass either way and exist to prove it does not
over-reach. CLAUDE.md updated for the third Vitest file.

Not fixed: a pre-existing unused `leftIdx` warning in unlinkAdjacentTeeth,
unrelated to this change.

Gates: 52 Vitest tests, tsc --noEmit clean, next build clean, ESLint
unchanged at 1 pre-existing warning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 15:49:35 +03:30
f014793dec fix(voice): prime the lab-drafts ref before the save that reads it
"برای دندون ۱۲ و ۱۴ یه فول متال کران" previewed correctly, then Apply failed
with TREATMENT_TOOTH_NOT_ON_DETAIL. The form still showed both teeth and both
crowns — that is React state. The database got a detail with no teeth.

applyVoiceResult called setLabCaseDrafts but never wrote
labCaseDraftsRef.current, which is only refreshed in the render body.
persistDraft runs in the same tick and starts by pruning each lab-dependent
detail down to the teeth its jobs cover, reading that stale ref. It found no
draft for the brand-new detail, so jobs were empty, so the detail was saved
with teeth: []. persistLabCases then posted rows for 12 and 14 against it,
and the server — which re-reads the detail's teeth from the database, because
the lab-case endpoint carries no teeth field — correctly refused.

detailsRef was already written by hand two lines above for exactly this
reason (3911477). This is the other half of the same mistake.

Two sibling handlers had the same gap and are fixed with it: adopting an
orphan lab case, and creating a new lab draft. Both call setLabCaseDrafts and
then persistDraft in the same tick. handleLabCasesChange was the only site
that already primed the ref, and it is the pattern the others now follow.

Adds a comment at the ref declaration, because the coupling between this ref
and persistDraft's prune is invisible at the call sites and has now cost two
bugs.

No automated test: this is state-and-ref ordering inside a React component,
and the frontend's Vitest scope is pure helpers only — no React, no DOM.
Mirroring the server's tooth rule on the client to make it testable would
duplicate a rule across layers, which decision 38 rules out.

Gates: tsc --noEmit clean, 37 Vitest tests, next build clean, ESLint warnings
unchanged at 10 (all pre-existing, count verified against HEAD).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 15:49:35 +03:30
ae33159e7d fix(voice): stop reading a dictated jaw as a broken tooth
"یه کامپلیت دنچر برای فک بالا میخوام" resolved the upper arch and the
complete denture correctly, but the review sheet also showed
position_out_of_range against «فک بالا» and asked which tooth was meant. No
tooth was said.

The model names the jaw in the top-level `teeth` array as well as in the
prosthesis target it belongs to. resolveVoiceIntent passed that array
straight to resolveToothIntents, extraction.wire.ts turns `position: null`
into NaN, and unresolvedReason tests positionBad first — so it reported a
range fault for a value that was never a number, before ever reaching the
quadrant branch.

resolveVoiceIntent now drops jaw-shaped entries — an arch with no usable
position — before the tooth resolver sees them. The jaw already reaches the
form through its assignment, so the duplicate carries no information worth
reporting. An entry that DOES give a position survives: arch plus position
without a side is a real tooth described without its quadrant, and must keep
offering its candidate chips.

Two invitations removed as well, both ours: the `teeth` property in
VOICE_INTENT_JSON_SCHEMA had no description at all, and no prompt rule said a
jaw must stay out of it, while TOOTH_SCHEMA — shared with
prosthesis[].targets — describes jaws as acceptable. Adds the description and
HARD RULE 6.

Four tests. The two jaw cases fail without the filter; the out-of-range and
missing-quadrant cases pass either way and exist to prove the filter does not
over-reach.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 15:49:35 +03:30
068faf7e15 docs: mark the catalog-kinds work item done now that it is applied
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 15:49:35 +03:30
3740787306 docs: prove the migration and seed against dev Postgres
Five migrations were pending, not one: this dev database had not been
migrated since the prosthesis overhaul landed on master. Used migrate deploy
rather than migrate dev so no drift prompt could offer a reset, and counted
the DELETE first — 0 rows matched against 2 total.

Seeded translations verified: 7 categories and 5 subcategories in fa, en and
nl, with crown resolving to روکش‌ها. Both login accounts survived.

Also records that .gitea/workflows run no test, lint or typecheck step at
all, which is why two spec-file type errors reached the branch unnoticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 15:49:35 +03:30
6f2be4a7e8 docs: record that a prosthesis dictation reaches teeth only through the merged row
Decision 39 couples teeth and jobs on a lab-dependent type, so unticking the
merged row and then the type row applies a detail with no teeth at all. master
kept them, because the rows were separate. Approved as an accepted consequence
rather than patched, since applying teeth the clinician just unticked would
break the sheet-is-a-contract rule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 15:49:35 +03:30
88c33059fd docs: record the second refute round and the one open consequence
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 15:49:35 +03:30
4194442654 fix(voice): validate a code's region against its target properly
Second correctness pass on e271858 found three ways an arch-only appliance
could be written as a per-tooth job, plus a gap in the previous repair.

1. The deferred region check was never completed. A mixed-region category
   resolved with no region recorded, and nothing re-validated the leaf the
   clinician then picked — "پروتز متحرک برای دندون ۱۲" wrote a complete
   denture onto tooth 12, which the manual chart cannot produce and which
   task generation would expand as denture steps. Targets now resolve BEFORE
   types, so the target kind narrows a category's candidates and an
   impossible leaf is never offered. The deferral disappears with it.

2. `some` over the stack's regions let one legal code admit every other.
   `types: ['pfm_crown','night_guard_soft']` on tooth 12 passed, because
   `crown` suited the tooth, and wrote a night guard onto that tooth. Now
   every named code must suit the target.

3. `regions.size === 1` also deferred `implant`, whose leaves span root and
   crown — both tooth regions, so not a tooth/arch category at all. An
   implant aimed at a jaw resolved as a UA target. Narrowing by target kind
   rejects it instead.

4. The e271858 type-row lock ticked the row and the payload but not the
   count, so the sheet read "Apply 1 field" while two landed. One
   `effectiveSelection` now drives the count and the payload.

Also narrows a `filter(Boolean)` in the disjointness test that left two
implicit-any errors under the full tsconfig (nest build excludes specs, so
the repo gate never saw them). Pre-existing at 15ddb9a.

Adds four resolver tests: candidate narrowing per target kind, a category
with no usable leaf, a stack where only one code suits, and a legal stack.
All three defects passed the previous 209 tests.

Gates: backend 212 tests, nest build, ESLint clean on the voice module;
frontend 37 Vitest tests, tsc --noEmit, ESLint clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 15:49:35 +03:30
08b4d36f27 fix(voice): repair the two defects the correctness critic found
Both found by the /orchestrate correctness critic against 15ddb9a, and both
verified by hand before fixing.

1. A picked tooth chip was dropped on Apply. pickCandidate ticked the teeth
   row from `available`, which is memoised from `effective` — the value the
   same handler is in the middle of changing. On "ترمیم برای دندون دو" the
   backend returns no teeth, so `available.teeth` was false at pick time and
   `prev.teeth` was false too; `false || false` stuck permanently. A variant
   of the original live-test failure. Each branch now ticks the row it feeds
   and returns, so nothing reads the stale memo.

2. Decision 41's type-row lock was missing. The treatmentType row was a plain
   toggle, and applyVoiceResult derived `labDependent` from
   `result.treatmentType` rather than the `detail.treatmentType` it writes —
   so unticking the type row saved prosthesis lab rows on whatever type the
   appointment purpose had seeded. Row gains a `locked` state, the type row
   locks while the prosthesis row is ticked, Apply sends the forced tick, and
   `labDependent` now comes from the detail being written.

Gates: backend 16 suites / 209 tests, frontend 37 Vitest tests,
tsc --noEmit clean, next build clean, ESLint 0 new warnings
(VoiceReviewSheet.tsx 0 issues; the 10 in TreatmentWorkspace.tsx are
pre-existing and unchanged in count).

Not covered by a test: both fixes live in component state logic, which the
chosen Vitest scope — pure helpers, no React, no DOM — cannot reach. §12's
manual checks cover them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 15:49:35 +03:30
58ef87ba17 feat(voice): adapt voice entry to the stacked-jobs prosthesis model
Authored by the /orchestrate builder agent, committed unrepaired so the
fixes that follow are reviewable against it.

Backend: replaces the flat prosthesisDefaultType/prosthesisOverrides wire
shape with a prosthesis: ProsthesisAssignment[] list whose targets can be a
tooth or a jaw; adds resolveAssignmentTarget / classifyTypeCode /
resolveProsthesisAssignment for leaf-vs-category classification, region
validity with mixed-region deferral, and assignmentIndex on unresolved
items; adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY to
CatalogEntityKind with a migration and seeded fa/en/nl translations; and
rewrites the extraction prompt to render the catalog as a tree.

Frontend: merged "teeth and prosthesis" row, stack preview through the
existing applyLeafToJobs, three chip-fold paths, rewritten applyVoiceResult
and voiceForEditor, and the two carried-forward recording fixes — the
container fallback that refused Safari and the render gate that never
checked isMediaRecorderSupported().

Adds Vitest for the frontend's pure helpers, and updates CLAUDE.md.

Gate was green: backend 16 suites / 209 tests, nest build, prisma validate;
frontend 37 Vitest tests, tsc --noEmit, next build.

KNOWN DEFECTS, fixed in the commits that follow:
- VoiceReviewSheet.tsx:169 — a picked tooth chip is dropped on Apply
- VoiceReviewSheet.tsx:213 / TreatmentWorkspace.tsx:2215 — decision 41's
  type-row lock is missing, so unticking it saves prosthesis lab rows on a
  non-prosthesis detail

Reviewed on the correctness lens only; regression-risk never ran. The
migration was validated but never applied.

Spec: docs/specs/voice-treatment-entry/spec.md
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 15:49:35 +03:30
dd63182056 docs: close the gaps the orchestrate surveyor found
The Gap-check phase halted the run on one blocking gap and reported four
notes. Nothing was built.

Blocking: the prompt cannot offer a prosthesis category the model can name,
because no category label exists anywhere the backend can read —
CatalogEntityKind covers only TREATMENT_TYPE, PROSTHESIS_TYPE and
LAB_WORKFLOW_STEP. Adds PROSTHESIS_CATEGORY and PROSTHESIS_SUBCATEGORY as
catalog entities with seeded fa/en/nl translations, rather than sending bare
codes that would read untranslated on the locale this feature exists for.

Also closed: a chip that resolved to a jobless tooth and was then discarded
by the jobless-tooth rule, so the tap did nothing; the undefined region check
for a category whose leaves span crown and arch; the wrong endpoint path in
§3; and the availability-endpoint contradiction in §11.

Corrects two counts of my own: 5 subcategories, not 4 (night_guard was
missed), and the disjointness test now asserts against the live catalog
rather than a number written in prose.

Decisions 47-50. Work items 18-19 added to the ledger.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 15:49:35 +03:30
c528e1c805 docs: revise the voice spec for the new prosthesis model
The treatment form was overhauled after voice v1 merged (a3c14a1, 7f92e73,
72f885d, 5d3597f): one prosthesis type per tooth became stacked jobs,
jaw-level appliances and a category tree. Voice still compiled against it
but could no longer express it, and in two places wrote data the form
itself refuses.

Rewrites the extraction contract (§5), the resolver rules and the
unresolved-reason table (§6), the review sheet (§7) and verification
(§12), and records decisions 34-46. Adds the repos: block and a progress
ledger so the task resolves from the branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 15:49:35 +03:30
3ed60d9cfa Merge pull request 'users get notified whenever a task is done completely, not per step' (#78) from improvement/despam-notifications into master
Some checks failed
Registry — build, push, deploy / build-and-push (push) Successful in 39s
Registry — build, push, deploy / deploy (push) Failing after 36s
Reviewed-on: #78
Reviewed-by: admin <admin@localhost>
2026-09-08 12:03:08 +03:30
e4f1759035 fix: use mapped table name in case_completed migration
All checks were successful
Production — tag build, push, deploy / build-and-push (push) Successful in 28m21s
Production — tag build, push, deploy / deploy (push) Successful in 12m29s
Prisma migrate failed on prod (P3009) because DELETE targeted
"UserNotification" instead of user_notifications.

Co-authored-by: Cursor <cursoragent@cursor.com>
v1.0.14
2026-09-08 10:42:22 +03:30
62d5d94121 review notification logic and fix minor issue.
All checks were successful
Production — tag build, push, deploy / build-and-push (push) Successful in 1m0s
Production — tag build, push, deploy / deploy (push) Successful in 31s
v1.0.13
2026-09-07 23:18:00 +03:30
880809fdbe improvement: users get notified whenever a task is done completely, not per step. 2026-09-07 22:36:33 +03:30
e52701dba3 Merge pull request 'update adminjs with current db schema.' (#77) from feature/ci-cd into master
All checks were successful
Registry — build, push, deploy / build-and-push (push) Successful in 1m26s
Registry — build, push, deploy / deploy (push) Successful in 2m43s
Reviewed-on: #77
Reviewed-by: admin <admin@localhost>
2026-09-07 21:40:17 +03:30
b1bcfc69e4 Lab Case Detail datatset load faliure fixed.
All checks were successful
Production — tag build, push, deploy / build-and-push (push) Successful in 26m42s
Production — tag build, push, deploy / deploy (push) Successful in 13m27s
v1.0.12
2026-09-07 20:41:18 +03:30
0d2c279f4b Merge branch 'master' into feature/ci-cd
All checks were successful
Production — tag build, push, deploy / build-and-push (push) Successful in 36s
Production — tag build, push, deploy / deploy (push) Successful in 12m8s
v1.0.11
2026-09-07 18:33:04 +03:30
fb59aed2f8 Merge pull request 'improvement: Frontend Dockerfile updated so that aseests in public folder loads on the website.' (#76) from improvements/subscription-v1 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: #76
Reviewed-by: rameen <rameen.naghdi@gmail.com>
2026-09-07 18:31:29 +03:30
4add3ab859 update adming js 2026-09-07 18:21:30 +03:30
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
ea6976351a Merge pull request 'improvements/subscription-v1' (#75) from improvements/subscription-v1 into master
All checks were successful
Registry — build, push, deploy / build-and-push (push) Successful in 31m6s
Registry — build, push, deploy / deploy (push) Successful in 2m1s
Reviewed-on: #75
Reviewed-by: aminmsvi <amn.mousavi@gmail.com>
2026-09-06 12:55:39 +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