Brings in the truncate retry, the socket fix for the record-fence spec that
failed this branch's second e2e shard, and the corrected absence assertions.
Six operation descriptions in the API document opened with a version and
a bracketed letter-number code from the notes the work was planned in.
A client author reading the document has no way to know what B1 or B6
mean, and they mean nothing outside those notes. The version stays,
since it says when the operation appeared.
The done screen's one-tap button posts `POST /api/consent/ai/web`, a path the
demo instance's edge allowlist refuses — correctly, since the demo is a single
shared account and a receipt one visitor minted would turn the operator's
provider on for everyone after them. The button was offered anyway, so a
visitor who took it got the generic "give it another try in a moment" toast on
the one screen whose whole purpose is not to say things it cannot back.
The route withholds the offer under `DEMO_MODE`, which leaves the panel in its
neutral form, claiming nothing either way.
The done screen folded the `unknown` provider verdict into `unhealthy` and
painted "This instance's provider is not answering right now." `unknown` is
an empty `provider_health` table, which is the state of every instance until
its first AI call — so the most common path through the screen made a claim
about something nobody had measured. It now falls to the neutral panel, which
says nothing either way; the offer is withheld in both cases as before.
The projection itself drew the same unearned conclusion from staleness alone:
a quiet instance whose last success was two days old read `unhealthy` without
any failure behind it. A verdict now needs an observed one — a row whose last
outcome was a failure — and a merely old success reads `unknown`.
The done screen's assistant panel stops claiming that the instance's
provider already serves the person. A tri-state read over the operator's
own provider ledger decides between a one-tap offer that mints the
consent receipt, a plain "not answering right now" note, and today's
setup link; anything unknown, any read error, a managed profile and an
operator who has switched the assistant off all fall to no offer.
# Conflicts:
# e2e/setup/global-setup.ts
A provider with no sync history reports "unknown" instead of "connected",
so a fresh account is no longer told a device it never linked is already
delivering; the connect step resolves the sync verdict into four honest
tiles, records the flow's result only for a connection that is actually
serving, points an unconfigured provider at its credentials, and steps
over a source the account already has.
Two structural guards caught the new gate. The error-code catalogue is derived
from the route tree on every run and fails in both directions, so a code the
server emits and does not publish is a client that cannot branch on it; the
session-surface guard freezes the short list of files under `src/app` that
resolve a caller without `requireAuth`, and the register route now belongs on
it — `requireAuth` is the wrong gate there twice over, since it would throw at
the anonymous caller the route exists for and this route wants the ABSENCE of a
session.
The code is `auth.already_authenticated` rather than the bare
`already_authenticated`: the catalogue's own header rules that new codes are
dot.case, and it sits beside `auth.mfa.code_invalid` in the same family. The
OpenAPI document renders the catalogue into the ErrorEnvelope description, so
it is regenerated.
The ledger's synthetic "no row" snapshot returned `state: "connected"`, so
the field's default meant the opposite of its name: every account that had
never connected a provider published `connected` on the status envelope.
Settings survived it by never reading `state`; the setup flow's connect step
did read it, and told brand-new accounts their wearable was connected.
The snapshot now reads `unknown`. `syncHealth.verdict` is unchanged and stays
the only liveness truth; the managed-record projection maps `unknown` onto
`disconnected` the way it already maps a missing connection.
GET /api/user/ai-provider gains three additive fields: the instance-wide
health tri-state, whether this caller already holds a receipt, and the one
boolean that answers 'may the shared provider be offered in one tap'.
The offer is the conjunction of five preconditions and every one of them
can only take it away: the operator's provider is the one that would serve
the caller, the health is demonstrably healthy, the assistant master and
coach flags are on, the acting record holds its own credentials, and no
receipt is on file yet. A managed profile is excluded by the positive form
of the policy test - 'not deny' would have let a child's record through.
Nothing here probes a provider; a probe would bill the operator for every
registration on the instance. docs/api/openapi.yaml is regenerated from the
schema source.
A Nightscout self-host on host networking with
NIGHTSCOUT_PRIVATE_ORIGINS=http://localhost:1337 worked before and must
keep working unchanged; an operator who lists an exact loopback origin
with its port has made the same deliberate decision as one who lists a
LAN origin. The never-grantable floor is now the unspecified address,
link-local and the metadata endpoint only. Loopback (127/8, ::1,
localhost) is grantable when listed exactly, still resolved and pinned
at connect time with redirects forbidden, and still refused when not
listed.
::1 is answered before the embedded-IPv4 unwrap, which read it as the
IPv4-compatible spelling of 0.0.0.1 and refused it as unspecified. The
pinned Nightscout transport test runs the real floor again. Each refused
range keeps its own test; loopback moves to the grantable-when-listed
set with a test that an unlisted loopback is still refused, and a
Nightscout grant of http://localhost:1337 is asserted to parse.
Refs #947
"Finish without the profile" completed the flow with no record id, and
the route then derived the child's answers onto the guardian's own
record. The server now decides: answers given for somebody else with no
record to apply them to complete and latch the flow without deriving or
seeding anything, whatever the client sent. The checklist carries the
profile instead and reads none of those answers as the guardian's own.
A record id the caller does not guard answers 404 like every
id-addressed managed-profile route, one answer for missing, not managed
and not theirs alike; the contract documents the field, the 404 and the
422 text. Pinned by two integration cases and a second child journey.
A loopback, link-local or metadata target answered
private_origin_not_approved with a message telling the user to have the
operator list it, and the parser refuses such an entry forever: a dead
end the route test even pinned. Those targets now answer
private_origin_not_grantable (catalogued, on all four routes, translated
in seven locales) with a message that says no grant can open them and
to use the relay's LAN address. Both senders carry the reason into the
outcome so the test button forwards the same code.
The malformed-entry warning printed the entry verbatim, which is where
a pasted token or userinfo sits; the parser now hands callers scheme
and host only, plus the reason in words. The Nightscout list throws a
named error with that redacted entry and reason, and the connect route
answers it as a 422 with the message instead of a bare 500, so a
self-host that listed http://localhost:1337 reads what to change.
The two settings cards translate a coded save refusal like the test
button does; the docs and the changelog name the sub-host and sibling-
port narrowing; the dispatcher floor tests assert the policy marker so
a real NXDOMAIN cannot pass for a refusal.
Refs #947
"Someone I look after" derived the child's answers onto the guardian's
own record. The completion now takes the managed record the profile
step just created, checks the same access rule the guardian's modules
route applies, and lands the same derivation and dashboard seed on that
record through the same record-keyed write; the guardian's own flow is
stamped complete and latched without touching its map. Three
integration cases pin it, and the child journey reads the child's map
through a real switch — it asserted the guardian's map before, which
pinned the wrong record.
Also from the review: the checklist's visit row read a meta the visits
list never publishes and now counts its body; the first-result tile
read the measurements list as an array and now shows the reading; the
demo allowlist admits the disclaimer and tour writes, with a structural
test over every onboarding route file; restart clears the old first
result, and the screen trusts the ledger before painting a result as
done; the headroom sweep's raw-key check matches the flow's own key
shapes; the demo migration script stops copying the dropped column; the
questions-to-pass rule and the shell's onboarding-path rule are pure and
pinned; the baseline form composes the shared action row; the schema
diff loses the unrelated formatting churn.
The mark named its action unconditionally, so a job that had already
named its own (the low-stock underflow branch) or a settings route
whose test then failed at the relay lost that name on the wide event.
Set the action only when the event has none, and always land the
channel and origin meta pair, through a small hasAction() on the
builder. Note the parse-time floor on the Nightscout page, which shares
the grammar.
Refs #947
Put the variable on the compose environment whitelist (without it a
value in .env never reaches the process), on both example files, and in
the env manifest so check-env reports it. The self-hosting notifications
page gains a section on notification targets on a private network: the
exact-origin grammar, what stays refused even when listed, and a table
of which target is refused where so an operator knows which lever to
pull.
Refs #947
The save routes evaluate the same policy the senders do. A target on a
private network the operator has not listed is a 422 with
meta.errorCode = private_origin_not_approved and a message that names
NOTIFICATION_PRIVATE_ORIGINS, instead of the generic "Invalid data"; a
listed private address saves although the plain public floor would
have refused it. The settings schemas take the target predicate as a
parameter so the validator module never reads the environment; the
exported constants keep the public floor for the OpenAPI contract.
The test routes forward the sender's refusal as the same 422 and code
instead of a bare 500, so the card can say why the test failed rather
than sending the operator to the wide-event log. A delivery fault is
still a 500. Both test routes and the dispatcher reach the same sender
function, and a structural test holds them there.
The OpenAPI descriptions name the code on all four routes; the test
button's error map gains the code in every locale.
Refs #947
The catalogue lists the answers and restart codes and no longer the
retired step ones, and the generated document quotes that list. The
skippable-step id type nothing read goes with it.
The route's description told a native client only that phase never leaves
the cycle routes. It now says what a grant scoped to the cycle alone
receives: the phase rows with no measurement or mood outcome columns and no
lagged discovery, the same as a record with nothing logged. The owner and a
whole-record grant are unchanged.
The catalogue's header read as a current count; it is history and now says
so.
The needs-based flow is the one setup flow now. POST /api/onboarding/step
and the six goal slugs go, and with them the two account columns the
wizard kept — its checkpoint and its goal list — through a migration
that drops them; onboarding_completed_at stays as the first-run
redirect's gate. The one durable effect the goals had, the dashboard
tile order, moves to the completion route: the answers now carry both
halves of Q2, which modules are on and what sits on top, and the seed
keeps the wizard's one contract of writing only while the layout column
is still unset.
The OpenAPI table loses the step path and the completion's description
says what the route does today; the demo-allowlist test names a
mutation that still exists.
Two branches each added an onboarding surface to the catalogue; the object literal carried the key twice and the second entry hid the first from the guard.
The write-level row listed "a vaccination dose and the booster reminder it
suggests" three paragraphs under a section table that puts the vaccination
record in Health background. Read together, that tells a self-hoster a
health-background WRITE grant can plan a booster — which the route beside it
now refuses, because the reminder it mints lives under Readings. The row lists
the dose alone, and the exceptions paragraph, which is where a reader looks for
exactly this, carries the two-section case and says the dose logs either way.
With it, the small things the same pass turned up:
The cycle-insights fixture asserted a property it could not show. Its docblock
claims the cycle-native answer survives the fence, and it seeded no symptom at
all, so `symptomPatterns` was empty in every leg and a regression that emptied
it along with the foreign columns would have passed green. One catalogue
symptom on the luteal days, asserted on the owner leg and on the cycle-only
one.
The immunization rows matched on a bare `role="button"`, which passes vacuously
the day the card becomes a real button and fails for the wrong reason if
anything else in the subtree grows one. They carry the same stable marker the
visit card took.
`/api/cycle/calendar` reads the same wrist-temperature channel the crosstab
does and is deliberately not fenced, and it was the only one of the pair with
nothing saying why: no value from it reaches the response.
The encrypted export's new 403 documented the switch refusal and not the
narrow-scope one it answers first, unlike the two settings operations beside
it. Both sheets say at the top that their Save and Delete are gated at the
caller, so a third mount reads the constraint before it violates it. And one
docblock line ran past the wrap.
Four of them. The confirm re-stamped its completion instant on every call, so
a replay quietly moved the moment the setup finished at while the derivation
beside it was carefully latched. The two list answers were stored exactly as
sent, duplicates included, so the column could hold something the reader would
never return — the reader de-duplicates on the way out and now so does the
write. The wide-event name for saving an answer used a different noun from its
two siblings, which makes a dashboard filter awkward for one feature. And the
contract said the restart body was required while the route accepts none.
The account payload resolved the setup state for the actor while its module
map, its cycle flag and every count the checklist reads beside it answer for
the active record. A guardian looking at a child's dashboard therefore got the
child's rows ordered by their own answers, and the child's reading count
tested against their own flow state. The two halves of that component have to
agree about whose record they describe, so the field is record-scoped now,
like the module map next to it.
Under a grant narrowed to one section the answers come back empty, for the
reason the module map is masked: they name the domains the record tracks, and
a grant that opens readings does not open that. The shape of the flow still
comes through.
The three write routes stay actor-only and say why: a delegable declaration is
a reviewed entry in the sharing surface guard, and a read that is wider than
its write is the honest shape while that entry is still to come.
The setup flow writes a record's module map, its glucose unit and its unit
preference. All three have a dedicated settings route that records an audit
row, and the flow recorded none, so "why did my modules change" had no answer
in the activity panel. Restarting the flow did write a row, which made the gap
look like an oversight rather than a decision, because it was one.
Same event names as the dedicated routes, since it is the same column and the
panel should group them. The module row names the keys that actually moved and
the domains kept for their content.
The only gate on the derivation was Q1 being set, so one answer was enough to
turn the whole questionnaire into a module map. Two ways that landed wrong and
they compound. Somebody answers who the record is for, closes the tab, and
later finishes the legacy four-step wizard, which posts to this same endpoint
and knows nothing about these questions: the five unanswered ones each read as
their conservative default, eleven surfaces are written off, and the
once-only marker makes it unrepeatable. Or any client patches one answer and
posts the confirm straight back.
The gate now asks whether every question has an answer or a deliberate pass.
A half-answered flow derives nothing, is not stamped complete, and keeps its
confirm step pending, so finishing the questions later still does the work.
The legacy arm is untouched: a caller with no setup row still answers the
fixed acknowledgement it always did.
The spec's sixth question is skipped when the account already carries the
preferences, and the flow had no way to know: `User.unitPreference` is
nullable with null meaning "default metric", and the account payload coerces
that away, so nothing downstream could tell a chosen metric from an unset
column. The two columns are read raw on the server now, into the same
vocabulary the wire publishes, and a record whose account holds both reads its
`units` step as done — the question answers itself and is never put again.
Read-side only: the stored ledger keeps saying pending, because the person
really has not answered it, and an answer given later still lands.
The two writes the units answer makes — the display columns and the record of
the answer — go in one transaction, so a failure between them can no longer
leave the columns changed with no record of what changed them.
Measured against the route declarations on this tree:
The health-background section names allergies, family history and the profile
facts, and omits visits, the practices behind them and the vaccination record —
all three of which `scope.ts` puts in that section, so a grant is narrower on
the page than it is in the product.
"Aggregate surfaces appear only on a whole-record grant" is true and
incomplete: a whole-record grant does not open them equally either. The daily
digest and the dashboard's snapshot answer at read, the health score, the
dashboard's summary and the insights need manage.
The write level lists seven creates against twelve WRITE declarations. A visit,
a practice, a vaccination dose and the booster reminder it suggests were
missing, and the guide said nothing about the creates that need manage inside a
section a write grant can otherwise add to, or about the vault taking no
delegated write at any level.
"Exports, the health-record archive" sat under what a delegate cannot reach,
while `POST /api/export/health-record` answers at manage on a whole-record
grant and its own comment says why. The row is about the full exports and the
encrypted archive; the doctor report is named as the exception it is.
The sharing-denial guard's docblock carried the same class of stale claim — 116
fenced modules, 26 never published — and that gap is closed on this tree. The
caveat becomes an assertion: a new leg recomputes the fenced modules from the
route sources and requires every one to be published, and it names no number,
because a hard-coded figure is what turned the paragraph into a false statement
about the contract. It fails when a published path is withheld from it.
`POST /api/mental-health/assessments` is fenced on `mind` and writes one row
into `measurements`: the instrument's total, minted `COMPUTED` and keyed on the
administration it came from, so the trend reads through the chart and rollup
tier without the encrypted item answers going anywhere. A grant scoped to mood
and mind therefore adds a point to the owner's measurement series, and nothing
said so.
Left in place rather than fenced. Withholding the projection the way the
booster mint is now fenced would file a screener whose own total is missing
from the record — an inconsistency inside the section the grant does open, to
avoid a derived point in one it does not. So it is documented instead, in the
three places it is decided or agreed to: the declaration, the section's own
definition, and the consent copy in all seven locales, which is where a
widening belongs. The published operation says it too.
`POST /api/vaccinations/{id}/booster` is declared `("write", "profile")`, and
that is right for the act — filing the Impfpass and arming the booster it
suggests are one thing to the person doing them. The row it writes is not: a
`MeasurementReminder` belongs to the measurements section, every direct route
over the model declares it there, and creating one needs MANAGE. So a WRITE
grant scoped to the health background alone produced a reminder on the owner's
checkup list that the delegate could neither read nor change through the
section that owns it.
The seam is fenced the way the visits service fences a checkup closure, with
one difference stated at the call site: this refuses rather than skips. There
the closure is one effect of a larger save and reporting it as not done keeps
the save honest; here the reminder is the whole request, and a 201 over a row
that was never written would tell the person their booster is planned while
nothing rings. The prompt that raises the call asks the same question client-
side, so a delegate outside the section is not offered it in the first place;
the dose itself still logs either way.
`vaccination-booster-scoped-delegate.test.ts` drives the owner, a delegate
holding both sections, a profile-only delegate and a whole-record one. The
scoped-refusal leg fails against the previous handler, which answered 201 and
left the row.
Additive: `onboarding` carries the nine steps with their status, the answers as
given, the flow's own completion stamp and the one task it offered. Always
present — a record that never entered the flow reads as empty answers with nine
pending steps, so a client branches on the statuses rather than on whether the
field exists, and an unknown step id is skipped rather than refused.
It is resolved for the CALLER's own record rather than for the active one,
unlike the two module fields beside it. The three write routes are actor-only,
and publishing a setup state the browser cannot write would be a read it could
act on and never complete.
Both ends land here, because a field with no reader is the failure this
repository keeps rediscovering. The getting-started checklist orders its rows
from the answers — medication first for somebody who takes one daily, the
data-source row first for somebody who named a wearable — and it stays on screen
while the flow is unfinished. That is the second half: five readings were never
evidence that the setup finished, and now a pending step, an unconfirmed set of
answers or an offered task that produced nothing keeps the list where it is.
The ordering only applies once the answers are confirmed. Re-ordering the
dashboard under somebody who is still mid-question would be movement they did
not ask for.
The three routes that act on the whole of an account — `DELETE
/api/settings/data`, `DELETE /api/settings/account`, `POST
/api/export/encrypted` — name no record. They resolve the caller and act on
whatever the caller owns, so the question a switched session puts to them is
what happens when the browser believes it is somewhere else.
They already refuse: all three resolve through `requireFreshMfaIfEnrolled` or
its erasure sibling, and both open with `requireAuth()`, which reads the acting
carrier and throws `sharing.not_permitted` for anything but `none`. What was
missing is that the contract never said so. `/api/settings/{data,account}`
published a 403 about narrow Bearer scope alone, and the encrypted export
published no 403 at all, so a client that switches records had no way to learn
which of its calls stop working short of trying one.
One sentence, spliced onto the three, byte-identical for the same reason the
delegable refusal is one string. `switched-session-destructive-refusal.test.ts`
drives each route with a live whole-record MANAGE switch and asserts both
halves: the status, the code and the wording, and that neither record moved —
a refusal raised after the transaction would satisfy the first alone. The
export leg additionally asserts no archive rode along with the 403.
The badge read the enabled flag plus a DSN that parses. Neither says anything
about the target accepting a report: a wrong public key, a wrong project id and
a host that stopped answering all parse, and every one of them showed green
with a line naming the host. That is the false reassurance an operator finds
out about when they go looking for a crash that was never reported.
Nothing in the tree recorded an outcome, so there was nothing to read. The
sender now stamps one next to the DSN it used — instant on a success, a short
classification on a failure. Never the response body: that is text from a
remote host and has no business in this database or on this page.
The badge is green only while a report has left inside the last fortnight,
amber while a target is set and nothing has come back, and it names the reason
underneath. The clock lives with the ledger, so the verdict is resolved in the
route and the card paints what it is handed. A failure that a later success
superseded is not shown at all. Pressing Test records like any other send, so
the button proves the target on demand.
On the release that adds the ledger the table is empty, so on a host that has
been uploading nightly for months every account read Never, under a sentence
saying no copy had ever reached the bucket. That sentence is untrue on exactly
the morning an operator opens the new card for the first time, and it is a
claim the host cannot make: what is empty is the record, not the bucket.
The run now records every account it walks, not only the ones an object came
of. A walk that produced nothing moves the attempt instant and leaves the
success columns alone, so the two cases separate: no row at all is no record
yet, a row without a success is never. Never now means what the word says —
a run looked at this account and has never put anything there.
The card names the last check on the never row, so the state is a fact with a
timestamp rather than an assertion. Runbook and all seven bundles carry the
same two sentences. The integration file proves both arms against real
Postgres, including that a failed run leaves the previous good copy on the
row.
The threshold was the schedule period with no budget on top of it. Two things
spend that budget on a host where nothing is wrong. The cron runs 02:30
Europe/Berlin, so on the DST fall-back Sunday the gap between two runs is 25
hours and every account on every host would cross one period on the same
morning. And the ledger instant is when that account's object landed, by
design, so on a cohort walked one account at a time an account reached an hour
later tonight than last night drifts past 24 with two clean runs behind it.
The schedule period and the verdict threshold are now separate numbers. The
period is still what the card quotes; the threshold is the period plus six
hours, applied to both steps, so due still means one run produced nothing and
stale still means two. The fall-back night is pinned in the unit file by its
real dates rather than by an arithmetic 25, and a genuinely missed night at 31
hours still reads due. The runbook says why the slack is there.
There is no default error-reporting DSN in this tree and there should not be:
a self-hoster's crash reports must not leave their host because nobody told
them not to. That part was already right, so nothing here adds a default.
What was wrong is what the console said about it. The GlitchTip card showed a
green "Configured" badge as soon as a DSN existed, whether or not the switch
that actually sends anything was on, and it never said where reports go. An
operator reading that badge has been told their errors are being collected
somewhere, which may be true of neither half.
The badge now tracks reports actually leaving, and one line under the switch
states the case: the host they go to, or that they stay here, or that tracking
is on with no target. The host only — the public key in the DSN answers a
different question. The deploy runbook says the same thing for a
maintainer-operated instance.
The nightly off-host job reports counts — uploaded, failed, total — and a
count cannot name the account it is failing for. A cohort where one object
has been refused every night for a month reads as "99 uploaded, 1 failed"
each time, which looks like weather rather than like an account with no
disaster-recovery copy at all.
The worker now writes a ledger row when an object lands (account, instant,
size), and Admin → Backups gains a second card that rates every account
against the nightly schedule: fresh inside one run, due when one run
produced nothing, stale when two did not, never when no object has ever
landed. On a host without BACKUP_S3_* the card says off-host backup is not
configured and stops there.
The verdicts are computed from the ledger, never from a bucket listing: the
worker's grant is deliberately PutObject + GetObject, and an admin page must
not put the operator's credentials on its render path to state a number the
database already holds.