Repository navigation
Added
-
Engine CodeSystem embedded in the FHIR export (issue #23 follow-up). The
Bundle now carries the engine's ownCodeSystemresource (canonical
…/fhir/CodeSystem/odontogram, placed right after the Patient) enumerating
every code the export can emit under the local system — finding types taken
straight from the clinical-axis registry, every enum value (repeated bare codes
such asnone/mesial/temporaryare merged, their alternative displays kept
as designations), the periodontal panel/index/qualifier codes, the 2017 stage/
grade/extent summaries and case-condition laterality. Validators can therefore
resolve the local codes from the Bundle itself instead of timing out on an
unpublished system. Opt out withincludeCodeSystem: falsein the FHIR export
options. The same resource is published asfhir/CodeSystem-odontogram.json
(npm run fhir:codesystemregenerates it; a test keeps it in step with the
generator, and a maximal-export test proves every emitted local code is
covered). Plugin-definedcustom-state:<pluginId>codes stay open-ended by
design. -
Guided tour: Diagnoses steps. Two new tour steps introduce the per-tooth
Diagnoses card and the Case / regional diagnoses pop-up button (18 steps in
total), in all 12 UI languages. -
README refresh in all 12 languages: the project logo at the top, a pointer
to the official Angular port (Angular Advanced Odontogram) under the API-docs
link, the author linked to LinkedIn in the license section, and the 2.5.0
features (Diagnoses card controls, the case-diagnoses pop-up, the official
NEAK BNO-10 titles, the validator-clean FHIR export with the embedded
CodeSystem). -
Dental diagnosis coding (DX-0 foundation). Charted findings now export as
FHIRConditionresources with a WHO ICD-10 base coding, plus an optional
pluggable national code pack (Settings → General → Diagnosis coding system;
ships WHO-only + BNO-10). DX-0 covers caries (K02) and refactors the perio
(K05) Condition to share the coding builder (default output unchanged). More
findings, the diagnosis picker, and further packs (US ICD-10-CM, SNOMED) follow
in later sub-projects. -
Dental diagnosis coding: charted findings (DX-1). Beyond caries, the FHIR
export now derives WHO ICD-10Conditions for pulp (K04.0/.1), apical
(K04.4-.9, radicular cyst K04.8), root caries (K02.2/.3), tooth wear (K03.0-.2,
.8), resorption (K03.3), calculus (K03.6), discoloration (K00.3/.8, K03.7),
tooth loss (K08.1) and retained root (K08.3) — one Condition per finding per
tooth, presence-gated. Fractures and peri-implant disease are not yet coded
(no WHO ICD-10 code). The active national pack (e.g. BNO-10) still overlays. -
Dental diagnosis coding: per-tooth picker + overrides (DX-2). A new
Diagnoses card lists a tooth's chart-derived ICD-10 diagnoses and lets the
clinician suppress one or add a tooth-level diagnosis the chart does not
represent; the effective coded set (derived − suppressed + added) drives the
FHIR export and the coded-diagnoses summary/tooltip. Payload 2.21 (additive
dxOverrides); the visual chart is unaffected. -
Guided intro tour, reworked and extended. The tour now steps with the left
and right arrow keys (a teardown bug used to unbind the keyboard handler after
the first step), targets the real restoration control instead of a control that
no longer exists, points the numbering step at Settings (where numbering now
lives) and the language step at the language menu, and grows from 12 to 16 steps
with new steps for root canal treatment and the periodontal view (it switches
into the periodontal view and back, and skips those steps when the periodontal
chart is turned off). The Intro toolbar icon is now a play in a circle. -
About and credits popup. A new toolbar button after Import opens an About
and credits dialog with a short program description, a contributor list that
links each person to their GitHub profile, the external libraries the app is
built with, an invitation to open a pull request, and a Star on GitHub badge. A
GitHub link is also added to the toolbar. Translated into all 12 UI languages. -
Community health files. Added
CODE_OF_CONDUCT.md,CONTRIBUTING.mdand
SECURITY.md, and a Credits section to every language README inlang/. -
Selectable tooth-anatomy profile (Settings → Odontogram → Tooth anatomy;
#18/#22). A new session-level setting chooses the tooth-anatomy profile:
classic (the default, unchanged) or measured. The measured profile
renders nine literature-measured tooth templates in a two-arch, per-tooth-width
layout (from the cognovis "candidate anatomy" work), switchable at runtime via
the shared session. Thetools/toothgenPython generator (dev-only, out of the
npm package) regenerates the measured drawings from their hand-authored source
SVGs. No payload/render change for the classic profile; goldens byte-identical.
Known limitation: the measured molar templates 17/46 omit ~16 milktooth /
pulp-inflammation layers, so those specific findings do not draw on those
positions in the measured profile. -
Composable UI: declarative control cards (issue #20, Tier 3). The control
cards are being converted from imperative id-bound wiring to self-contained
declarative React components (followingPerioSidebar), one per release step,
each reading engine state via a newuseEngineState()hook and writing through
new getters/selection-setters. Converted so far: the Orthodontics card
(OrthodonticsCard;getActiveOrtho(),setOrtho{Appliance,Drift,Vertical,Rotation}ForSelection)
the Statuses card body (StatusesCard;getEdentulous(),resetMouth(),
reusingsetEdentulous/applyPrimaryDentition/applyMixedDentition/getStatusExtras/applyStatusExtra)
— also fully declarativizing the Edentulous toggle (itsaria-pressednow derives
from state, replacing the delegated handler + imperative syncs) — and the
Caries card body (CariesCard+ a reusableSurfaceCrossper-surface grid
component;getActiveCaries()+ its selection setters), and the Fillings card
body (FillingsCard, reusingSurfaceCrosswith the recurrent-caries + defect
indicators and the simple/complex mode swap;getActiveFillings()+ its setters),
and the Root / periodontium card body (RootPeriodontiumCard;
getActiveRootPerio()+ its setters — pulp/endo optgroups with the endo↔pulpDx
exclusion, apical/resorption/mobility/peri-implant/calculus/mods; the 6-site
probing grid stays imperative for now), and the Tooth details card body
(ToothDetailsCard;getActiveToothDetails()+ its setters +resetTooth()—
base/substrate/restoration with per-tooth option lists and${type}|${material}
encode/decode, wear/discoloration, and the full row-visibility sweep incl. the
#extractionPlanRowreparenting). With this, all six control cards are
declarative:wireControls()/syncControlsFromState()no longer build or sync
any control card, and the imperativebuildSelectmachinery is removed (only the
carved-out 6-site perio probing grid remains imperative). Behavior and rendered
DOM are unchanged. No payload/FHIR/render change; all goldens byte-identical;
payload version unchanged. -
Composable UI: on-demand surfaces (issue #20, Tier 2). Control wiring is now
re-runnable / idempotent, so a surface can unmount and remount (e.g. mount on
demand in a drawer or tab) — the Tier 1 "mount every surface before init / hide
with CSS rather than unmount" constraint is lifted. New exportedrewireControls()
andrebuildGrid()let a remounting surface rebind its fresh DOM (surfaces do this
automatically on mount). Internally, per-element binding is deduplicated via a
WeakMapmarker (replacing the one-shotcontrolsWiredguard), and the three
former hide-instead-of-unmount workarounds (the control panel across the perio
view, the chart column in the Dental Chart segment, and the Status|Plan toggle) are
now real conditional mounts.OdontogramShell's default composition is unchanged
and still renders byte-identical DOM. No payload/FHIR/render change; all goldens
byte-identical; payload version unchanged. -
Composable UI surfaces (issue #20, Tier 1). The shell's four UI regions are
now exported as standalone components —OdontogramTopbar,
OdontogramChartSurface,ToothInfoSurface,ToothControlsSurface— that a
host can place in its own layout regions, together with a new
OdontogramProviderand auseOdontogramUi()hook (plus the
OdontogramUiContextValuetype) for building custom surfaces. All surfaces
share one package-owned session through the provider.OdontogramShellis
unchanged and remains the supported all-in-one component — it is now a thin
composition of these surfaces under the provider, and its rendered DOM is
byte-identical to before (frozen by a new shell DOM-parity test). Tier 1
keeps two documented constraints (one provider per document; surfaces mounted
beforeinitOdontogram()and hidden with CSS rather than unmounted); later
tiers lift them. Session/composition only — no payload/FHIR/render change;
SVG-fingerprint, FHIR-golden, and roundtrip fixtures byte-identical; payload
version unchanged. -
Fillings settings as controlled props (issue #17).
<OdontogramShell>
now accepts the four fillings-tab settings as optional props —
fillingComplexity?: "complex" | "simple",
fillingDefectEnabled?: boolean,
fillingMaterialAvailability?: Record<string, boolean>(over
"amalgam" | "composite" | "gic" | "temporary", unknown keys ignored), and
fissureSealingEnabled?: boolean— plus their write-back callbacks
onFillingComplexityChange(v),onFillingDefectEnabledChange(v),
onFillingMaterialAvailabilityChange(material, enabled)(per-material,
mirroring the engine setter) andonFissureSealingEnabledChange(v), fired
from the Settings → Fillings tab. Unlike thepulpDetailLevel-style props,
the sync is defined-gated: an omitted prop never writes the engine, so
an imperativesetFillingComplexity()call before mount is preserved and
standalone mode is unchanged; a provided prop writes the engine and the
React state together, so the Settings modal never shows a stale value. The
fillingMaterialAvailabilityprop is dependency-keyed on a canonical
serialized form (sorted keys) with a diff against the last applied record —
an inline literal with identical content does not re-fire the engine writes.
FillingComplexityis re-exported from the shell. Session-only state: no
payload/FHIR/render change (SVG-fingerprint, FHIR-golden, and roundtrip
fixtures byte-identical; payload version unchanged). -
Fillings session-flag setters notify + are idempotent. The four
fillings setters (setFillingDefectEnabled,setFillingComplexity,
setFissureSealingEnabled,setFillingMaterialAvailability) now follow the
same convention assetPulpDetailLevel/setSurfaceNotation: an early
return when the value is unchanged (no redundant
notifyStateChange()/re-render), and anotifyStateChange()on every real
change soonStateChangesubscribers (e.g. hosts persisting preferences)
observe fillings-setting changes.setFillingMaterialAvailabilityalso
ignores unknown materials without notifying. -
Dental diagnosis coding: tooth fracture + peri-implant (DX-3a). A broken
tooth now codes to WHO ICD-10 S02.5 (Fracture of tooth). Peri-implant
mucositis and peri-implantitis are recognized as diagnoses but have no WHO
ICD-10 code, so they are surfaced as "no WHO code" in the tooltip and summary
and emit no FHIR Condition — a coding follows when SNOMED CT is activated.
Payload unchanged (2.21); the visual chart is unaffected. -
Dental diagnosis coding: case/regional conditions (DX-3b). A new
"Case / regional diagnoses" picker in the perio case panel authors whole-mouth /
regional conditions (malocclusion & TMJ K07, oral cysts K09, salivary disease K11,
stomatitis & oral mucosa K12/K13, arch-level developmental K00 — ~28 conditions),
each optionally lateralized (left/right/bilateral). They export as patient-level
FHIR Conditions and appear in the whole-mouth summary. Payload 2.22 (additive
caseConditions); the visual chart and per-tooth data are unaffected. -
Dental diagnosis coding: complete BNO-10 (Hungarian) pack (DX-4). The BNO-10
coding pack now carries Hungarian displays for every coded tooth-level and
case-level diagnosis (selectable in Settings → diagnosis coding); case Conditions
localize under the BNO system too. A newCODING_PACKS.mdguide documents how to
add another country's pack. No payload change (2.22). -
Dental diagnosis coding: US ICD-10-CM pack (DX-5). A selectable ICD-10-CM
(Clinical Modification) coding pack — the firstmodification-class pack — that
remaps the diagnosis code (not just the display) on tooth AND case Conditions
(e.g. caries → K02.9, the K07 dentofacial anomalies → the M26 range), on top of the
always-on WHO ICD-10 base. Reference/best-effort — verify against the official
ICD-10-CM tabular list before US clinical use. No payload change (2.22). -
Dental diagnosis coding: SNOMED CT overlay (DX-6). An opt-in Settings toggle
(SNOMED CT, default off) adds a SNOMED CT coding to each FHIR Condition alongside
the WHO ICD-10 (and any national pack) coding. It codes the peri-implant findings
(which have no WHO ICD-10 code) as SNOMED-only Conditions and adds a SNOMED
laterality qualifier to case-condition bodySites. The seeded concept IDs are
provisional — verify against the official SNOMED CT browser before clinical use.
No payload change (2.22). -
Dental diagnosis coding: FHIR import round-trip (DX-7).
parseFhirBundlenow
reconstructs the diagnosis layer from Condition resources — case/regional conditions
(with laterality) and per-tooth diagnosis overrides (add/suppress, diffed against the
re-derived chart) — completing the FHIR round-trip for our own exports. Chart-only
bundles are unaffected. No payload change (2.22).
Fixed
- FHIR export now passes the HL7 validator's Bundle identity rules (issue
#23). validator.fhir.org flagged thecollectionBundle on two counts: 32 of
33 entries had noBundle.entry.fullUrl(constraint bdl-15 requires one on
every entry of a non-transaction Bundle), and the placeholder subject used
urn:uuid:odontogram-subject, which is not a valid RFC 4122 UUID (the
urn:uuid:scheme may only wrap a real UUID — the same latent fault sat on the
Condition and evidence-Observation fullUrls). Every entry now carries a
deterministicidplus an absolute, readablefullUrlunder the engine's own
FHIR base (https://github.com/ZoliQua/React-Odontogram-Modul/fhir/<Type>/<id>,
a sibling of the local CodeSystem URL); per-tooth Observations derive their id
from content (type + FDI tooth + finding code, ordinal-suffixed for per-surface
repeats), and everysubject/evidence reference points at those fullUrls. No
random UUIDs, so the export stays golden-testable. The FHIR import is
unaffected (it readsresource.id, neverfullUrl); the JSON round-trip golden
is byte-identical. The remaining validator messages are terminology-server
validate-codetimeouts on the engine's own (unpublished) CodeSystem — expected
for a local code system, not a defect. Reported by @BabuBahir. - Dental diagnosis coding: verified the ICD-10-CM + SNOMED codes. Corrected two
provisional SNOMED concept IDs (pulpitis → 32620007, peri-implantitis →
699422003) and added a SNOMED concept for peri-implant mucositis (699684005),
and fixed the ICD-10-CM tooth-fracture display title; all ICD-10-CM codes were
confirmed against ICD-10-CM 2026 and the SNOMED concepts against SNOMED CT
International (CSIRO Ontoserver). - FHIR Condition ICD-10 code for the molar-incisor pattern. A molar-incisor
periodontitis emittedK05.2with a "Acute periodontitis" display, which is
the WHO ICD-10 meaning of K05.2 (an unrelated diagnosis; the ICD-10-CM
"aggressive periodontitis" meaning does not belong on an R4/WHO bundle). It now
emitsK05.3(Chronic periodontitis) like every other periodontitis; the
molar-incisor pattern is still carried by the periodontal-extent stage entry. - Plugin
customStatesisolation between the status and plan charts. An
object-valued plugin state was shared by reference across the two charts (they
are cloned viaserializeState->hydrateState, which passedcustomStates
by reference), so mutating it in one leaked into the other. Object values are
now deep-copied on hydrate.
Changed
- Case/regional diagnoses moved into a pop-up. The "Case / regional
diagnoses" list left the Periodontal-status sidebar and now lives in its own
focus-trapped dialog (CaseDiagnosesModal), opened from a new Diagnoses
button beside the Odontogram / Periodontal-status view toggle. The add-picker
in the pop-up is code-first (K07.6 …) and code-sorted, matching the rows.
(Shell-DOM parity fixture re-frozen for the one new button; the SVG/FHIR/
round-trip goldens are byte-identical.) - Diagnoses row controls: exclude toggle + delete, and a full-delete that
clears the chart. Each diagnosis row is now three explicit parts — the
code-first text, an exclude toggle (eye/eye-off icon with a hover tooltip;
drops the finding from the FHIR export but keeps it on the chart), and a
delete (×) button. Delete now calls a newremoveDiagnosisFromSelection,
which clears the underlying chart axis so the diagnosis and its glyph are
removed (it also coverscaries, clearing the surfaces). This replaces the old
CSS-hidden suppress checkbox, whose "Exclude" label rendered as unclickable
text. The periodontal Case/regional diagnoses list adopts the same code-first
layout and an × delete icon, is sorted by ICD-10 code (getCaseConditionsnow
code-sorts, so the list and its summary enumeration match the Diagnoses card),
and its rows stay on one line (the × no longer wraps under a long label). - BNO-10 pack aligned to the official NEAK titles. The Hungarian (BNO-10)
diagnosis-coding pack now carries the official NEAK BNO-10 display strings
(verified against the NEAK "3- és 4-jegyű BNO kódok" törzs) instead of the
earlier clinician-friendly paraphrases, and uses the standard ICD-10 system URI
(http://hl7.org/fhir/sid/icd-10) rather than the previous stand-in — the
Hungarian codes are identical to WHO ICD-10, so a consumer sees one ICD-10 code
with both English and Hungarian displays. All 53 codes were confirmed present in
the official törzs, with one documented exception: oral mucositis (K12.3) is a
valid WHO code but absent from the Hungarian BNO-10, so it keeps the WHO code
with our own Hungarian display. Default (WHO-only) FHIR export is unchanged. - Diagnoses card: code-first, sorted, and add-writes-the-chart. The per-tooth
Diagnoses card now shows the ICD-10 code first (e.g. "K04.0 Pulpitis"), sorts the
diagnoses by code, and — when you add a diagnosis from the picker — writes the
underlying chart finding (so the glyph appears on the tooth and it derives/exports
normally) instead of only tagging the coded layer. Plain caries stays authored per
surface in the Caries UI. Suppressing a derived diagnosis remains coded-layer only.
Security
- Prototype pollution on FHIR import. A crafted bundle whose
bodySite
carried a non-numeric tooth code (e.g."__proto__") reachedensureTooth,
where the naiveif (!teeth[id])guard returnedObject.prototypeand later
writes leaked onto it. The importer now accepts only two-digit numeric tooth
codes (rejecting__proto__/constructor/prototype) andensureToothuses
an own-property check. Regression-tested.
CI
- Run ESLint as a required CI step alongside type-check, test and build.