Skip to main content

Changelog

All notable changes to LOSPOR are documented here.


9.9.5 - A case that was not submitted no longer looks like one that was

Reaching the summary is no longer proof the case was accepted

Submitting the postoperative form asks the server to start the closure countdown. If the server refused — because the case was not complete enough to close — both the web app and the phone ignored the refusal and carried on to the summary anyway.

That summary is the screen that says the case is finished. Behind it the case was still in progress with no countdown running, and nothing on the screen distinguished the two. Nothing would have told the clinician later either.

A refusal now keeps you on the postoperative form and says what is missing.

Automatic closure is a hospital-appliance feature

On a hospital appliance a background job finalises expired cases every five minutes, whether or not anyone is looking at them.

The hosted service has no such job. There, a case is finalised when you click Close Now, when the countdown reaches zero while you have the case open, or when you next open a case whose window has already elapsed. A case you submit and then navigate away from stays Awaiting review until someone returns to it. The postoperative guide now says so plainly.


9.9.4 - The dashboard tells the truth about which cases are closing

A case in its closure window was labelled "Awaiting postop"

The dashboard never checked for the awaiting-review status. It looked at whether the intraoperative record had an end time first — and a case inside its 30-minute window has one, along with a completed postoperative form — so it was shown in the state it had just left. The one status that is time-critical was the one displayed wrongly. It now reads Awaiting review.

The countdown could start on a case that could never close

Submitting for review checked only the recovery score and the discharge destination. Finalisation checks considerably more: the preoperative sections, an intraoperative record with both times and a technique, and the postoperative form. A case could therefore enter the review window and promise a closure that could not happen.

Both now ask the same question, and a refusal names what is missing.

One ward's unfinished paperwork could stop closure for everyone

The job that closes expired cases takes the twenty-five oldest and works through them. A case it could not close kept its place in that queue, so it was picked up again on every run, for ever. Twenty-five such cases meant the twenty-sixth was never reached — and nothing reported it.

A case that cannot be closed is now set aside for a while and retried later, so the cases behind it are always reached.

"Awaiting allocation" meant two different things

The web dashboard and the phone disagreed about when a case was ready to schedule. The web app wanted a diagnosis and ignored age and sex; the phone did the reverse. The same case could read as ready on one and not the other.

There is now one rule, used by both: diagnosis, planned procedure, ASA grade, age and sex.

The phone's "Awaiting Postop" count matched no list

The number on the tab counted cases whose operation had finished. The list underneath showed every case with any intraoperative record at all, including those still in theatre. The tab therefore showed a smaller number than the list it labelled. Both now mean the same thing.


9.9.0 - Finishing postop is what starts the countdown, not an autosave

The closure countdown starts when you say you are done

The 30-minute window used to begin whenever an autosave happened to complete the last postoperative field. Continuing to type could start the clock on a case you were still writing. Reaching the summary — a deliberate act — is now what starts it.

The countdown is no longer shown before the case has actually entered review

The banner could appear before the server had confirmed the case was awaiting review, showing a countdown for something that had not begun.


9.8.0 - The printed record stops cutting itself short

Both pages continue instead of clipping

The two-page anaesthetic record silently cut off anything that did not fit rather than carrying it to the next page. Long cases lost the end of what they had recorded.

Investigations are recorded as intraoperative

Investigations on the printed record now sit with the intraoperative section, which is where they are taken.


9.7.1 - The phone reported the wrong version of itself

The application sent version 8.0.0 to the server regardless of the version it actually was. The server compares that number before permitting paediatric work, so an app that was perfectly current could be told it was too old to record a child.


9.7.0 - Maintenance

No clinician-visible change.


Scanning a laboratory report sends a photograph of a printout that carries the patient's name and national identifier in its header, and cannot be redacted.

The route used to accept the application's own word that consent had been given. It now reads the consent recorded on the case and ignores anything the client claims — the same way the monitor scanner has always worked. A report cannot be sent for a case whose record does not carry consent.


The scanner appeared whether or not AI use had been agreed

The lab-report scanner was offered on every case, including those where external AI processing had not been consented to.

A redaction defect affected research exports

Corrected in the API. It applied whether or not external AI was ever enabled.


9.4.0 - Correcting a case from paediatric to adult no longer strands it

A case corrected to adult now saves, instead of sitting on "saved locally"

If a case was started in paediatric mode and then corrected to adult — because the patient was an adult all along — the change could never be saved. The screen reported "saved locally, waiting for connection" and kept trying, while the server was reachable and answering the whole time.

The cause was that switching to adult did not actually tell the server to forget the paediatric age. It simply stopped mentioning it. The server kept the precise age it already had, that age continued to say "this is a child", and it declined the change every time it was resent. Nothing about retrying could ever help, but the wording invited the clinician to wait for a connection that was never the problem.

Switching now clears the stored age explicitly, and the save goes through.

The age limit itself is unchanged: a patient genuinely under eighteen still cannot be recorded as an adult. What is fixed is correcting the age and the mode together.

The warning about the wrong mode no longer sounds like a personal-data alert

When one of these refusals did reach the screen, it borrowed the wording used for entries that contain identifying information — so a clinician correcting an age was told the age field contained personal data. Age and mode problems now say what they actually are.

"Switch mode" now says which mode it will switch to

The button beside the warning gave no destination. Sitting next to a message about the mode you had just chosen, it read as an offer to overrule that warning, when it does the opposite. It now reads "Switch to paediatric mode" or "Switch to adult mode".

Registration: the guide said institution was optional. It is required.

The Getting Started guide told new users they could skip choosing an institution. Registration has always required one. The guide now says so, and explains what to do if your institution is not on the list: register with the closest one, then request the correct department from Settings.


9.3.1 - The offline vitals were never lost, but the phone said they were

A phone offline mid-case now says "unsynced," not "could not be saved"

A vital recorded while the phone had no signal was already doing the right thing: it stayed queued and replayed automatically the moment the connection came back. But the screen didn't say that. It showed the same alarming message as a genuine rejection — "The case could not be saved. Check the connection and try again" — for an entry that was never in any danger.

The two situations are told apart now. A save that is actually stuck offline reads "unsynced," matching what is true: it is waiting, not lost.

An exported PWA now actually reaches the API, instead of quietly not

The installable web app talks to the API through its own address so a browser never needs a separate login token. A gap in that address's setup meant a freshly exported PWA could silently answer every request with its own start page instead of forwarding it — including the sign-in request itself, which still came back looking like success. The clinician appeared to be signed in in a build that had never reached the server at all.

The exported PWA now proxies every one of those requests to the API for real, and a client that only ever received its own start page back is now treated as signed out rather than as logged in.


9.3.0 - A case remembers who created it, even after it changes hands

A transferred case keeps its author on record

Who wrote a case and who is currently responsible for it used to be the same field. Handing a case to a colleague — or a HOD moving it during a reorganization — quietly rewrote who had authored it. The person who ran the anaesthetic and finished the record was no longer named as either.

Authorship is now permanent and separate from the current assignee. The clinician who created a case keeps read-only access to it after a handover, even though someone else now owns it.

The interface asks which language you speak before it asks anything else

Web, the phone app, and the exported PWA now default to Bulgarian, with English available from the same prominent selector on the sign-in screen before any credential is entered. A completed login adopts the account's saved language; an explicit choice made at sign-in is written back to that account for next time. Every authenticated clinical screen was carried through this pass — the live intraoperative timetable, the irreversible end/discontinue/continue prompts, keyboard shortcuts, fluid-conflict handling, and the clinical-rule editors included. What was left in English on purpose — drug names, standardized scores, units, and other controlled clinical terms — is an explicit, reviewed list, not an oversight.

A hospital can require its own usernames instead of email

The public Cloud demo keeps ordinary email sign-in, self-registration, and email-based password recovery exactly as before. A Hospital deployment can instead require a case-preserving, administrator-issued username with no email fallback at all — registration and email-recovery links disappear from that build entirely, and a missing or contradictory server answer about which mode is active fails closed rather than guessing.

Administrators can turn on two-factor sign-in

A clinical administrator's account can now require an authenticator app in addition to a password. First sign-in walks through enrollment — scan a code or enter a key by hand — and hands back exactly ten one-time recovery codes; the screen will not let the administrator move on until they confirm the codes were saved. The six-digit code and recovery-code secrets never touch browser storage and are discarded the moment the sign-in finishes, one way or the other. Ordinary clinician sign-in is unaffected, and the public demo stays on single-step login unless a deployment explicitly turns this on.

Dose guidance stopped reading like a recommendation

Premedication, bolus, and infusion panels used to show a configured range, a source note, and quick-pick dose buttons alongside the entry field — worded and laid out like the app was suggesting a dose. It wasn't meant to be read that way, but it was reasonable to read it that way.

The range prose, the source note, and the quick-pick buttons are gone from those panels. A calculated starting value is still offered and stays editable, and a withheld-drug or manual-entry safety message still shows when it applies, but nothing on screen now looks like a recommendation.

Every audit entry has a real name, in both languages

The administrator audit log used to show whichever internal code a client happened to hard-code for an event, which drifted out of step with new event types over time and never had a Bulgarian label. The log now reads from one registry the server owns: every entry, in Bulgarian or English, with a filter that only ever offers a code that is actually registered. An older entry whose code has since been retired still shows its raw value rather than disappearing.

An account can be suspended or removed without erasing who did what

Continuing the change in 9.2.0 that stopped deleting an account from also deleting the record of what it had done: an administrator can now suspend, delete, or restore an account as distinct, reversible states, with the exact timestamps that separate an invited, active, suspended, deletion-pending, and recovery-required account. A deleted account is anonymised after thirty days, not immediately — restoring it inside that window requires the returning clinician to set a new password before signing in again. The last remaining clinical administrator on a deployment cannot be demoted, deleted, or converted away, so a hospital can never end up with none.

Terms and Privacy acceptance is now a real, checkable record

Registering used to record a bare "accepted" flag. It now records exactly which version of Terms and which version of Privacy were shown, in which language, and their content hash — checked against the API's own copy at the moment of registration, so a client cannot silently submit a hash for text the clinician never actually saw. Existing accounts are not retroactively treated as having accepted anything; nothing is invented on their behalf.

Research access is granted for hours, not indefinitely

