2440 Commits
Author SHA1 Message Date
Marc-André Bombeck 1cb50ed21d Merge pull request #1144 from MBombeck/fix/restore-key-probe
fix(backup): restore opens legacy values, drill checks every account, backups hold less memory
2026-10-04 17:44:57 +02:00
Marc-André Bombeck 006679ed9e Merge pull request #1145 from MBombeck/perf/dashboard-client
perf: faster charts and insights, no sidebar scrollbar flash, Umami events delivered
2026-10-04 17:33:51 +02:00
Marc-André Bombeck 92f17c8ca4 Merge pull request #1140 from MBombeck/ready/pr-1131
fix(import): leave out Apple Health samples HealthLog wrote itself
2026-10-04 17:33:47 +02:00
Marc-André Bombeck 45fafdf096 Merge pull request #1138 from MBombeck/feat/pulse-hourly-mean-complete
fix(pulse): a day of pulse is the mean of its hours' means, everywhere
2026-10-04 17:33:43 +02:00
Marc-André Bombeck 6c0a606bf3 fix(a11y): give the loading score and vitals grids a role to name
Both grids carry an aria-label while their data loads, on a plain div,
which axe reports as aria-prohibited-attr: the label has no role to
name. A scan that lands while the scores strip is still loading fails on
it. The loading grids now take role=status, the role a loading region
has and which already implies the polite live region; the loaded vitals
grid carries neither.
2026-10-04 16:24:37 +02:00
Marc-André Bombeck 4916a29bbe perf(insights): mount below-the-fold overview sections near the viewport
Entering the Insights overview fired every section's reads at once, about
two dozen requests on a seeded account, and the server worked through
them in a queue: the hero and the first sections waited behind trends,
the period review, cycle, signals, rhythm events, health status,
breathing, labs and ECG, all far below the fold.

The first three sections in the account's own order still mount with
the page. Every later one renders a 1 px sentinel until it comes within
400 px of the visible area of the shell's scroll container, then mounts
and fetches; once mounted it stays. Measured on a seeded account under a
4x CPU slowdown: 24 -> 13 requests on entry, the entry network span
1.3-1.8 s -> 0.5-0.9 s, LCP about 1.35 s -> 0.9 s, blocking time about
550 ms -> 200 ms. Scrolling to the end still loads all 25 and renders
all ten sections.

Specs that assert on, measure or scan the lower sections scroll them in
first through a shared helper, so their coverage is unchanged.
2026-10-04 16:24:37 +02:00
Marc-André Bombeck fa48110875 fix(nav): mark the scroll edge when the sidebar list overflows
On a short window (around 785 px and below) the nav list is taller than
the room between the logo band and the footer group, so it scrolls. With
nothing marking the edge, the last visible entry was cut off right at
the Collapse row and read as the footer covering it; macOS hides the
scrollbar that would have said otherwise.

