A snooze lives on the medication row, and the reminder tick applied it to
every slot of that medication. A Telegram "skip" of the morning dose
snoozes to the end of the day, so the evening dose of the same medication
was neither reminded nor marked missed by the reminder path. A snooze now
holds only slots whose reminder window had opened when it was set, dated by
the row's updatedAt that the snooze write stamps (the same reading the skip
and pin suppression already uses). A later edit to the row can only widen
the hold back to the old behaviour.
The doctor report kept adherence in a map keyed by medication name, so two
medications with the same name (an ended course and a new one, two
strengths) collapsed into one row and the second overwrote the first. The
map is keyed by medication id now and carries the name as the label; the
report's medication list carries the id. The PDF, the FHIR adherence
observations, the MCP visit summary, the clinician view and the GLP-1
section read it that way.
The runway estimate read FREQ=MONTHLY as one dose every 30 days whatever
its INTERVAL, so a quarterly injection with two doses left showed about 60
days of supply instead of 180. FREQ=DAILY;INTERVAL=2 counted as daily, a
yearly rule ignored INTERVAL and BYMONTH, and a monthly rule ignored extra
BYMONTHDAY values. A CYCLIC schedule was counted as if it never paused,
and a PRN schedule as if it were due every day. The rate now honours
INTERVAL for every frequency, counts the listed days and months, scales a
cyclic schedule by on / (on + off) and leaves PRN out. The low-stock job
selects the schedule type and cycle lengths so it reads the same rate.
Whole-dose counts no longer lose one to binary floating point: 2.4 mL at
0.8 mL per dose is 2.9999999999999996 in IEEE 754 and used to read as two
doses. The stock summary, both runway estimators, the list and detail
routes, the GLP-1 route, the Coach snapshot and the inventory readout now
floor through one helper that treats anything within 1e-9 of the next whole
number as that number.
A reminder with FREQ=DAILY;BYHOUR=7,19 (the morning-and-evening blood
pressure course) was only ever sent in the morning. The tick opened its
send window at notifyHour alone, so the 19:00 slot stayed due until 07:00
the next day, and the claim was keyed per local day, so a second send that
day would have been refused anyway.
The window is now the hour of the current slot: the local hour of
nextDueAt when it is one of the rule's BYHOUR values, else notifyHour. The
early skip for a reminder already sent for its slot uses the same hour, so
it stays consistent with the hold-open rule and lastNotifiedAt. A
multi-hour reminder claims per slot hour; a single-hour reminder keeps the
per-day key.
A medication's startsOn/endsOn are calendar dates stored as UTC midnight,
and the rrule library emits its day anchors the same way. The engine read
those midnights on the user's wall clock, which west of UTC is the evening
before. A FREQ=WEEKLY;BYDAY=MO plan therefore fired on Sundays in New York,
a daily course starting on the 14th produced a dose on the 13th, and an
evening dose on the last course day was cut off. One-shot doses, the first
dose of a rolling cadence and the legacy weekday walk (startsOn floor,
endsOn cap, every-N-weeks phase) carried the same shift.
The engine now keeps a calendar day as a calendar day end to end: startsOn
and rrule anchors are read from their UTC fields, instants (createdAt,
intake times) are converted to the local day they fall on, and only then is
the HH:mm materialised in the user's zone. Rolling cadences count calendar
days from the local day of the last intake, so a late-evening dose across a
DST change no longer slides a day. A nonexistent local time now always
moves forward, including in zones whose clocks skip midnight. RRULE anchors
with BYHOUR are collapsed per day so a twice-daily protocol cannot emit
duplicate slots.
CYCLIC on/off weeks are counted as seven-day blocks from the start day in
the local calendar. They used to snap to Sunday-rooted UTC weeks, so a
three-weeks-on course starting on a Wednesday lost its first days and the
off week began on the wrong day east and west of UTC.
Measurement reminders pass their anchor and course end as instants; the
adapter now converts them to the user's calendar day before handing them to
the engine.
Papra's date: filter matches the document date, so a row now shows that
date or none, and an undated document falls out of a date range in the
list as it does on the server. Import still files an undated document
under the day it was added.
Papra organization ids are checked against org_ plus 24 characters
before anything is sent. A 403 user.not_in_organization and a 400
server.invalid_request.params read as no such organization instead of a
refused key; the error body is read for its code and never passed on.
The connection test also reads the tags, so a key without tags:read
fails at save (permissionMissing) instead of on the first filter. Name
searches quote each word, so a leading dash is not a negation, and the
date range goes to Papra as date:>= / date:<= with the per-row check
kept as a net.
Keyed uploads and picker imports also check deleted, not yet purged
documents by content and answer deleted, remembering the new key on the
tombstone so the purge carries it into the ledger. A hand upload without
a key still stores a new copy. No content hash of a deleted document is
kept past the purge.
The settings card stays visible, read-only, while a connection is
stored but the operator list is gone or Documents is off, so the token
can still be removed; a new address asks for the token again. The
picker clears a selection when the search or a filter changes, says
when the 25-document limit is reached, steps back the rows a finished
run did not touch and offers Pick more, shows an empty state, and
announces each row's result to screen readers. Both system switches
are radio groups with one tab stop and arrow-key movement. Strings in
all seven languages.
- Source keys carry the normalised origin of the Paperless-ngx or Papra
they came from (source_instance on the document, the alias and the
purge ledger; unique NULLS NOT DISTINCT). A key stored without one
(v1.39.2 imports) matches any instance of its system, and a lookup
without one matches any stored instance. The upload route, the lookup
route and the import script accept or send it; backups carry it.
- Saving a connection reuses the stored token only for the same origin;
a new origin needs the token again (422 tokenRequired).
- Document ids are checked against what each system issues (Paperless
digits, Papra [A-Za-z0-9_-], never . or ..) before any URL is built;
the Papra organization id likewise.
- Paperless API 9 filters dates on created__gte / created__lte, in the
picker and the script. A search page the local re-check empties reads
on, up to five source pages. Tags are read past the first 250.
- The import's key pre-check uses the lookup allowance, a failed fetch
no longer charges the upload allowance, and one import per key runs at
a time under an advisory lock, so a double click downloads once.
- Reading and deleting one's own connection works with the documents
module off and the operator list unset.
97507-8 is the mean of interstitial glucose in mass/volume. A spot-reading
mean is neither, so it now goes out text-only in the account's unit. A
continuous-stream mean keeps the code and is sent in mg/dL, the unit it is
computed in, whatever unit the account displays.
A run of the HL7 validator against every code the export can emit found
terms that do not exist, are inactive, mean something else, or carry a
display the code system does not publish.
- Body water and bone mass use 101683-1 and 101685-6; the old codes are a
nasogastric fluid output and a calculated body-fat mass.
- Step length, walking speed, walking asymmetry and VO2 max ride their
HealthKit identifiers: LOINC has no fitting term, and two of the old
codes do not exist.
- Mood, estimated A1C, average cycle length and average period length are
text-only. The old codes did not exist or were questionnaire items.
- LDL, vitamin D and eGFR use the method-less and total terms (2089-1,
62292-8, 69405-9) a recorded value can honestly claim.
- Resting heart rate and pulse-oximetry SpO2 also carry the heart-rate and
oxygen-saturation codes the R4 vital-signs profiles require.
- Blood pressure, adherence, glucose-panel and injection-site codings carry
the published displays; ATC codings carry none, since the user's
medication name is not the ATC name. The name stays on the text.
- Mood is a survey finding, not a vital sign, so it no longer rides the
vital-signs panel.
Allergy used 106190000, inactive since 2013; it is now 473011001 Allergic
condition. Injury keeps 417163006 with the display SNOMED CT publishes,
Traumatic or non-traumatic injury; the old display was rejected.
A window without a vital sign emitted the panel with result: [], which R4
rejects. The DiagnosticReport now goes out only when it has a result, and
the vital-signs section drops an empty entry list rather than sending one.
The HL7 validator reports 419161000 and 419465000, the left and right
concepts of the R4 bodysite-laterality value set, as inactive. Use the
active qualifier values 7771000 Left and 24028007 Right, with 51440002
Right and left. The side word in bodySite.text stays English, the language
of every other generated text in the Bundle; the builder takes no locale.
Follow-ups to the clinical-record search, test first:
- Condition and vaccination notes are no longer searched or returned.
Both reads use an explicit column list without the note, as the Coach
illness snapshot and the doctor report already do. A structural guard
fails when any MCP read of either model names noteEncrypted or has no
explicit select.
- A document uploaded with AI reading deferred is not searched by its
text and returns no excerpt (metadata.reason "ai_read_deferred"); its
title, kind and links stay findable.
- Documents older than the newest 500 are found by title and file name
through a bounded SQL lookup on those plaintext columns.
- Query words shorter than four letters match whole words or word starts
only, so "arm" no longer finds "Darm".
- A deleted practitioner is no longer named or matched, in search, fetch,
get_visits and the appointments in get_preventive_care. The empty-query
listing shows visits that happened, not booked ones.
- get_labs, get_medication_schedule, get_medication_compliance and the
lab and medication resources (read, list, completion) answer
{ present: false, reason: "module_disabled" } when their module is off,
without reading. get_visits withholds condition labels when illness is
off and says so in conditionsReason.
- The consent screen and the tool descriptions name what a connection
can read, the indexed text of stored documents included.
A settings card under Integrations connects either system and tests it
before saving. An import button beside Upload in Documents, on a
condition's documents card, and beside Add in the visit and vaccination
document pickers opens a sheet that searches by name with tag and date
filters, shows what HealthLog already holds, and imports the ticked
documents one at a time with a result per row. The buttons appear only
for a person in their own record with a connection; strings in all
seven languages.
The ingest path takes the capability from its caller, so the upload and
the import route each read getAiCapability("documentAi") in their own
source, where the AI route inventory classifies them.
A person stores one connection per system (base address, Papra
organization id, API token encrypted at rest) and can search it by
document name with tag and date filters, then import picked documents
one per request through the same ingest path as an upload: source-key
answer before any download, content dedup, deleted stays deleted, quota,
provenance, and the person's own AI reading setting. An import can link
the document to the visit, condition or vaccination it was picked for.
The operator lists the instances in DOCUMENT_SOURCE_ORIGINS; unset, the
feature is off. Only listed origins are contacted, through safeFetch with
the origin pinned, redirects refused, timeouts and byte caps. The routes
are cookie-only and owner-only. The token is registered for rotation,
removed by Delete all data, and not carried in backups.
In its default mode ChatGPT can call only search and fetch, and until now
those two covered metric domains, medications and lab analytes. A question
about a knee operation or a discharge letter had nothing to land on.
search now also returns visit:<id> (procedures included), condition:<id>,
document:<id> and vaccination:<id>. Matching runs after the decrypt over
the account's own rows, bounded per kind, folded the way the body-sites
view folds sites, with German/English body-part and vault synonyms.
Documents match on title, file name, kind and whole words of the blind
content index; the body is never read for a search. Results rank by
matched query words, then score, kind, date and id, so the offset cursor
stays stable. Record results are capped at 50.
fetch returns each record's key fields and its one-hop links through the
link service, every link carrying an id fetch accepts. Free text is fenced;
short labels are scrubbed. A document returns metadata and at most a
1500-character excerpt of its indexed text, never the file or its stored
summary.
A switched-off module's records are neither read nor returned: illness,
inboundDocuments and vaccinations for the new kinds, and now medications,
labs and the owning module of a discovery signal for the older ones.
The upload route keeps authentication, its rate buckets, multipart parsing
and the response shape; the source-key answer, size and type checks,
pre-link narrowing, content-hash dedup with alias memory, the quota-gated
insert, the audit row and the background jobs move to
src/lib/documents/ingest.ts unchanged, so a second way in can store
through the same rules.
Condition.bodySite carries the free-text site as text. The side rides a
contained BodyStructure reached through the core bodySite extension, coded
with the SNOMED CT concepts of the R4 bodysite-laterality value set, and is
repeated in the text so a receiver that ignores extensions still reads it.
A snooze that had already run out before the send is history, not a
choice about the reminded slot, so it no longer keeps a row from being
repaired. The "no due date" arm matched nothing, because a reminder
cannot be created without a cadence; it now covers a rule whose last
occurrence was the one sent, which is open unless it was done or
skipped since, while a course whose window closed before the send stays
closed through the end-date guard. The start-page step reads a stored
item list only when it is a list, so a malformed layout can no longer
fail the migration.
Renaming an overdue booster check-up on the web still moved it to next
year. The edit sheet sends the first due date as the browser's local
midnight on every save, a booster row stores the dose instant plus N
days, and the route read the different instant as a new first due date.
The route now compares the first due date by calendar day in the
profile timezone and drops a resent same-day value from the write. The
edit sheet sends only the fields that changed. Re-enabling a reminder
that stays due after it is sent keeps the slot it was on instead of
scheduling from now; a short-cycle reminder is still scheduled from
now. Moving the notify hour later on the day a reminder was already
sent carries the last-sent cursor along, so the slot does not go out a
second time at the new hour.
The start page read every reminder due before the end of the day, so a
daily weigh-in at 20:00 or a twice-daily blood-pressure course showed
"check-up due" from the morning on and took a pinned place on the rail.
`holdsOpenAfterReminder` is now the one definition of a reminder that
stays due after it is sent: an interval longer than seven days, a rule
whose next gap is longer than seven days, a rule with no occurrence
left, or no cadence; never an appointment. The tick uses it to decide
whether a delivered reminder rolls on, and the start page uses it to
show such a reminder for its whole due day and keep it under the cap.
A short-cycle reminder shows only once its time has come and is not
pinned.
The tick also skips a reminder already sent for its current slot
outside its notify hour before the auto-resolve read. The eventful
satisfy worker still resolves it the moment a reading or lab result
lands, and the poll still runs for it once a day, in the notify hour;
the weekly hold stays after that poll so the safety net is daily.
The first cut kept only free-text check-ups due after their reminder
and let every reminder with a measurement type roll on. A fortnightly
PHQ-9 or GAD-7 reminder is a measurement reminder, and each one still
jumped two weeks forward the morning it was sent without the
questionnaire being filled.
The rule is now the cycle length. A reminder whose next slot is more
than seven days away, or that has no cadence, stays due after its
reminder until it is satisfied (a matching reading, a lab result or
"done"), skipped or snoozed, and is sent again at most once a week. A
reminder on a weekly or shorter cycle rolls on as before. Appointments
stay one-shot. A reading that lands on an open measurement reminder
still sets lastSatisfiedAt and schedules the next cycle from itself.
The 0355 repair drops its "no measurement type" condition for the same
cycle rule, so the fortnightly questionnaires in the reported shape
are put back on the day they were reminded.
The snoozed fixture sat inside a week of the send, so the one-week clause already excluded it and dropping the snooze guard from the migration left the suite green. Snooze it further out, a minute after the send, so only the snooze cursor tells it apart from a roll-on.
The start-page item choice stores the enabled kinds, so a list saved before a kind existed reads as that kind switched off. Say so next to the kind list, where the next kind will be added.
Every edit of a reminder recomputed its due date from now, so fixing
the label or the location of an open, overdue check-up moved it to its
next slot, the same "gone until next year" the reminder tick caused.
The due date is now recomputed only when the edit changes when the
reminder recurs: the interval, the rule, the first due date (compared
by value, since the edit forms resend every field), or switching a
disabled reminder back on. A notify-hour edit keeps the due day and
moves the hour on it, together with a snooze pinned to that slot.