A researcher can self-authorize aggregate-only access to a cohort for at most eight hours, and at most once in any rolling day — export, row-level inspection, OMOP mapping, and sharing are outside what that self-authorization covers. Case labels in the research Browser use a stable, unlinkable pseudonym; the underlying case ID, case code, and patient number are never rendered there, in an aggregate view or an inspectable one.

A saved research cohort no longer disappears under someone else's edit

Editing a shared cohort's filters could previously overwrite a colleague's more recent change with no warning either way. Saving now checks the version you started from and refuses — with a visible conflict, not a silent loss — if someone else changed it first. A cohort's sharing visibility is also no longer rewritten as a side effect of an unrelated metadata edit.


9.2.0 - The record says what it does not know

A risk score now says how much of it was asked

RCRI, Apfel and STOP-BANG count a criterion nobody asked about as absent. That is deliberate: a question never put to a patient must not push a score upward. But the card showed a bare number and a colour band, so "RCRI 1 — low" looked identical whether five criteria had been answered "no" or never asked at all.

Each score now says how many of its criteria were answered, and only when some were not. The number and the band are unchanged — a partial score is still the best estimate available, as long as it says what it rests on.

Researchers get the same distinction: the component inputs export as yes, no, or not asked, so an analysis can tell a negative finding from an absent one.

A case cannot be moved to another hospital

Transferring a case to a colleague at a different institution used to rewrite which hospital the case belonged to. The printed protocol, the record, and the care_site in the OMOP export then all said the operation had happened somewhere it had not.

Transfers stay within the institution now, for administrators too. A finalised case cannot be transferred at all — unfinalise it first, so the change is captured rather than applied underneath an attested record.

Finalising a case no longer overwrites the last time you finalised it

Finalising froze a copy of the record. Unfinalising, correcting something and finalising again replaced that copy, so what was first attested to was gone. Each finalisation is now its own record, and the database refuses to alter or delete one. A correction supersedes; it does not erase.

Overwriting a colleague's edits is recorded

Saving over a newer version was possible without any trace that a conflict had occurred. It is still possible — a save queued while offline is out of date by definition, and refusing it would lose work — but it is now recorded: which sections were overwritten, and which version was discarded.

Removing an account keeps the cases

An administrator removing an account previously failed outright for any clinician who had recorded a case, and where it succeeded it destroyed the record of what that account had been permitted to see.

The account is now marked deleted, every session ends immediately, and it is anonymised after thirty days. The clinical records it authored are untouched and keep their author.


9.1.0 - A question you did not ask is not a "no"

Clinical yes/no questions became three-valued: yes, no, and not asked. An untouched field and a recorded negative used to reach the register as the same answer, and a study counting them together was counting something it had not measured. Both the web form and the phone app now ask as a pair of answers, say when a question has not been asked, and let a mis-tap be cleared.

Existing records were deliberately left as they are. Rewriting them would have discarded the genuine "no" answers among them, and that distinction cannot be recovered afterwards.


9.0.0 - The same question gets the same answer everywhere

A child matched by two dosing rules is now told so

Where two approved paediatric dose bands both covered the same child — an age range and a weight range that overlap — the phone sorted the candidates and used the first one. The web app refused. The same child, on the same ruleset, could therefore be suggested a different dose depending on which device was in your hand.

Neither device now guesses. The overlap is stated on screen, no dose is suggested, and no clinical rule is recorded against whatever you enter, because no rule was used. You can still record the dose you judge correct — an earlier version of this fix refused to invent a dose and then also refused to accept a typed one, which left the sheet unusable. Stating a conflict and enforcing it are different things.

Infusion routes a ruleset has withdrawn are no longer offered

Selecting one produced an empty box with no explanation. A drug could also disappear from the list entirely when only its default route had been withdrawn, even though another usable route remained.

Work you had already saved is no longer lost when you reopen a case

Reopening an unfinished case silently cleared several fields that had been saved — including whether the surgery was elective, and the AI consent flag. Everything that was saved now comes back.

Two related corrections: a patient not yet scored in recovery was displayed as 0 out of 10, the worst possible Aldrete, in alarm red, because an unrecorded score was treated as a real one. And a recovery save the server had refused still advanced to the summary and started the countdown that closes the case, so work that had not been accepted looked finished.

The benchmark screen no longer answers questions it cannot answer

The benchmark tool offered fourteen measurements and could calculate five. The other nine returned an empty chart — indistinguishable from "no patients matched your filters" and from "withheld because too few cases to report safely". Three very different answers looked identical, and the most natural reading of a blank is a finding about your data.

The menu now lists only what can be calculated, and the two remaining empty states say which one they are. Where some periods are withheld for small numbers, the chart is still drawn and the withheld periods are counted for you.

Research exports: numbers are now exported as numbers

Twenty-two scored variables — the Aldrete subscores and total, RCRI, Apfel, STOP-BANG, POVOC, COLDS, PAED, the paediatric pain scales, anaesthesia duration, fluid totals and others — were documented as numeric but written into a free-text column. Anyone analysing them had to convert text back to numbers and hope the conversion matched ours.

Two further corrections matter for anyone who has already planned an analysis:

  • A pain score was being exported under the standard OMOP concept for body temperature, inherited from the vitals mapping. Pooled with other sites, a pain score of 2 would have answered a temperature query as 2 °C. It is now exported under its own LOINC code.
  • Seventeen documented value ranges were narrower than what the application actually accepts — age documented as 0–120 while accepting up to 149, oxygen saturation documented as 50–100 while accepting 0. Filtering on a published range would have silently excluded real records. The documentation now states the ranges the software enforces.

The data dictionary and the export are checked against each other automatically, so a variable can no longer be documented under one name and exported under another.


8.5.0 - The intraoperative screen gets out of the way

Switching intraoperative tabs is roughly fifty times faster

Moving between tabs during a case took well over a second, and every tab felt the same regardless of how much it displayed. The cause was that each switch re-rendered all fourteen bottom sheets — drugs, infusions, fluids, agents, vitals and the rest — even though every one of them was closed. Nothing was drawn on screen, so nothing looked wrong, but each was still filtering the drug catalogue, building scenario lists and calculating doses.

Measured on the device, the closed sheets accounted for 1335–1652 ms of every switch; the tab actually being opened took 5–41 ms. Only the open sheet renders now.

The preoperative form no longer slows down as you fill it in

Every keystroke compared all 106 form fields against their previous values by serialising both — over two hundred serialisations per character, across data that grew with every diagnosis, medication and laboratory value added. So the form became progressively less responsive the more of the patient you recorded, which is exactly backwards. The comparison it was making only concerned yes/no fields, and now costs nothing.

Saving no longer reports "Offline" while online

A save that took longer than a few seconds — ordinary over mobile data — was being treated as a failed connection, so the application announced itself offline while it was saving perfectly well. It now waits properly before drawing that conclusion, and having drawn it once, queues subsequent work immediately rather than waiting again each time.

Background syncing cannot silently stop

A single request that never answered could leave the background sync asleep for the rest of a session — queued work sat until someone pressed sync by hand. Polls now run under a watchdog, so the loop always recovers. The same fix restores live case updates in the web application, which stopped refreshing under the same conditions.

The application does less work behind the scenes

Every call to the server performed three encrypted Android keystore operations — reading back the sign-in token and recording two diagnostic timestamps — on the path of every save, every poll and every event recorded during a case. None of it was necessary.

Diagnostics

Settings → Diagnostics now reports where time goes during a case: how long tab switches take and which part of the work accounts for it, alongside queued edits, whether the server is reachable, and the offline vocabulary version. This is what identified the sheet rendering above, after several plausible explanations turned out to be wrong.


8.4.0 - Diagnosis and procedure search without a connection

Diagnosis and procedure search now work offline

Both pickers were network-only. With no connection they returned an empty list, which reads as "there is no such diagnosis" rather than "there is no network" — and because a case cannot be finalised without a diagnosis, a case could be documented in full offline and only then found to be unfinishable.

The mobile application now carries the whole ICD-10 vocabulary — 16,175 codes with Bulgarian and English labels — and the full procedure list. Ranking moved into the shared clinical core, so offline results are the same results the server returns rather than an approximation; this is verified against the live database rather than assumed.

Results that came from the device say so, and fields with no offline copy say that too, so an empty list is never mistaken for an answer.

Anything chosen offline records which version of the vocabulary produced it. A code is stored as text and nothing rejects one that has since been retired, so without that stamp a case coded from an old copy would be indistinguishable from a current one.

The application stops waiting to discover it is offline

Every save used to wait eight seconds before concluding the server was unreachable, and intraoperative writes are serialised per case — so one unreachable save held up everything behind it, and the next save paid the cost again. Saves now write straight to the queue after a failure and stop attempting the network briefly, resuming as soon as any request succeeds. Nothing is lost: an unsent save was always queued first.

Fewer redundant redraws during a case

The intraoperative screen refreshed itself every ten seconds whether or not anything had changed — rebuilding the whole timetable and re-rendering the screen to display an elapsed time that reads in whole minutes. It now redraws when the minute changes or the timetable actually advances.

A diagnostics screen

Settings → Diagnostics reports queued edits, whether the server is reachable, the vocabulary version on the device, and how long recent intraoperative tab switches took. Performance problems on a phone cannot be measured remotely; this turns an impression into a number.


8.3.3 - Blood pressure entry and slider handling

  • Dragging a vital slider no longer changes tab part-way through. The intraoperative tab swipe claimed any horizontal movement and the slider gave the gesture up, so the screen changed while a value was being set.
  • A three-digit blood pressure no longer truncates. Blood pressure sits in two columns, which leaves the value about 61 pixels between the two buttons — 130 mmHg needed 88. The unit now sits below the field, which is what actually created the room; reducing the type alone did not, at any legible size. Applied to every vital field, in preoperative assessment and recovery.