While the list overflows, the footer group draws the same hairline the
user section does. A ResizeObserver on the nav and its list keeps the
state current as the window or the entries change; with room to spare
the rail is unchanged. The e2e spec checks the edge, that the last entry
scrolls fully clear of the footer, and that a tall window draws no edge.
2026-10-04 16:24:37 +02:00
Marc-André Bombeck 194712c916 Merge pull request #1143 from MBombeck/fix/rest-mode-date
fix(insights): show the Rest Mode onset as a date
2026-10-04 16:15:03 +02:00
Marc-André Bombeck dc9b0e3936 test(import): read the note's text without a tag-stripping replace 2026-10-04 16:14:47 +02:00
Marc-André Bombeck 23961d2280 perf(medications): stop re-sending the read a nav prefetch just made
The medications list and the compliance summary refetch on every mount,
so a take or skip logged on another device shows on return (#316). The
nav link's hover / touch / focus intent and the route-commit preload
prefetch both reads too, so every visit through a link sent each of
them twice: the prefetch, then the identical request from the mount a
few hundred milliseconds after the first answer landed.

The mount now skips its refetch only when the cached data answers a
prefetch that went to the network for this visit: within the last 10 s,
and since the route last left /medications. A direct load, a return
with an older entry, or a quick return whose prefetch found the entry
still fresh keep the refetch on mount.
2026-10-04 16:12:04 +02:00
Marc-André Bombeck f18e6ed34b fix(backup): notice a backup run the process died under, and take its account last
A weekly or nightly backup job whose process died (a restart, a
container out of memory) stayed active until its two- or four-hour
expiry, and its retry walked the accounts in the same order: the account
that killed it has the oldest copy, so it led again, and every account
after it went without a copy for as long as that record stayed too
large.

Both passes now send with a five-minute pg-boss heartbeat, so a dead run
fails within minutes and its retry picks it up. Each pass records when it
started and finished every account (backup_pass_attempts, migration
0373); an account whose last attempt never finished goes behind all the
others. The backups page shows when the run in progress started and names
an account a run died under, for the weekly and the off-host card, and a
heartbeat timeout reads as what it means.
2026-10-04 15:56:56 +02:00
Marc-André Bombeck b25df614f9 fix(insights): show the Rest Mode onset as a date, not a raw timestamp
The health score card passed the episode onset, an ISO instant, straight
into its sentence, so it read "Rest Mode active since
2025-08-04T09:55:00.000Z". It now shows the day that instant falls on in
the profile timezone, in the user's date format. The test that pinned the
raw value now pins the formatted day.
2026-10-04 15:36:04 +02:00
Marc-André Bombeck 2c1369f01b perf(charts): measure chart text outside <body>
Recharts sizes every tick and label by writing it into one off-screen
span and reading getBoundingClientRect(). Under <body> each read forced
a document-wide style recalc, because the stylesheet's descendant-subject
:has() rules leave Chromium no way to scope the invalidation. A seeded
dashboard made 149 such reads per cold load at about 2.8 ms each, about
1.8 s of main-thread time under a 4x CPU slowdown.

The chart runtime now parks that span in a zero-size, contain: strict
host under <html>, which Recharts finds by id and keeps using. Each read
drops to about 0.01 ms; the dashboard's total blocking time under the
same slowdown falls from about 2.5 s to about 0.75 s. The host copies
<body>'s classes, where the app font is set, so every string measures
exactly as before; an e2e spec checks that parity on the drawn labels
and that the host survives a client-side round trip.
2026-10-04 15:28:28 +02:00
Marc-André Bombeck 1b1da652d3 fix(monitoring): route Umami events to the endpoint the tracker was built with
The tracker posts to `${data-host-url}${collect path}`, and the collect
path is compiled into the operator's Umami build: /api/send by default,
anything else when the instance sets COLLECT_API_ENDPOINT. The tag
pointed data-host-url at the app origin and the proxy listened only on
/api/send, so a renamed endpoint sent every page view and event to an
app path that does not exist and got a full 404 page back each time.

Point data-host-url at /api/monitoring/umami and answer every path under
it with one proxy route. The route reads the collect path from the
configured tracker (cached for an hour), forwards only that path to the
Umami host, and answers anything else, or an unreadable tracker, with an
empty response. The admin test event posts to the same learnt path. The
send-URL resolver lives in one module instead of two copies.
2026-10-04 15:13:52 +02:00
Marc-André Bombeck 2e150e97cf fix(nav): stop the sidebar flashing scrollbars while it expands
The rail animates its width from w-16 to w-64, and the labels laid out
against the growing width: every row overran the nav and multi-word
labels wrapped, so the nav's overflow-y-auto painted a vertical and a
horizontal scrollbar for the length of the transition.

Lay the expanded column out at its final width from the first frame and
let the rail clip it. The final layout is unchanged; the column fills
the expanded rail exactly. A unit test pins the structure and an e2e
spec samples the nav on every frame of a real expand and collapse.
2026-10-04 15:08:42 +02:00
Marc-André Bombeck 13b8920dd5 Merge remote-tracking branch 'origin/main' into feat/pulse-hourly-mean-complete 2026-10-04 14:54:53 +02:00
Marc-André Bombeck 67e8e7c4f2 fix(import): pluralise the note on records HealthLog wrote itself
The note had one form per locale, so a single left-out record read as a
plural. It now resolves One, Few and Other through tCount like the other
counted strings, with the singular in every locale and the Polish few form.
2026-10-04 14:34:42 +02:00
Marc-André Bombeck 29798a76e3 fix(labs): keep the trend column as wide as its sparkline on a wide list
The wide tier let the trend column grow to 8rem while the sparkline it
holds is a fixed 72px glyph, so at 1920 px an empty 56 px strip opened
between the sparkline and the chevron. The column now stays 72px in both
tiers; the extra width goes to the range bar and the name, as before.

The wide-list spec reads the resolved grid tracks and asserts the trend
track is 72px at every width it covers.
2026-10-04 14:28:04 +02:00
Marc-André Bombeck 0ae50849d8 Merge remote-tracking branch 'origin/main' into ready/pr-1132 2026-10-04 14:08:29 +02:00
Marc-André Bombeck fbd639d9f1 Merge remote-tracking branch 'origin/main' into ready/pr-1131 2026-10-04 14:08:27 +02:00
Marc-André Bombeck dee00613d8 Merge remote-tracking branch 'origin/main' into release/v1.40.0 2026-10-04 12:23:40 +02:00
Marc-André Bombeck 32de7ecf37 Merge pull request #1135 from MBombeck/fix/cycle-backup-and-med-put
fix(backup): carry encrypted free text readable in portable backups
2026-10-04 12:23:35 +02:00
Marc-André Bombeck 4e88243075 Merge remote-tracking branch 'origin/main' into release/v1.40.0 2026-10-04 11:29:47 +02:00
BernhardBuckel a1a84f924b feat(labs): let the lab list's range bar use a wide card
On a wide window the compact lab list was one five-column row whose only
flexible column was the name: the name column sat mostly empty while the range
bar and the trend stayed small at the right edge.

The list's card is now a container, and the columns follow its width instead of
the window's. Below 64 rem of card the row is the lg table it was, with the
range bar at 12 rem; from 64 rem the bar column grows from 12 to 18 rem and the
trend column from 72 px to 8 rem, and the bar fills its column. The two tiers
exclude each other, so the result does not depend on the order the CSS is
emitted in. The trend sparkline itself is unchanged.

From lg up a long analyte name wraps instead of being cut off; a phone keeps the
single line with an ellipsis, and the bar keeps its own 12 rem cap there.

The shared 1280 px frame is untouched; a Playwright spec measures it on /labs,
/measurements and /mood at 1280, 1920 and 3440 px.

Fixes #1057
2026-10-04 10:56:43 +02:00
BernhardBuckel a5e98c9e71 fix(import): leave out Apple Health samples HealthLog wrote itself
The iOS app writes manual entries into Apple Health, and mirrors Withings and
import rows into it, each stamped with dev.healthlog.app.origin = healthlog.
The server already holds those readings under their own source, so the same
sample arriving through an Apple Health export is a second copy: the series
shows it twice and the rollups count it twice.

The export import now leaves out a record carrying that marker. The marker is a
child <MetadataEntry>, read after the record's open tag, so a record is
committed at its close tag. As a fallback for manual entries mirrored before the
app set the marker, a record is also left out when a MANUAL row of the same
type and value sits within 2 s (same rule and tolerance as the sync path).
Withings and import rows are not matched by value and time: they reach Apple
Health only through the mirror, which always sets the marker.

The import summary says how many records were left out for this reason.

Fixes #1109
2026-10-04 10:53:23 +02:00
Marc-André Bombeck 39a5862d54 feat(settings): add a Reasoning select to the Local and gateway forms
One select under the model field of the Local and OpenAI-compatible
provider forms: Default, Off, Low, Medium, High, with a one-line hint
that Off leaves the answer budget for the answer itself. It reuses the
NativeSelect, label and hint layout of the field above it, seeds from
the stored entry, and saves with the form. Strings in all seven
locales.

Refs #1126
2026-10-04 10:43:04 +02:00
BernhardBuckel 497da7ca8b fix(pulse): make a day of pulse the mean of its hours' means
A watch samples pulse about every 5 to 10 minutes at rest and about every
5 seconds in a workout. The daily value was the plain mean over a day's
readings, so a workout hour of several hundred readings outweighed the other
23 hours: a day with one hard hour and 23 resting hours (60 bpm) read about
128 bpm, not 64.

A day of pulse is now the mean of its per-hour means, in the rollup DAY
bucket, the live per-day read the chart serves, and the series route's day
grain. Windows over several days weigh each day once instead of by its
reading count, so the bias does not return one level up. Reading counts,
min/max, and the regression sums still run over every reading. The chart
tooltip and the data table name the statistic, in all seven locales.

The statistic is limited to pulse; HRV, SpO2 and glucose keep the plain mean
(see src/lib/measurements/day-statistic.ts). Stored DAY rows are rewritten by
the next rollup recompute (admin recompute route), not on read.

Fixes #1107
2026-10-04 10:41:08 +02:00
Marc-André Bombeck 8ac3fddc3b Carry visit and vaccination free text readable in a portable backup
A practitioner's note, a visit's reason, outcome and body site and a
vaccination's note travelled as stored ciphertext in a portable file, the
same defect the cycle and mood sections had. They now go through the
shared sealed-text helpers: readable in a portable file, sealed under the
receiving host's key on restore, ciphertext kept only where this host
opens it and otherwise reported as visitCiphertext or
vaccinationCiphertext. The practitioner address and phone ciphertext of a
disaster-recovery file gets the same check, since the key preflight now
leaves these sections to it.
2026-10-04 10:19:31 +02:00
Marc-André Bombeck 2798cc953f refactor(workouts): drop the unused clock arguments of the blank draft 2026-10-04 10:18:16 +02:00
Marc-André Bombeck 17b653b819 feat(workouts): read a blank start as now minus the duration
Refs #1085. Logging a workout right after finishing it meant changing the
start first, or the form refused it as ending in the future. The start field
now opens blank; left blank, the workout is saved as starting the duration
before now. A start the person enters is validated as before.
2026-10-04 09:07:39 +02:00
Marc-André Bombeck 43a53255c9 Carry custom mood labels readable in a portable backup
The labels of the mood tags and mood categories an account created
travelled as the stored ciphertext in a portable file, so a restore onto
a host with another key refused the file or wrote labels nobody could
open. They now follow the cycle note: readable in a portable file, sealed
under the receiving host's key on restore, ciphertext kept only where
this host opens it and otherwise reported as moodLabelCiphertext.

The sealing helpers move to one module both sections share.
2026-10-04 08:31:11 +02:00
Marc-André Bombeck 8121935642 Carry cycle free text readable in a portable backup
A portable file carried the day-log note, the sensitive-category envelope
and custom symptom labels as the stored ciphertext, so a restore onto a
host with another key either refused the file or wrote values nobody
could open. The portable file now carries them readable and the restore
seals them under the receiving host's key; a disaster-recovery file keeps
the ciphertext.

A file written before this still restores: each ciphertext value is
opened with this host's keys first, and one that does not open is kept
out of the row and reported in the skip summary as cycleCiphertext. The
key preflight leaves the cycle sections to that per-value check.
2026-10-04 08:17:34 +02:00
Marc-André Bombeck db9618ee7b Merge remote-tracking branch 'origin/main' into wip/v140-H 2026-10-04 00:43:10 +02:00
Marc-André Bombeck b74c6de7ce fix(cycle): give the custom-symptom popover the shared symptom icon picker 2026-10-03 23:38:12 +02:00
Marc-André Bombeck 0100256c60 test(labs): pin the lab scan's waiting line to the copy that does not promise the page can be left 2026-10-03 23:13:59 +02:00
Marc-André Bombeck 367a1bb4b4 fix(symptoms): size the icon pickers for touch and name each icon in the reader's language 2026-10-03 23:10:56 +02:00
Marc-André Bombeck 1eab6145a9 fix(symptoms): show the load error in the quick entry instead of the empty hint 2026-10-03 23:07:16 +02:00
Marc-André Bombeck 1494409209 Merge remote-tracking branch 'origin/main' into wip/v140-H
# Conflicts:
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/ko.json
#	messages/pl.json
#	src/__tests__/sharing-surface-guard.test.ts
#	src/__tests__/v137-wave-zero-fixtures.test.ts
#	tests/integration/sharing-manage-handler-matrix.test.ts
2026-10-03 22:52:49 +02:00
Marc-André Bombeck 4d7abdd12b Give a category its own line in the manage sheet
At phone width the three actions left the label a few characters; the
count and the hidden state now sit under it, the course list's row
anatomy.
2026-10-03 22:47:09 +02:00
Marc-André Bombeck a14174fe4d Let a medication have several courses
A medication taken now and then (#1024) keeps one row and gains course
rows: calendar spans, both ends inclusive, an optional encrypted note, no
schedule or dose of their own. Courses never overlap and every course but
the latest has ended, so the latest course is always the one the
medication's startsOn/endsOn project. src/lib/medications/courses.ts is
the only writer of those two columns for a medication with courses; it
writes under a per-medication advisory lock and re-projects in the same
transaction, so reminders, the next-due line and intakeActionable keep
reading the columns and stay right.

- Migration 0368 adds medication_courses and backfills one course per
  medication window (an end-only window starts on the creation day, or
  on its end day when that lies earlier); a medication without a window
  stays course-less. Idempotent, data-preserving.
- GET/POST /api/medications/{id}/courses and PATCH/DELETE
  .../courses/{courseId} (owner-only); create seeds the first course
  from a window; the medication PUT edits the latest course and refuses
  a window that would break the rules (medications.course.*).
- List, detail, create and update carry courses, courseCount,
  previousCourseEndedOn and canStartCourse, resolved on the server.
- Compliance expands over the span of all courses and drops the slots
  between them; every compliance read selects the courses. A rolling
  cadence is not anchored by an intake from a previous course.
- The card names the last course's end and the running course's number;
  the detail Lifecycle group lists the courses with start, edit and
  delete. The Coach compliance block, the MCP resource, the doctor
  report window and table, the clinician view and FHIR (one
  MedicationStatement per course with effectivePeriod and status) read
  the courses.
- Backup carries courses per medication and the restore writes them and
  re-projects; an older file derives the one course its window
  describes. Wipe plan, encrypted columns, rotation and OpenAPI follow.
2026-10-03 22:39:25 +02:00
Marc-André Bombeck ef0028c020 Merge remote-tracking branch 'origin/main' into wip/v140-I
# Conflicts:
#	docs/api/openapi.yaml
2026-10-03 22:16:29 +02:00
Marc-André Bombeck 8d64da2807 Name the sick note kind in every locale
Label SICK_NOTE in all seven bundles and pin that every document kind
the database knows has a label in each locale, a place in the vault's
kind list and a glyph: the kind reaches the screen through a dynamic
key, which the call-site coverage guard cannot see.
2026-10-03 21:43:37 +02:00
Marc-André Bombeck 1564e7c396 Add the sick note document kind
A certificate of incapacity for work (in Germany the
Arbeitsunfähigkeitsbescheinigung) is now its own document kind,
SICK_NOTE, instead of Other. The enum value rides migration 0370.

Uploads, edits, the kind filter, bulk edits, the import script and the
backup format accept it; the vault files it with a thermometer glyph
between prescriptions and referrals; the filing suggestion can propose
it and names the words a sick note carries. The extraction's own kind
field keeps its three values.
2026-10-03 21:40:01 +02:00
Marc-André Bombeck 4cb8118802 Let a person name their own medication categories
A medication can be filed under a category the person named, next to the
built-in ones (#1041). The labels live in medication_category_labels,
encrypted at rest, capped at 20 per account; a medication filed under one
carries its custom:<uuid> key in the category column.

- GET/POST /api/medications/categories (POST idempotent, 30/min per actor,
  admitted at MANAGE like the cycle symptom vocabulary) and owner-only
  PATCH/DELETE /api/medications/categories/{key}. DELETE moves every
  medication filed under the category to OTHER in the same transaction
  and answers movedCount.
- category is widened to a built-in value or custom:<uuid> on the wire,
  the create and update bodies refuse a key that is not the caller's
  (422 medications.category.unknown), and list, detail, the insights
  medication tiles and the MCP medications resource carry categoryLabel.
  A key with no owned label reads as OTHER and is counted on the event.
- The wizard's class step lists the person's categories under the
  built-in rows with an inline add row; the list page gains a category
  filter (session only) and a manage sheet for rename, hide and delete.
- Backup carries customMedicationCategories (decrypted labels) and the
  restore writes them before the medications; wipe plan, encrypted
  column registry, rotation script, sharing guards and OpenAPI follow.
2026-10-03 21:38:03 +02:00
Marc-André Bombeck 65b2f75421 perf(dashboard): load the quick-entry forms when a sheet opens
The dashboard and the bottom bar's capture picker imported all five
quick-entry forms eagerly, so every authenticated paint carried them for
sheets most visits never open. One module now wraps them in next/dynamic
(client-only, a skeleton holding the sheet body while the chunk loads)
and both callers use it. The discard guard stays static. The picker tests
mock the one module instead of five.
2026-10-03 21:30:38 +02:00
Marc-André Bombeck 9272daa59c feat(symptoms): log a symptom in one step and show it on the illness pages
- A new capture kind "symptom" (illness module, illness write grant) in
  the bottom bar's picker and the dashboard Add menu. The sheet follows
  the measurement and mood forms: the person's symptoms as chips with the
  cycle sheet's dashed "new symptom" chip, the 0-10 slider with its
  anchors, the time prefilled with now, an optional note, and a "during"
  selector only while an illness episode is open.
- "Your symptoms" on the illness page: one line per symptom (how often in
  30 days, how strong at worst, when last), the last 30 days grouped by
  day, edit and delete per entry, rename, icon, hide and delete per
  symptom. Renders nothing until a symptom exists.
- "Symptoms logged" on an episode's page: the entries filed against it or
  logged while it ran, the filed ones marked. This is the reader of the
  episode link.
- Query keys live under the illness root, so either side's writes
  refresh both.
2026-10-03 21:24:12 +02:00
Marc-André Bombeck bb58b40b2a Run fact extraction in the background, re-checking under a row lock
The extract route accepts `Prefer: respond-async` for all three inputs
(text, stored, vision) and queues a DOCUMENT_EXTRACT run (added to
migration 0370). The run's result is the count of staged facts and the
document status; the facts themselves are read from the document.
Without the header the route answers with the document detail as
before; both paths run one shared body.

A background read can take minutes, and a review the person finishes in
the meantime must not have its approved facts replaced. Staging now
locks the document row and asks again whether it may stage, refusing
with the route's own alreadyConfirmed / alreadyPartlyConfirmed codes.
The synchronous path gets the same re-check.

The vault's stored-text extract uses the background path and shows the
shared waiting line in place of its hint while the read runs.
2026-10-03 21:14:47 +02:00
Marc-André Bombeck d6571c4e9c feat(symptoms): add person-defined symptoms and timestamped occurrences
A symptom of one's own ("aura", "headache") is a definition with an
encrypted name and an icon from the cycle custom-symptom allowlist; each
occurrence carries a time, a 0-10 intensity (the pain score's scale), an
optional encrypted note and an optional link to one of the record's open
illness episodes. Both ride the illness module: every route answers
illness.disabled when it is off.

- Migration 0369 (idempotent): symptom_definitions and symptom_events,
  intensity CHECK 0-10, episode link ON DELETE SET NULL.
- /api/symptoms/definitions{,/[id]} and /api/symptoms/events{,/[id]}:
  eight active definitions per record (a hidden one frees its slot,
  showing it again re-checks), hide or purge, events create/list/edit/
  delete, a new link must name an open episode of the same record
  (422 symptoms.episode.notOpen). Error group symptoms.*.
- Sharing: reads at READ, logging an occurrence at WRITE, defining and
  editing at MANAGE; C1/C3/C4 audit details; frozen lists, the handler
  matrix and the activity verbs updated.
- Both encrypted columns registered and rotated; both models in the wipe
  plan; both carried at both backup ends with occurrences nested in their
  definition, plaintext in a portable file and ciphertext in a recovery
  file, and a link to an episode the file lacks dropped and named
  (symptomEpisodeReference).
- OpenAPI regenerated.
2026-10-03 21:13:45 +02:00
Marc-André Bombeck eda5776886 Run document summaries and filing suggestions in the background
The summary and suggest routes accept `Prefer: respond-async`: after the
same quick refusals they answer 202 with a run id, and the background
document AI worker runs the read under two new run kinds
(DOCUMENT_SUMMARY, DOCUMENT_SUGGEST, migration 0370). Without the header
both routes answer synchronously as before; both paths run one shared
body, so they cannot drift.

The worker asks the capability for the record and re-checks the wire for
the provider it picks before every read. A summary that is to be stored
marks the document PENDING while queued, READY when it lands, and
UNAVAILABLE when the run fails or the write does not land. A second
request attaches to a live run only when it would produce the same thing.

The vault asks for both in the background and shows the same calm
waiting line the index uses, worded so a transient read never promises
that leaving the page keeps it. The egress guard and the route inventory
now hold every run kind to its dispatcher re-check and its capability.
2026-10-03 20:51:57 +02:00
Marc-André Bombeck cd3c0b7de2 Merge remote-tracking branch 'origin/main' into feat/v140-fixes
# Conflicts:
#	docs/api/openapi.yaml
2026-10-03 20:06:16 +02:00