8.3.2 - Pediatric premedication

  • Premedication is dosed for the child. Nineteen drugs resolve from recorded weight and age, capped at the adult dose, with the arithmetic shown beside the number. Drugs that should not be given to a child are withheld with the reason; drugs with no pediatric rule ask for a hand-entered dose rather than offering the adult amount; a child with no recorded weight gets a prompt for one rather than a dose. See Pediatric mode.
  • Intranasal dexmedetomidine, 4 mcg/kg capped at 200 mcg, added to the premedication catalogue.
  • Premedication for a child was previously dosed as an adult on the web app, which took a drug list and a dose table with no clinical mode, weight or age. Both clients now use the same resolver.
  • A haematocrit reported as a fraction is converted to a percentage. Analysers commonly print 0.41, sometimes unlabelled and sometimes labelled % regardless; the latter previously passed through as though already canonical. This is confined to haematocrit, where the two scales cannot overlap — it is deliberately not applied to reticulocytes or eosinophils, which are normally below 1%.
  • A laboratory value that could not be converted is no longer labelled with the canonical unit, which had put a number and a unit on screen that did not belong together.
  • Pediatric drug and infusion menus regained their scenario categories.
  • Equipment suggestions are translated and no longer clipped.
  • Device preferences no longer follow one clinician into another's account on a shared phone or tablet.

Unreleased - Pediatric mode

  • Added an explicit Adult/Pediatric mode with exact age in days, months, or years. LOSPOR deliberately does not collect gestational or postmenstrual age; chronological age and recorded body size are the available dose context.
  • Added shared pediatric ASA/POVOC/COLDS, fasting, pain, vital-reference, BSA, maintenance-fluid, and resuscitation rules.
  • Added mode-specific complete clinical rulesets with immutable publication, copy provenance, and deterministic personal, institution, then platform precedence. Adult and Pediatric selections are independent and never fall back across modes.
  • Added the canonical Adult platform baseline (LOSPORADULTS Rules) from the existing drug, infusion, fluid, and equipment library. Web provides the editing workbench; mobile/PWA show the effective selections read-only.
  • Added canonical administration routes, dose components, concentration units, and internal UCUM codes shared by Core, API, web, PWA, and mobile.
  • Added pediatric web, PWA, Android, and Database Browser workflows, including exact chronological-age filters and bilingual labels.
  • Adult dose, rate, fluid, gas, volatile-agent, airway-equipment, ventilation, blood-volume, bleeding, and local-anaesthetic defaults are blocked in pediatric mode unless a reviewed pediatric profile exists.
  • Production remains disabled until the full clinical manifest is reviewed.

[7.3.0] - 2026-07-28

  • Finalization and clinical writes now serialize on one PostgreSQL case lock, preventing completed snapshots from racing with section or event saves.
  • Research export manifest v2 tracks parent, event, relational, and section revisions; research exports now accept finalized-only cohorts.
  • OMOP generation maps each page once, private working files are cleaned, and downloadable artifacts expire after 30 days while audit metadata remains.
  • Database Browser v0.3.0 shows artifact expiry and disables unavailable files.
  • The API release tag now starts the exact cross-repository release gate.

[7.2.1] - 2026-07-27

  • All six repositories now install with zero npm audit findings.
  • GitHub Actions use Node.js 24-compatible releases and run project checks on Node.js 24.
  • API, web, mobile/PWA, Database, and documentation carry traceable maintenance versions.

[7.2.0] - 2026-07-27

  • Research permissions now retain a separate institution scope for aggregate queries, case inspection, standard export, and OMOP export. A permission granted for one hospital cannot escape into another grant's scope.
  • Aggregate-only researchers no longer receive pseudonymous case rows. The Database hides and server-guards case and export surfaces independently.
  • One disclosure-control policy now protects query, comparison, benchmark, distribution, and quality results using valid observation denominators and both sides of binary rates. Protected totals are shown as ranges without leaking hidden numerator or denominator values.
  • Research exports are immutable background jobs. Creation freezes the cohort, action scope, source cutoff, source version, and a transactionally captured, hashed case-revision manifest. Source drift fails visibly instead of silently omitting cases. Completion stores one checksummed artifact in filesystem storage for local/self-hosted use or S3-compatible object storage for serverless deployments.
  • OMOP CSV exports are ZIP archives containing a manifest and one CSV per OMOP table. Existing pre-artifact export records are marked legacy and must be recreated rather than silently regenerated from current data.
  • API CI runs migrations and real PostgreSQL lock/governance integration tests. Browser coverage includes aggregate-only navigation, protected count ranges, authenticated API navigation, and immutable export job states.

[7.1.0] - 2026-07-27

  • Added a separate lospor-browser product for governed cohort building, comparisons, pseudonymous case review, data quality, benchmarking, saved cohorts, exports, and research access administration.
  • Added provider-independent research contracts to Core and a complete /v1/research/* API with explicit scope, small-cell suppression, auditing, export checksums, and OpenAPI coverage.
  • Added additive persistence for research access grants, saved cohorts, and export history.
  • Added the canonical Core clinical display registry with 950 clinician-reviewed English/Bulgarian terms shared by API, web, PWA/mobile, Database, and exports.

[7.0.1] - 2026-07-25

Reliability

  • Case-editing leases are acquired atomically in PostgreSQL, preventing two simultaneous clients from both being told that they own the same lock.
  • New-case drafts use IndexedDB in the PWA and the private filesystem on native mobile, preserving offline work across reloads.
  • OMOP exports over 5,000 matching cases fail before producing a partial research file and report the matching count and limit.
  • Personal data export is a streamed ZIP containing all account-owned records and exact counts instead of silently truncating cases or audit history.
  • Email-verification links return to the configured web application after the dedicated API verifies the token.
  • The generated OpenAPI contract explicitly describes every supported public API operation. Internal maintenance operations are retained only in a private inventory.
  • A cross-repository release gate verifies a real PostgreSQL migration, concurrent locking, exports, API and client builds, browser workflows, PWA offline recovery, and Android export before release tags are created.

[7.0.0] - 2026-07-25

Dedicated API service

  • Database, authentication, email, AI, PDF, audit, OMOP, maintenance jobs, migrations, and HTTP routes now live in the new lospor-api repository.
  • Web is a database-free browser interface. Mobile and PWA call the versioned /v1 API directly.
  • The old web /api/* address remains as a forwarding compatibility path for V6 clients during the 12-month transition.
  • API live/ready checks, capability discovery, request IDs, generated OpenAPI, and repository boundary checks were added.
  • Core now defines shared API/session/error capability contracts without becoming a server or gaining database/network code.
  • The API can run serverlessly today and produces a standalone Node build for future institution-hosted deployments.
  • Mobile/PWA clinical queues now use durable storage suitable for full case data. Intraoperative taps are stored before network debouncing, older queued work migrates automatically, and the dashboard recovers automatically when Wi-Fi returns.

See Application architecture, API, and Self-hosting.

[6.0.0] - 2026-07-24

One clinical rule set

  • Web, PWA, and mobile now use Core for case payloads, validation, readiness, finalization, timetable projection, active-item reconstruction, totals, and clinician-facing case stages.
  • The full clinical option catalog, offline fallback, laboratory rules, ICD-10 systems, ASA/risk bands, airway/monitoring decisions, Aldrete/handover rules, event descriptions, and timetable summaries now have one Core implementation.
  • Option caching, case locks, polling, revision/conflict headers, account policy, and typed search results share pure Core controllers while each app keeps its own storage and network adapters.
  • Option-library metadata is checked through strict shared readers. Invalid dose, route, range, weight-basis, and event metadata is no longer trusted by one client while another applies a different fallback.
  • Units, default monitoring, vital autofill, and intraoperative favourites follow the signed-in account. Exact offline edits are merged over the newest account settings when connectivity returns.
  • Favourites use stable option identities, so display-label or translation changes do not lose the saved choice.
  • Database/API ownership remains in the web repository. Core stays a pure TypeScript clinical library, and no database migration is required.
  • Privacy-rejected fields now remain visible locally with a specific reason. Safe sibling fields still save, unchanged rejected text is not retried in a loop, and editing the affected field retries it.
  • Uppercase Bulgarian ICD-10 labels selected from the coded catalogue no longer look like patient names to the privacy filter. Strong EGN, ID-number, date, and email checks remain active.
  • Web gas bars show every FGF and O2/Air or O2/N2O change at the correct five-minute column, use the same rounded edges as the other running bars, and describe gas events in readable language.
  • Intraoperative timelines now persist one exact start/end instant plus the case timezone. Timing saves are ordered before events, and legacy wall-clock values are no longer guessed into dates or allowed to create future vitals.
  • A dry-run anomaly report and separately guarded tombstone repair command are available for reviewing older shifted duplicate events.
  • Automated guards now reject copied catalogs, threshold functions, aliases, and hardcoded timetable intervals before they can reach a release.

See Application architecture for the ownership boundary and release order.

[5.6.1] - 2026-07-24

Shared records and stricter checks

  • Web and mobile now read the same Core definitions for case details, intraoperative events, and timetable data.
  • Old or malformed saved timetable rows are checked before display instead of being trusted as valid clinical data.
  • Mobile's automated lint check now fails on every warning, including warnings hidden by an inline suppression comment.
  • The intraoperative mobile screen keeps unchanged tab and sheet inputs stable to avoid unnecessary rerendering.
  • No database migration is required.

See Application architecture for repository ownership and the shared-contract boundary.

[5.6.0] - 2026-07-23

Autosave Manager

  • Mobile, PWA, and web now use one save coordinator. Every change is recorded locally before the app tries to send it.
  • Saves for one case are sent in order. If another device changed the same section first, the app adopts the new revision and retries the small changed fields once.
  • Timetable additions, edits, and deletions are separate durable operations. The apps no longer replace the whole timeline to remove or edit one item.
  • Reopening a case restores unsent work over the latest server copy.
  • A case cannot be finalized while changes are still waiting to sync.
  • The database adds monotonic revision counters for preop, intraop, and postop.

See How autosave works for the recovery rules.

[5.5.1] - 2026-07-23

Auto-fill vitals is now canonical across the web app and mobile app.

Fixed

  • Turning off Auto-fill vitals also turns off the BP/HR carry-forward and backfill-on-reopen child options, so stale hidden settings cannot reactivate later.
  • Background backfill no longer runs unless the master auto-fill option is on.
  • Missed 5-minute columns after a browser/app resume are planned consistently through the shared core helper.

Release

  • Web, mobile, core, and docs are aligned on the common 5.5.1 release line.

[5.5.0] - 2026-07-23

The release line is aligned across the web app, mobile app, core package, and documentation. The current mobile live-refresh docs now describe the production version-polling path rather than the removed SSE stream.


[5.4.2] - 2026-07-22

A small fix on top of 5.4.1: whole-number weights could not be entered. Raising the weight minimum to half a kilogram in the previous release, without also adjusting the step, meant the field would only accept half-kilo values — so a round number like 105 kg was refused, offering 104.5 and 105.5 as the nearest options. Weight now accepts both whole and half kilograms.


[5.4.1] - 2026-07-22

A correctness release, prompted by a report from the ward. One of the faults could destroy a complete preoperative assessment, so it is worth reading even if you skip the rest.

Fixed

  • A single out-of-range value could lose an entire preoperative assessment. On the web app the height slider could be dragged below 30 cm, which is the lowest the record accepts. Saving a new case was all-or-nothing, so that one value made the whole save fail — and because the case had never been created, there was no draft to return to. Going back to the dashboard lost everything that had been typed. Creating a case now behaves the way editing one already did: the value it cannot accept is set aside and named, and everything else is kept.
  • Four measurements offered values the record refuses. Systolic pressure could be set as low as 1 when the minimum accepted is 40; diastolic 1 against 20; heart rate 1 against 10; and temperature 0 against 25 °C. Dragging any of those sliders to the bottom produced a reading that silently would not save. Every control is now limited to what the record actually accepts, on both the web app and the phone.
  • A refused value now says so, where you typed it. Previously it was dropped in silence and the form went on looking saved. The field is now outlined and states what it will accept — "must be 30–250 cm" rather than "invalid" — and the message stays until the value is corrected, so it cannot be missed by moving on quickly.

A note on how this was found

The four measurement faults above were not in the original report. They surfaced when the picker limits were checked automatically against what the record accepts — a check that now runs on every change, so the two cannot drift apart again unnoticed.


[5.4.0] - 2026-07-21

Start and end times are now recorded as real moments in time, with the timezone they were entered in. This matters beyond the two visible faults it fixes: a time that cannot be placed on a real timeline cannot be compared between cases, between sites, or against anything else recorded during the anaesthetic — and the register's research value rests on exactly that.

Fixed

  • The start time could lock itself to 00:00 with no way to correct it. On the web app: open a case, reach the intraoperative screen, change anything at all — ticking a monitor was enough — then leave and come back, and the start time showed a locked "00:00". The record had no way to say "not started yet", so the first save wrote midnight as a stand-in; every later check then read that as a genuine start. Simply recording one entry on the chart could do the same. A case that has not been started is now genuinely blank, and the field stays editable until you set a time.
  • The chart could begin at the wrong time, and the previous fix for it was never working here. Start times were stored as a bare clock reading — "08:00", with nothing to say where in the world that was — while everything charted against them is an absolute moment. Combining the two put the start of the chart out by the local time difference from UTC: three hours in Bulgaria in summer. The correction released in 5.3.0 measured that disagreement, judged the record inconsistent, and silently reverted to the old behaviour every single time. It would only ever have worked in a country on UTC. Both apps now record the timezone alongside the time, so the chart begins where you said the case began.
  • A case running across a daylight-saving change reported the wrong length, because the duration was taken from the clock face rather than from time actually elapsed. In October that hid an hour; in March it invented one.
  • Finishing a case is now genuinely blocked when no start time was recorded — that safeguard existed but could never trigger.
  • The research export no longer records a placeholder date as the day of surgery.

A note on existing records

Cases recorded before this release are left exactly as they are. Their timezone was never stored, so converting them would mean guessing — and a guessed timestamp is worse than an acknowledged gap, because afterwards it is indistinguishable from a real one. They keep their current behaviour and remain clearly identifiable as predating the change.


[5.3.0] - 2026-07-21

An external review of the code produced around twenty-five findings; every one that was checked proved genuine, and they are fixed here. Alongside that, the intraoperative chart on the web app was brought back into line with the phone, which had quietly become the better-behaved of the two.

Fixed

  • Handing a case to a colleague could fail with an error. Case numbers are counted per person and everyone's begin at 0001, so if the colleague receiving the case already had that number, the transfer was refused outright. The case is now renumbered into their sequence when — and only when — the number is genuinely taken, and the number it arrived with is kept on record so a printed copy can still be traced.
  • A deleted account kept working until its sign-in expired. Deletion now takes effect immediately, and a change of role applies on the next action rather than the next sign-in.
  • The chart could begin at a different time depending on which device you opened it on. If you started a case at 08:25 but entered a start time of 08:00, the web app drew the chart from 08:00 and the phone from 08:25. Nobody charts at the moment of induction — there is a patient to attend to — so the time you enter is now the beginning of the chart everywhere. Existing records can be realigned.
  • Changes made on one device did not appear on the other. The live-update mechanism could not function on the hosting platform at all, and the web app had no fallback, so it had simply never worked in production.
  • A typing slip in a number field quietly erased the value. Entering something that is not a number stored an empty value and said nothing. Such entries are now refused and named, like any other value out of range.
  • Rows on the intraoperative chart showed internal labels such as intraop.timetable.drugs in place of "Drugs", because the translations were missing entirely.
  • The welcome tour no longer appears on top of the settings window.

Changed

  • The drug, infusion, fluid, gas and agent menus on the web app now match the phone exactly — the same eight clinical categories and the same favourites, rather than the flat search list the web app had drifted into. Adding a fluid or starting an agent on the web used to record the first suggested amount immediately, without showing it to you; both now ask you to confirm the dose, as the phone always has. The menus are now defined once and shared, so they cannot drift apart again.
  • Typing in a drug by hand on the web chart has been removed. It was never intended to be there, and it recorded names that no shared drug library could recognise — which is precisely the kind of entry that cannot be analysed later.
  • Sex is recorded as unknown when it was not asked, instead of quietly defaulting. "Not recorded" and "recorded as other" are different facts, and treating them as one distorts any figure calculated from the register.
  • Favourite drugs and infusions can be edited from the web app as well as the phone.
  • Accounts are permanently anonymised 30 days after deletion is requested. Audit records outlive the account deliberately — they are the evidence that the account existed and what it did.

[5.2.1] - 2026-07-21

A correctness release. Two saving faults were found and fixed, one of which could lose data you had already entered.

Fixed

  • Saving no longer clears fields you did not touch. LOSPOR saves only what changed, but the server was treating any field left out of a save as though you had deliberately emptied it. Editing one value in the preoperative or postoperative form could therefore blank others in the same section. If a case looks like it has lost a height, weight or age that you know you entered, this is why.
  • Autosave no longer fails while you are still typing a number. Entering a height sent the value mid-entry; the server refused anything below 30 cm and rejected the whole save, so every other field you had just edited was discarded with it. Values that are still out of range are now set aside on their own, everything else saves normally, and the form tells you which value was not accepted instead of leaving it on screen looking stored.
  • The height, weight and age pickers now offer only values the record can actually accept.
  • On the live intraoperative chart, entering a start time later in the day is no longer read as "this case started yesterday" — which used to march the now-marker forward, grow the chart by an hour every few seconds, and could fill the record with observations that were never taken.

Changed

  • The research export now includes the two tables standard OMOP tools require (PERSON and OBSERVATION_PERIOD). Exports could previously not be loaded directly into ATLAS or ACHILLES without building them by hand. Year of birth is derived from age at operation, with month and day left explicitly unknown rather than invented; race and ethnicity are marked as not collected.
  • The pseudonymous person identifier is now generated as the export manifest always described it (SHA-256), and is wide enough that two unrelated cases cannot be assigned the same identifier as the register grows. The manifest also now states plainly that one person is recorded per case, so the same patient across two operations appears as two people — an intended consequence of storing no patient identifier, and a limitation to declare in any study protocol.
  • The export's data-contract version moves to 3.5.0 to signal the new tables to anyone consuming earlier exports.
  • The mobile sign-in screens show the full LOSPOR mark, and chart labels are translated in Bulgarian.

[5.2.0] - 2026-07-20

The case summary now looks and reads the same on mobile, on the web, and on the printed protocol — all three are built from one shared model — and the printable A4 protocol has been redesigned around the intraoperative timetable.

Added

  • A completely redesigned printable anaesthesia record: a clean A4 paper layout where the vitals graph, the numeric vitals table, and every treatment lane (agent, infusion, gas/FGF, fluids, patient position) line up on the same time columns, with clinical events flagged on the chart and every dose shown as a numbered pin (① ② ③ …) at its exact administration time, resolved in a drug administration log (time · drug · dose · totals) — like a classic paper record.
  • Long cases chart like paper records do: up to ~5 hours is one full-height chart; longer cases continue onto a second half-height chart on the same page at the same time scale — nothing repeats and nothing gets squeezed. The record stays exactly two A4 pages for cases up to ~24 hours.
  • A "Print case" flow: the summary page is now a clean review of the case; on the web, printing lives on a dedicated print page for finished cases — offered automatically when you close a case, and available from a Print case button. The print page always shows the record as white paper, whatever your theme, and can hand you the finished A4 PDF built on the server.
  • Printing on a phone never leaves the app: Print case (long-press a finished case, or the print action on the case screen) downloads the ready-made A4 PDF in the background — showing a "Generating PDF…" state — and then opens your phone's normal share sheet, so you can view it in your PDF app, save it, send it, or print it. No browser, no print dialog, no web app on your phone.
  • A read-only timetable viewer for finished cases: tapping the summary timetable on a closed case opens the printed record's chart in the app — traces, event flags, numbered drug pins, the vitals table and all lanes, with the drug administration log below. Cases still in progress open the live intraoperative screen as before.
  • Pinch-to-zoom on that viewer: pinch, or use the − / + buttons, to move between detail levels. Zoomed in you see every 5-minute reading; zoomed out the vitals table thins to the coarser printed sampling (a badge shows the current interval). The graph traces, drugs, events and lanes always show every recorded point at every zoom level — zooming never hides data, and the printed record is unaffected.
  • Patient position changes can now be logged with a time (web intraop event picker) and appear as a Position lane on the printed record.
  • The case summary and its timetable now follow your theme — dark in dark mode, paper-light in light mode — while the printed record and PDF deliberately stay white. The record's own labels are fully translated into Bulgarian, and the PDF is generated in the language you are using.

Fixed

  • Heads of department and administrators can now print a case they do not personally own. Previously the print page and PDF returned "Not found" for them.

[5.1.0] - 2026-07-13

Hardening release following an external code review of v5.0.0.

Added

  • Adding intraoperative entries (drugs, fluids, vitals, events) now works offline on the web app too — they are kept in the browser and sent automatically on reconnect. Removing or editing existing timeline items still needs a connection.
  • Resetting your password now signs you out everywhere: existing web sessions and mobile logins stop working within a few minutes of the reset.
  • Signing out on a shared computer warns you if unsynced saves exist and removes them from the device, so they can never mix with another user's session.

Fixed

  • Adding a drug, fluid, vital, or event to a case on the web no longer occasionally shows a false "edit conflict" prompt (or makes autosave appear to fight itself) when you are the only person editing.
  • Vitals edited on the web timetable now keep a single stored identity per time column — re-editing a value replaces it cleanly instead of occasionally showing an older value after a refresh.
  • Offline saves that conflicted with a newer edit no longer get stuck retrying forever — they merge automatically, field by field.
  • A long-standing storage overlap that could make a queued offline save and queued offline events overwrite each other has been eliminated (existing queued data migrates automatically).

[5.0.0] - 2026-07-12

Added

  • The web app can now save without a connection: saves are kept locally in the browser and sync automatically when the connection returns, with a clear "saved locally" indicator, a global "saves waiting" badge in the header, and a discard control in privacy settings.
  • Mobile settings gain an "Unsaved events" screen: intraop events the server rejected are kept on-device and can now be reviewed instead of sitting invisible.

Fixed

  • Vitals typed into the web intraop timetable could silently disappear from the stored chart once the case had other logged events. Web vitals are now stored the same robust way mobile stores them.

Changed

  • Saving, offline queues, and conflict handling across the web and mobile apps now run on one shared, tested engine — the same defenses everywhere, so a fix reaches both apps at once.
  • Preop and postop saves are field-level: only what you changed is sent, so two clinicians editing different parts of the same case no longer overwrite each other.
  • Editing the same case in two browser tabs no longer risks silently overwriting your own newer changes.
  • Taps on toggles and pills save near-instantly; retries back off politely while the connection is down and resume immediately when it returns.

[4.1.6] - 2026-07-11

Fixed

  • Rapid intraop entries no longer race or overwrite each other — vitals, drugs, and events entered in quick succession now save one at a time and all persist, instead of colliding under fast documentation.
  • An intraop save that got stuck as "failed" (needing a manual "Sync retry") after the case was also edited on the web now recovers automatically.

Changed

  • Version alignment to 4.1.6 across all four LOSPOR repos. This release's fix is mobile-only; the web app and API are unchanged.

[4.1.5] - 2026-07-05

Fixed

  • Mobile/PWA preop section-overview floating button now uses a real icon instead of corrupted text.
  • Version alignment to 4.1.5 across all four LOSPOR repos.

[4.1.4] - 2026-07-05

Fixed

  • Airway devices with sub-options (LMA, oral/nasal ETT, double-lumen tube, endobronchial tube) can be re-edited again — reopening one now clears its options so you re-pick from scratch, instead of getting stuck open.
  • Changing an infusion's rate now applies from the point you change it onward, instead of retroactively for the whole case.
  • Adding vitals no longer intermittently fails and requires a manual "Sync retry".

Changed

  • Adding or ending a fluid now saves once instead of twice — the running fluid totals are calculated on the server from the fluids you record, so the app no longer sends a separate, redundant total each time.

[4.1.3] - 2026-07-05

Version alignment across all LOSPOR repos — no functional changes beyond v4.1.2.


[4.1.2] - 2026-07-05

Fixed

  • Resolved a production incident where an exhausted database connection pool was causing widespread slowness and errors across the app.
  • Removing an event from the intraop log no longer produces an error.
  • Stopping an infusion, agent, or fluid, and changing an infusion or agent's rate, no longer feels stuck or unresponsive.
  • Ending a case with multiple active infusions, agents, or fluids finishes faster.
  • Changing a volatile agent's percentage no longer shortens its bar on the intraop timeline.
  • Adding vitals now closes the entry screen immediately, matching how adding a drug, infusion, or fluid already worked.

[4.1.1] - 2026-07-05

Fixed

  • Airway device, vascular access, and premedication selections in the mobile intraop screen no longer flicker and revert after tapping.
  • Rapid drug/event entry in the mobile intraop timetable no longer silently drops an item under fast back-to-back taps.
  • Preop data (age, height, weight, diagnosis, comorbidities, etc.) is no longer silently lost when closing and reopening a case shortly after editing.
  • Account deletion wording corrected on both apps' Bulgarian text to accurately describe what happens (access disabled immediately, further deletion/anonymisation per retention policy) instead of overstating it.
  • Cleaned up remaining text-encoding corruption in the mobile app.

[4.1.0] - 2026-07-05

Added

  • Full Bulgarian translation pass across both apps — dashboard, preop, intraop, postop, case detail, settings, admin, and the printed anaesthesia protocol now display correctly in Bulgarian.
  • Bulgarian Privacy Policy and Terms of Service pages.
  • The complications picker on web now shows Bulgarian category titles for the first time, and shares its complication list with the mobile app so both stay in sync going forward.

Fixed

  • Email addresses are case-insensitive everywhere — registration, login, password reset, and verification emails now treat Doctor@example.com and doctor@example.com as the same address. If you previously had trouble logging in due to capitalization, it is resolved.
  • Added missing Bulgarian translations for the intraop auto-fill "Backfill on reopen" setting.
  • Documentation refreshed (removed stale version references).

[4.0.0] - 2026-07-03

Added

  • Email verification — After registering, you receive a verification email (valid 24 hours). Click the link and you can sign in right away; waiting for admin approval is no longer required.
  • Password reset — Use Forgot password? on the login page (web, PWA, and mobile app) to receive a reset link valid for 1 hour.

Changed

  • Faster, more reliable intraop timetable — Tab switching and timetable scrolling are noticeably faster, button press feedback is back, and quick sequences like adding an infusion and immediately undoing it no longer show a brief "Sync error".
  • Privacy Policy and Terms updated to v4.0 — Brevo (EU) is listed as the email delivery sub-processor and account-email processing is described; the account section reflects email verification.

Under the hood

  • The mobile intraop and preop screens were restructured into small, tested modules (mobile test suite grew from 95 to 228 tests; web from 133 to 147), continuous integration now runs on every change, shared clinical logic moved into a common @lospor/core package, and case finalization now refuses to complete if research-grade data mirroring fails.

[3.4.7] - 2026-06-28

Fixed

  • Mobile — Drug autofill and rounding in main drug sheet — The main "Add drug" sheet (DrugSheet) now pre-fills the dose from the option library's doseCalc profile (IBW-based weight calculation for induction agents, flat doses for others) and rounds the confirmed dose to the library's roundTo increment (e.g. Propofol rounds to the nearest 10 mg). Previously only the inline timetable-column picker had this logic; the refactored DrugSheet path had none. Patient height, weight, and sex are now forwarded from preop data into the dose calculation.
  • Mobile — No "Sync failed" after adding a timetable event — Adding a timetable event (drug, fluid, vital) could produce a spurious "Sync failed" badge because POST /events updates intraop.updatedAt via rebuildProjection, and a concurrent fluid-totals PATCH with the old baseline timestamp was then rejected with a 409. The mobile now retries the PATCH exactly once using the server's current timestamp returned in the 409 response body, silently resolving the conflict without user intervention.
  • Mobile — Timetable shows events on reopen when timestamps are in the past — If timetable events were saved with backdated timestamps (or the case is reviewed hours after the events occurred), the timetable viewport auto-scrolled to the current time and left the events off-screen. The auto-scroll now detects when events are more than 30 minutes before the current time and scrolls to show the events instead of showing an empty recent area.

[3.4.6] - 2026-06-28

Fixed

  • Mobile — No "Sync failed" on case reopen — Reopening an intraop case on mobile (or the Expo PWA) no longer shows a "Sync failed" badge and no longer triggers a spurious 409 in the API logs. The fluid totals aggregation effect was firing immediately on component mount — before the case data loaded — with an empty timetable and no conflict timestamp, causing the server to reject the request. The effect is now guarded by the caseLoaded flag (consistent with all other autosave effects in the same screen) and skips its first post-load fire to avoid writing data that was just read from the database.

[3.4.5] - 2026-06-28

Fixed

  • PII validation error display — Entering a patient name or other identifiable text in a clinical text field now shows the specific rejection reason from the server (e.g. "Possible name detected in field: intraopComplications") instead of a generic "Auto-save failed" or "Saved locally" message. This applies to the new-case form, the intraop form, and the postop form on both web and mobile.
  • Mobile — Drug autofill — Selecting a drug in the intraop timetable now pre-fills the dose input with the library-suggested dose (weight-based or flat, per the option library doseCalc profile). Previously the dose field was always empty on mobile; the calculation was only working on the web app.
  • Mobile — Drug category landing — Re-opening the drug picker after an addition now shows the top-level clinical category grid (Induction, Opioids, Relaxants, etc.) instead of jumping straight into the previously-selected subcategory's drug list.
  • Intraop — Agent and fluid delete buttons — The × button on anaesthetic agent segments and fluid rows was invisible on touch devices (PWA, tablet, phone browser) because it was hidden behind a CSS hover rule that does not fire on touch screens. It is now always visible on touch devices.

[3.4.4] - 2026-06-28

Fixed

  • Intraop — Finalize case — "Close Now" in the case summary now shows a readable message when the case cannot be finalized (e.g. "No anaesthesia technique recorded", "Aldrete score missing"). Previously the button appeared to do nothing on an incomplete case.
  • Intraop — Event log delete — The × button in the event log is now visible on touch screens (PWA) and correctly removes the corresponding fluid or infusion bar from the timetable when clicked. Previously only the log entry was removed while the visual bar remained in the chart.
  • Intraop — Fluid add — Clicking a fluid name in the picker now adds it immediately with the library default volume and closes the menu. Previously a separate dose confirmation panel opened which was easy to miss on touch, causing users to tap the same fluid multiple times and add duplicates.
  • Intraop — Agent switching — Switching anaesthetic agents (e.g. sevoflurane → desflurane) now clips the outgoing agent bar at the switch point and starts the new agent from there. Previously the old agent bar was deleted entirely. Switching is now a single tap — the agent starts with the library default concentration immediately.
  • Intraop — Drug chip — Clicking an existing drug chip now opens the drug picker so another drug can be added at the same time column. Previously the click had no visible effect.
  • Intraop — Drug dose rounding — Bolus doses are now rounded to the library-configured step (e.g. propofol rounds to multiples of 10 mg) even when the pre-filled dose is manually adjusted before confirming. Previously manual adjustments bypassed rounding.
  • Mobile — Autosave race condition — Selecting a technique, position, or monitoring option on the mobile intraop screen no longer reverts if an autosave was in flight at the moment of selection. The mobile postop form no longer resets to server values immediately after a save completes.

[3.4.3] - 2026-06-28

Changed

  • Admin export wording — The OMOP export card now reads "pseudonymised case-level hashes" instead of "anonymous hashes", consistent with the GDPR wording used throughout the application and privacy documentation.

Fixed

  • OMOP export quality gate — Exports now block (HTTP 422) if the batch contains non-finalized cases, cases with missing finalization snapshots, cases edited after finalization, or impossible intraop timestamps (end before start). Administrators can override with ?force=true; the override is recorded in the export manifest. The severity of two existing checks was also upgraded: NO_FIELD_STATUS_ROWS from warning to error, REDACTED_FREE_TEXT_PRESENT from info to warning.
  • Documentation encoding — Encoding artefacts (вЂ, в†) caused by copy-pasted smart quotes have been corrected throughout the documentation.

[3.4.2] - 2026-06-28

Added

  • Data dictionary — Research documentation now includes a field-level data dictionary covering all clinical fields, their types, units, valid ranges, and research notes.
  • OMOP quality warnings — Four new error-level quality checks: non-finalized cases in the export batch, missing finalization snapshots, relational drift (case edited after finalization), and impossible intraop timestamps.

[3.4.1] - 2026-06-28

Fixed

  • AI Advisor — The AI risk advisory button now flushes any pending autosave before showing the consent prompt. Previously, accepting consent before the autosave completed caused the aiOptIn flag to be overwritten back to null by the in-flight save, requiring the button to be pressed twice.
  • Serverless stability — Audit log writes and relational sync calls no longer risk truncating the Vercel function response. These background calls now run after the HTTP response is sent using after().

[3.4.0] - 2026-06-28

Security

  • AI trust boundary — Mistral AI requests are now proxied server-side. The API key is no longer present in the browser bundle. All AI features (lab scan, vitals scan, AI advisor) require an authenticated session on the server.
  • CORS hardening — CORS policy is now configured and enforced in one place, eliminating minor inconsistencies between API routes.

Added

  • Mobile idempotency — Creating a case on mobile while offline and retrying on reconnect no longer creates a duplicate. The server matches the local draft identifier and returns the existing case.

[3.3.1] - 2026-06-28

Fixed

  • PWA auth redirect loop — A minimal service worker clears stale cached redirects that sent some PWA users to the login page even when already authenticated.
  • The PWA manifest and service worker are now exempted from the authentication middleware so they can always be fetched without a session.
  • Navigation buttons now show press-down feedback on touch screens.

[3.3.0] - 2026-06-27

Added

  • Case review bar — A compact status bar at the top of the case detail page shows completion of preop, intraop, and postop at a glance with direct links to each section.

Fixed

  • Allergy and medication lists — Drug names in the allergy and current medications lists were being corrupted to [object Object] on save. Fixed.

Security

  • Authentication middleware and session handling hardening pass.
  • Corrected 401/404 response behaviour for unauthenticated routes.

[3.2.1] - 2026-06-27

Fixed

  • Background relational sync (the research-facing mirror of diagnoses, procedures, labs, medications, complications, etc.) was silently failing after every case save with a P2028 timeout. The fix is the same as the v3.2.0 intraop fix: all database writes that used an interactive transaction have been converted to sequential writes. Case data was never affected — the JSON columns are always authoritative. Affects all three save surfaces: preop, intraop, and postop.
  • Preop data entry occasionally showed a conflict dialog immediately after opening a case. This was a client-side race: the URL contained the case ID before the case data finished loading, so an autosave could fire before the conflict-timestamp reference was initialised. The client now silently recovers by adopting the server's current version on first contact and retrying without user intervention.

[3.2.0] - 2026-06-27

Added

  • Strict case finalization — finalizing a case now validates that all three sections are present and clinically coherent: preop must exist; intraop must be started with at least one anaesthesia technique; postop must include at least one Aldrete subscore and a patient disposition (Ward / PACU / ICU). An incomplete case returns a clear message explaining what is missing.
  • Offline case creation deduplication — if the mobile app saves a new case while offline and the network drops before the server response arrives, retrying no longer creates a duplicate. The server recognises the local draft identifier and returns the existing case.

Changed

  • Clinical numeric ranges — age, height, weight, blood pressure, heart rate, SpO2, temperature, respiratory rate, pain score, and Aldrete total now enforce clinically plausible ranges. Nonsense inputs (e.g. age 200, SpO2 101) are rejected at the API instead of being stored silently.
  • CORS production guard — the server now requires CORS_ALLOW_ORIGIN to be set explicitly in production. Previously it could silently fall back to * (allow any origin) if the environment variable was missing.

Fixed

  • Selecting a drug from the allergy or current medications list now saves correctly. Multi-word drug names (e.g. "Morphine Sulfate") were being blocked by the server PII filter, which mistook two capitalised words for a patient name. Drug catalogue fields now skip the name check while still being checked for EGN, ID numbers, dates, and email addresses.
  • Saving intraoperative events or case data no longer returns a 500 error under load. The case save handler was using a database transaction that is incompatible with Supabase's connection pooler, causing timeouts (P2028). Writes now run sequentially; conflict detection is unchanged.

[3.1.0-hotfix] - 2026-06-27

Fixed

  • PWA and mobile login was returning 403 after the v3.1.0 CSRF hardening. The server was incorrectly blocking cross-origin requests to the login endpoint. Fixed the same day.

[3.1.0] - 2026-06-25

Security and privacy hardening

  • Web API writes that use cookie authentication now require same-origin Origin/Referer validation; bearer-token mobile/PWA calls remain supported.
  • Clinical PII validation is field-aware for event rows, so controlled clinical labels are not blocked by the name heuristic while free-text notes remain protected.
  • AI lab-reading upload limits now check the actual parsed base64 payload.
  • Login flows no longer query pending-account state after a failed sign-in attempt, and the legacy pending-check endpoint returns a generic response.
  • Account deletion wording now reflects the implemented behavior: immediate access disable and token revocation, with further deletion/anonymisation handled by retention policy.
  • PWA documentation now calls out the weaker browser localStorage storage model and logout cache clearing.
  • Deployment examples use pwa.lospor.org for the mobile PWA.
  • Mistral requests for lab scan, vitals scan, and AI advisor now retry against the global API base if a configured regional endpoint rejects inference with regional_inference_not_allowed (code: 1914).
  • AI privacy wording now refers to the configured AI provider rather than promising a fixed regional inference path.
  • Mobile/PWA bolus drug and infusion pickers now use scenario-based cockpit menus with synced favourite drugs/infusions in user preferences.
  • Route-specific drug profiles are respected on mobile/PWA, including lidocaine dose mode for IV and concentration/volume mode for regional routes.

[3.0.0] - 2026-06-25

Why this is v3.0

This release is larger than the planned 2.3 line. It changes the canonical database/API contract, aligns mobile and web around one backend schema, introduces shared clinical libraries, moves intraoperative charting to append-only events, and adds research-grade export/provenance tooling.

Canonical app contract

  • lospor-app is the canonical database and API owner.
  • lospor-mobile no longer behaves as a separate schema; mobile payloads are mapped to web/API field names before persistence.
  • Case save conflict detection, offline queues, live refresh, and shared case access behavior were tightened so data can move between web, native mobile, and PWA without silent overwrites.

Shared libraries

  • The new OptionLibrary powers web/mobile/PWA pickers for techniques, airway, ventilation, monitoring, positions, premedication, drugs, infusions, agents, fluids, events, disposition, handover, demographic pickers, and numeric ranges.
  • The option library is seeded from structured source files and has a bundled/cached fallback snapshot so first-load or offline devices do not show empty clinical pickers silently.
  • The canonical lab catalogue, canonical units, LOINC codes, and normal ranges are shared by both apps. AI lab scan is asked to search the whole canonical catalogue and only imports recognised tests.

Database and research model

  • Normalized research rows now mirror the clinical JSON/cache data: diagnoses, procedures, comorbidities, labs, medications/allergies, vascular access, premedication, complications, selections, and event timeline data.
  • ICD-10 stores English and Bulgarian labels for the same code. The labels are display/search metadata, not duplicate clinical concepts.
  • ConceptMap stores source vocabulary/code/labels and OMOP concept IDs where confidently known. Source-only values are explicit and never represented by fake OMOP IDs.
  • ClinicalFieldStatus records key-field missingness/provenance so blank values are not interpreted as negative findings.

Intraoperative timetable

  • Web and mobile now use the same append-only CaseEvent event log for drugs, infusions, fluids, agents, gas settings, vitals, glucose, and clinical events.
  • Fresh gas flow is stored over time with FGF, carrier gas, FiO2, calculated FiAir, and calculated FiN2O. FiO2 cannot go below 21%; O2-only is FiO2 100%.
  • Running infusions, fluids, agents, and gas settings extend visually when reopening an active case.

Export and governance

  • v3.0 adds local Athena/OMOP vocabulary import tables and an import script for full vocabulary-backed concept resolution.
  • ConceptMap now records mapping method, confidence, review state, mapping notes, and Athena vocabulary version.
  • OMOP export reads normalized rows and active event rows, includes provenance/version metadata, preserves source codes/labels, and stores known OMOP concept IDs only where mapped confidently.
  • Export bundles include table counts, mapping summary, de-identification metadata, and quality warnings. App exports warn rather than block.
  • Free-text is redacted before AI advisor/export paths; coded values are preserved.
  • Case snapshots and OMOP export metadata now use 3.0.0.
  • Data should be described as de-identified / pseudonymised, not fully anonymised, because internal user, institution, audit, and timestamp linkage exists.

Verification

  • Web and mobile have deployment checks for typecheck, lint, and tests.
  • Mobile now has baseline ESLint/Vitest tooling and component/clinical utility tests.

[2.3.0] — 2026-06-20

  • Shared option library. Every intraop/preop pill-button option (position, technique, vascular access, airway management, monitoring, premedication drugs, intraop drugs, infusions, inhalational agents, fluids, clinical events) now comes from one shared catalogue instead of being hardcoded separately in each app. This fixed a real drift where mobile and web sometimes disagreed on technique codes for the same clinical technique, and where mobile's own screens disagreed with each other on drug/infusion/fluid lists.
  • Separate Infusions entry point (web). Starting an infusion on web no longer requires picking a drug and then choosing "Bolus" vs "Infusion" — there's now a dedicated Infusions row, matching how the mobile app has always separated Drug/Infusion/Fluid/Agent entry.
  • Web intraop events now persist the same way mobile's do. Bolus drugs, infusion start/rate-change/stop, agent start/stop, fluid start/stop, and clinical events on web now write to the same append-only event log mobile already used, instead of only a JSON snapshot.
  • Still-running infusions/fluids/agents now display correctly after reopening a case. Previously, a case closed mid-infusion and reopened later would show the bar frozen at wherever it was when you left; the timetable now extends active bars client-side using the user's local wall clock, the same way it already updated live while editing.
  • Security. Centralized role-authorization across admin/export endpoints; OMOP export and the AI advisor's data path now redact free-text fields that could carry identifying information.
  • Option lists no longer go blank if a device can't reach the server. Both web and mobile now fall back to a snapshot of the option library bundled into the app itself if a device has never connected successfully and has no cached copy either — e.g. a tablet's very first launch with no signal. A small banner appears whenever any picker is showing this offline/cached data instead of the live list, and it switches back automatically the moment a connection is available again — nothing is ever shown without you being able to tell whether it's current.
  • Mobile quality gates. The mobile app now has baseline npm run lint, npm run typecheck, and npm run test scripts, plus starter Vitest coverage for pure clinical utility logic and React Native component behavior.
  • Self-hosting: added a required post-migration seeding step for the new option library (see self-hosting guide).
  • Migration note: environments that only have the original option-library enum must apply the additive LibraryCategory enum migration before seeding the expanded preop/postop categories. If a live database was manually drifted, check _prisma_migrations before deploy.

[2.1.1] — 2026-06-19

  • Release hardening. Aligned web, PWA, and mobile metadata to v2.1.1.
  • Access control. HEAD_OF_DEPT users without an institution now fall back to their own cases only; they no longer match other null-institution users.
  • PII protection. The backend now uses a central clinical free-text PII gate across preop, intraop, postop, and event-save paths.
  • CORS. Production deployments now require an explicit CORS_ALLOW_ORIGIN; Vercel production no longer silently falls back to *.
  • Bulgarian ICD-10. Diagnosis and comorbidity search now stores stable ICD-10 codes with English/Bulgarian label snapshots and displays labelBg in Bulgarian UI.
  • Mobile privacy. Mobile/PWA settings include a clear local clinical cache action for offline drafts and queued saves.
  • Wording. Documentation now uses "de-identified/pseudonymised" and describes the OMOP export as partial/OMOP-inspired until full concept mapping is complete.

[2.1.0] — 2026-06-19

Added

  • Institution ID on cases. New cases now store the creating user's institution directly on the case record, improving research attribution and eliminating re-attribution risk if a case is later transferred to a user from a different institution.
  • Drug ID linkage. Intraoperative drug events now resolve the Drug catalogue entry (by ATC code) and store drugId on the event row, enabling precise drug record linkage beyond ATC code string matching.
  • OMOP export: drug events from event log. Drug exposure in the OMOP CDM export now reads from the CaseEvent table (type=drug, status=active) — the canonical append-only event log — instead of parsing the legacy keyEvents JSON blob. ATC codes appear in drug_source_concept_id.
  • OMOP export: lab results. Lab measurements are now included in the OMOP measurement table using LOINC-coded, canonical-unit rows from the LabResult table. Previously lab results were absent from the OMOP export.
  • OMOP export: institution care site. care_site_source_value in visit_occurrence now uses the case-level institutionId (populated from v2.1+) with fallback to the user's institution name.
  • Bulgarian diagnosis and comorbidity search. Searching for diagnoses or comorbidities in the Bulgarian-language version of the web and mobile app now correctly queries labelBg (Bulgarian ICD-10 labels) in addition to labelEn. Previously, Cyrillic queries returned no results because the locale parameter was not forwarded to the search API from the comorbidities field (web) or either field (mobile).

Changed

  • OMOP export source_version updated from 1.0.0 to 2.1.0.

[2.0.1] — 2026-06-19

Fixed

  • CORS preflight now accepts all intraop sync headers. The x-lospor-intraop-updated-at and x-lospor-force-update headers were missing from the Access-Control-Allow-Headers list, causing mobile conflict-detection saves to fail with a CORS error.
  • Plain-text medication sync. Preoperative medication lists entered as comma- or newline-separated text on mobile are now parsed correctly instead of being silently dropped.
  • ATC codes on intraoperative drug events. The event writer now persists atcCode and drugRoute to the CaseEvent table; previously these columns were populated in the schema but never written.
  • Search index performance. Added pg_trgm GIN indexes on ICD-10 labels, synonyms, and drug names so diagnosis and drug searches use index scans instead of full table scans across 100k+ rows.

Internal

  • Vocabulary seed script switched to bulk INSERT … ON CONFLICT batches, cutting a full live re-seed from ~4 hours to ~15 minutes.

[2.0.0] — 2026-06-19

Changed — Database Optimization

  • ICD-10 diagnosis and comorbidity search. The previous ICD-11 search required a live connection to the WHO API and used AI-translated Bulgarian labels. Diagnosis and comorbidity search now queries a local ICD-10 database seeded from the WHO international classification with official Bulgarian Ministry of Health labels — faster, offline-capable, and aligned with Bulgarian NHIF clinical coding.
  • Lab results are now numerically coded. Each lab result is stored with its LOINC code, canonical SI unit, reference range, and an automatically computed abnormal flag (low / normal / high / critical). Blood gas results use mmHg throughout.
  • Drug coding with ATC. The drug classification tree (ATC, ~6,300 codes) is now seeded into the database. Intraoperative drug events and preoperative medication entries gain ATC codes for research queries.
  • Field-level audit trail. Every preoperative and postoperative field change is now recorded individually — what changed, from what value, to what value, by whom, and when.
  • Finalisation snapshots. When a case is finalised (COMPLETE), a full-case snapshot is stored (one row per case, updated on re-finalization). Research datasets can cite the snapshot to ensure reproducibility.
  • Comorbidities coded in ICD-10. Comorbidity entries now carry an ICD-10 code alongside the free-text label, making them queryable across cases by standard code.

[1.2.0] — 2026-06-18

Changed

  • Clinical data is now stored as queryable database rows. Diagnoses, procedures, comorbidities, lab results, vascular accesses, vitals, and the multi-select fields (positions, techniques, airway, ventilation, handover) — previously held only as JSON — are now also written as proper rows, making research and data export much more powerful. No change to what data is collected or how you enter it, and no change to how the apps perform.

[1.1.1] — 2026-06-17

Fixed

  • Browser/PWA intraoperative saves that use the newer sync headers or the PUT method (edits/deletes, conflict-detected saves, offline replay) no longer fail in the browser. The installed app was unaffected.
  • Finalised cases are now fully locked — intraoperative entries can't be edited or deleted once a case is finalised.
  • Case codes now use the current calendar year (e.g. 2026-0001), resetting each January per user.
  • Offline intraoperative entries are kept and retried through a temporary sign-in expiry instead of being dropped.
  • Department-head view scoped to your own institution — a Head of Department now sees only cases from their institution, not every case in the system.
  • "Undo finalise" now works on mobile and uses a consistent 30-minute window across the app and server.
  • Hardened sign-in so a failed login can't reveal whether an email address has an account.
  • Fixed the desktop "Ongoing cases" shortcut, which could fail to list active cases.

Changed

  • Softened remaining "GDPR compliance" wording to "GDPR principles/considerations" in the documentation.
  • Self-hosting docs now use prisma migrate deploy (production-safe) for schema updates instead of db push.
  • Corrected the stored-data list (removed "time in recovery room," which is no longer collected).

[1.1.0] — 2026-06-15

Notifications

  • Case reminders — both the app and the PWA can now remind you to chart vitals during an active case. Turn it on in Settings → Notifications, choose how often you're reminded (3/5/10/15 min), and send a test notification to confirm it's working. The reminder resets each time you record a set of vitals. In the installed app these fire even when it's in the background; in the PWA they work while it's open (over HTTPS).

Intraoperative charting — reliability & safety

  • The timetable is now backed by an immutable event log, so nothing is lost when two people document the same case at once, and offline entries can't be duplicated when they sync.
  • Edits and deletions keep their full history under the hood (better for audit and medico-legal review), while the chart still shows the clean, current picture.
  • Infusion rate changes display correctly — the chart and pills show the right rate before and after each change.

Account security

  • Hardened login throttling and sign-out (a token is properly invalidated server-side when you log out), and tightened a few access-control edge cases.

Note

  • Wording across the app and site changed from "GDPR compliant" to "designed with GDPR principles" pending a formal legal review.

[1.0.1] — 2026-06-11

Mobile improvements

  • Settings redesign — settings is now two-level: a Profile screen (name, institution, edit institution from a DB list) and a Settings screen (UI: theme/language/preop layout; Automation: auto-fill vitals/BP/HR/background refresh; Privacy & Data: policy/terms/about/export/delete). Admin console visible to admins only. Sign out is a separate persistent button.
  • Inline procedure and diagnosis search — mobile search fields now use inline dropdown autocomplete instead of full-screen sheets. Procedure results display the clinical group as the primary label and the code and domain below it, matching the web app.
  • AI monitor scan — fixed on native Android; camera images now correctly pass base64 to the vitals-scan endpoint.
  • AI advisor removed from case summary — the AI pre-operative advisor button is available only in the preop form. It was incorrectly appearing on the case summary screen.
  • Case status chain completed — mobile dashboard and case summary now reflect the full seven-step status chain: Draft → In Consultation → Awaiting Allocation → In Theatre → Awaiting Post-op → Awaiting Review → Case Finished.

Lab scan improvements

  • Library-anchored extraction — the AI lab scan now only returns tests from the LOSPOR catalogue. Unknown or phantom test names (e.g. "absolute leucocyte count") are silently discarded server-side.
  • Normalised units — all extracted results are mapped to canonical units: Hb in g/L, Hct as a decimal ratio, glucose in mmol/L, and so on. Unit normalisation is enforced server-side regardless of how the value appears in the source image.
  • Custom lab results removed — free-form custom lab entries have been removed. All results must come from the catalogue, ensuring consistent units and reference ranges.

PWA fixes

  • Timetable autosize on PWA — vital-sign input fields, blood pressure popup, and drug dose controls in the intraoperative timetable now adapt to the browser window width. Previously they overflowed off the right edge of the screen.
  • Dark mode colour-scheme error fixeddarkMode: "class" is now set in the Tailwind config, preventing a Cannot manually set color scheme console error on the PWA.
  • Privacy Policy updated to v1.1 — sub-processors section now explicitly covers Mistral AI image processing for lab scan and monitor scan. Effective date updated to June 2026.
  • Terms of Service updated to v1.1 — new section 3a documents user obligations when using AI image scanning features. Effective date updated to June 2026.
  • AGPL-3.0 LICENSE added to mobile applospor-mobile/LICENSE created. Copyright (C) 2026 Kaloyan Dzhunov.

[1.0.0] - 2026-05-26

Dashboard and mobile navigation

  • Dashboard defaults to all accessible cases - the web and mobile dashboards now open to the full case history in reverse chronological order instead of hiding older cases behind a Today filter.
  • Clickable dashboard statistics - dashboard statistics can now act as case-list filters. The selected scope is visible and resettable.
  • Mobile clinical toolbar - the mobile dashboard now uses a compact LOSPOR toolbar with dashboard, new-case, and settings actions instead of an ambiguous plus-only workflow.
  • Visible mobile case scope rail - mobile case scopes are shown as a quiet horizontal rail with counts: All, Today, Month, Active, Drafts, Awaiting postop, Complete, and Handovers.

Mobile preoperative workflow

  • Preop section dashboard - mobile new-case and edit-preop workflows now start with a section dashboard summary. Tapping a section opens a focused full-screen editor instead of forcing one long scroll.
  • Shared clinical number entry - age, height, weight, mouth opening, thyromental distance, and other numeric clinical fields now use a reusable wheel/chip/stepper-style control with manual fallback.
  • Decimal input fixed - comma decimals such as 8,5 and dot decimals such as 8.5 are accepted safely. Empty or invalid numeric input no longer becomes NaN.
  • Mobile AI lab scan - mobile preop labs can now use camera or gallery upload, call the existing Mistral lab reader, review extracted results, and add selected rows. Manual entry remains available.
  • Medication search duplicate-key warning fixed - duplicate labels from drug search results no longer produce React duplicate-key warnings.
  • Continue to intraop validation fixed - invalid mobile preop submission now shows the missing fields instead of jumping back to the top of the form.

[0.4.2] — 2026-05-24

Features

  • Full Bulgarian UI translation — every user-visible string in the interface now adapts to the selected language. All previously hardcoded English labels, section headers, button text, error messages, and status indicators across the admin panel, case entry wizard, register page, settings, guided tour, and preoperative form have been converted to translatable keys. Switching to Bulgarian in Settings → Language now translates the entire app.
  • Vercel Analytics — anonymous page-view tracking added. No personal data is collected.

Security / compliance

  • AI disclaimer corrected — the AI advisor no longer uses "clinical decision support" language. The disclaimer now clearly states that the output is an informational summary, does not constitute clinical advice, and that the responsible anaesthesiologist retains full clinical responsibility.
  • Lab scan upload warning strengthened — the GDPR notice above the upload button now explicitly instructs users to crop patient names, date of birth, ID numbers, and other identifiers out of the image before uploading.
  • PII detection best-effort notice — the Privacy Policy now clarifies that automatic pattern detection is best-effort. Users remain responsible for not entering patient-identifiable information in free-text fields.

[0.4.1] — 2026-05-24

Fixes

  • Terms and Privacy links not opening when logged in — clicking Terms or Privacy in the app footer redirected back to the dashboard. Fixed.

[0.4.0] — 2026-05-24

Features

  • 30-minute review window — submitting the postoperative form now opens a 30-minute review period instead of immediately locking the case. A countdown banner is visible at every step. Navigate back to preop, intraop, or postop to correct any data. The case auto-closes when the timer expires or you click Close Now. The timer persists if you leave and return to the page.
  • Expanded lab catalogue — the preoperative Labs section now includes 100+ perioperative-relevant tests across nine categories: Haematology, Coagulation, Electrolytes, Biochemistry, Liver, Cardiac, Blood Gas, Thyroid, and Inflammatory/Other. Tests are shown in collapsible category rows.
  • Lab reference ranges — each entered result is compared to a reference interval and flagged as normal (green) or out of range (amber). No clinical action is implied; the flag is informational only.
  • Lab search — type in the search box above the catalogue to filter tests instantly.
  • AI lab scan — click Scan lab report to upload a photo of a printed lab result. Mistral AI reads the image and extracts test names, values, and units. A preview panel shows the extracted results; select which ones to add. A GDPR notice is shown above the upload button at all times.
  • HOD access restricted to own institution — Heads of department can view and edit only cases belonging to members of their own institution. Case transfers are also restricted to within-institution recipients. Admin access remains global.

Fixes

  • Autosave error on case reopen — returning to the intraop form after navigating away caused a validation error and autosave failure. Fixed.
  • Postop data blank on reopen — reopening a case that had already been submitted through postop showed empty postop fields. All data is now restored.
  • Review window resets on navigation — leaving and returning to the summary page restarted the 30-minute timer from scratch. The timer now resumes from the correct remaining time.
  • Parallel fluid lane disappears — inline-discontinuing one of two same-category parallel fluids caused the discontinued lane to vanish from the timetable. Fixed.
  • Lab results cut off in print — entering more than 12 or so lab results caused them to overflow and be clipped in the printed protocol. The summary now uses a multi-column layout with a compact font so up to ~40 results fit on the page.
  • Summary cards too narrow on first open — the printable summary was narrower than expected on the first open during case entry. Fixed.

[0.3.0] — 2026-05-21

GDPR — Data minimisation

  • Removed staff names — surgeon, anaesthesiologist, and nurse name fields removed from the preoperative form. Replaced by a free-text Team notes field with a privacy warning.
  • Removed exact surgery date — the date field is replaced by a month/year selector. No calendar date is stored.
  • Anonymous case codes — format changed from DDMMYYYY-NN to YYYY-NNNN (e.g. 2026-0001).
  • Patient identity never stored — the printable protocol leaves identity fields blank for hand-writing after printing. The print-time name/ID dialog has been removed.
  • Institution decoupled from Case — institution is now stored on the user account only.
  • Consent screen — shown on first login; must be accepted before using the app.
  • Terms checkbox on registration — new accounts must accept the Terms of Use and Medical Disclaimer.
  • Privacy Policy page (/privacy) and Terms of Service page (/terms) — accessible without login.
  • Footer links — Terms · Privacy · Open source · AGPL-3.0 added throughout the app.

GDPR — Rights (Articles 15 & 17)

  • Data export — Settings → Privacy & Data → Download my data (JSON, Article 15).
  • Account deletion - Settings - Privacy & Data - Delete my account (soft-delete/access-disable flow; later deletion or anonymisation follows retention policy).

Security

  • DB-backed JWT revocation — revoked tokens survive server restarts.
  • Constant-time login check — prevents email enumeration via response timing.
  • Last login tracking — displayed in Settings → Privacy & Data.
  • Soft-delete — deleted accounts cannot log in.
  • Server-side PII detection — free-text fields are checked for EGN, long digit sequences, date patterns, email addresses, and name patterns. Returns a clear 400 error and logs to the audit trail.

AI advisor

  • Migrated to Mistral La Plateforme — GDPR-oriented inference (EU region preferred; regional inference may fall back to global endpoint). Groq removed.
  • Free-text fields stripped — only structured clinical fields are sent; notes and free-text are never forwarded.
  • Opt-in per case — disabled by default; enabled via a toggle in the preop form.

Features

  • Settings → Privacy & Data — last login, data export, account deletion.
  • GuardedTextarea — live character counter and blur warning for EGN/MRN patterns on free-text inputs.
  • Admin / HOD case access — admins and heads of department can view and edit cases owned by any member.

Fixes

  • Timetable timezone — times were shifting by the UTC offset on every reload; fixed by using UTC methods when reading stored times.
  • Autosave schema coercion — HTML inputs return strings; API schemas now coerce string values, preventing Zod 400 errors mid-typing.
  • Autosave no longer locks cases — postop autosave no longer promotes the case to COMPLETE; only the final submit button does.
  • PDF empty 3rd page — footer text overflow fixed.

[0.2.0] — 2026-05-20

Security

  • Admin approval for new registrations
  • Completed cases locked (403 on edit)
  • Rate limiting on registration, login, AI advice, ICD search, custom terms
  • AI endpoint hardening (16 KB cap, Zod validation, no PHI forwarded)
  • Security headers (X-Frame-Options, CSP, etc.)
  • Session invalidation on logout (in-memory JWT blocklist)
  • Supabase PostgREST API disabled

Features

  • Audit log for case events and AI advice
  • Institution-scoped custom terms

Validation

  • Full Zod schemas for preop / intraop / postop API routes

Fixes

  • Broken UTF-8 characters across the app
  • Register page institution picker on LAN access
  • public/logo.png (1.5 MB) removed in favour of logo.webp (26 KB)

[0.1.0] — 2026-04-01

Initial release. Preoperative, intraoperative, and postoperative data entry. PDF export. ICD-11 diagnosis search with Bulgarian translation. AI pre-operative advisor. Guided tour. Dark mode. Bilingual (English / Bulgarian).