Two requests can probe the same workout, both find no heart-rate series,
and both mean to attach one. The unique index on `workout_id` picks a
winner; the loser has to report the duplicate it turned out to be rather
than claim an attach the database never made. Nothing could catch that
arm going wrong: the unit fake echoes every row back as landed, and the
race suite carries no series at all, so replacing the status with a
constant `enriched` left the whole suite green. Two concurrent posts of
the same enrichment now assert one `enriched` against one `duplicate`
and a single stored series.
The published contract also says now what the counters mean, instead of
leaving it in a comment: an entry reported as `enriched` counts towards
`duplicates`, because the top-level counters count workout rows and an
enrichment writes none. Field descriptions on the response and on the
per-entry status and reason, OpenAPI regenerated.
Marking an older missed dose as taken from the Verlauf pinned the
right slot but stamped the intake with the current moment. The body
builder is a pure helper now, anchored on the slot instant, matching
both the optimistic paint and the skipped-to-taken flip that already
behaved this way.
Two entries carrying the same external id and both a `samples` array
reached the enrichment arm one after the other, and both read the same
landed workout id back, so the response claimed two attaches where the
database had written one. The second entry now takes the duplicate arm:
a workout is claimed by the first entry of the batch that reaches it.
A batch entry matching a stored workout stays first-write-wins for the
workout row. What it can do now is hand over a heart-rate series the
stored workout does not have yet: the series is attached, the row itself
is not written at all, and the entry reports back as `enriched`. A
workout that already carries a series is a no-op, so a client can walk
its whole history as often as it likes.
- The pre-flight probe now answers both questions at once, whether the
row exists and whether it already has a series, and carries the id so
the attach addresses the stored workout without a second round trip.
- The attach rides the same child write as a fresh workout's series.
RETURNING names the rows that landed, so a workout that gained a
series between probe and write reports as the duplicate it is instead
of claiming an attach that never happened.
- A `samples` array that fails validation is refused per entry with
`reason: "invalid_samples"` rather than failing the batch. One
unusable session out of a hundred must not stall a history sweep the
client cannot repair by retrying; every other validation failure keeps
failing the batch.
- The wide event and the audit row tally enrichments. Counts only, the
series never reaches either.
- `enriched` is the one contract movement: additive on the per-entry
status, request shape unchanged. OpenAPI regenerated.
Integration coverage against a real Postgres pins the attach, the
repeat as a no-op, the refusal of a bad series alongside entries that
land, a mixed batch reporting each status, and the stored workout row
compared whole before and after, `updated_at` included.
The page ended on a "Why?" button that opened the coach with a
prefilled question about the session. It leaves on the maintainer's
call; the coach remains reachable from its own surfaces. The two
strings it carried leave all six locales with it.
The dead-code sweep found the window type exported and unread. It was
unread because the surface that offers the choice carried its own copy
of the same three numbers: the chart could not import the calculator's,
since that module reaches the database client two hops down and a client
component pulling it in would ship Prisma to the browser.
Both now come from the dimension table, which is pure by construction
and which the chart already imported. One declaration of 7 / 30 / 90,
read by the calculator that averages over them and by the control that
offers them, so the two cannot drift.
Three small things the audit named together.
The mood reads spread the whole row, so adding the wire keys shipped
every level-A value twice — once as the column name and once as the key
the write path accepts. A client would have picked one and been stuck
with it. The column spelling is stripped now, and the create response's
key set is asserted so it stays stripped. The published contract is
unaffected either way; these routes are not in it.
The dimension series carried its own copy of the day-key subtraction
that the freshness module already exports. One copy now.
And the reason a stale dimension ships with a notice instead of being
dropped the way the status surfaces drop one is written at the builder:
a one-line card has nothing left to say about a value it cannot narrate,
while an open conversation can say when the value is from — and silence
there would leave no way to tell an unanswered dimension from one nobody
has looked at in a week.
The snapshot stamps every metric block with an asOf built from the
newest reading in it, which for mood is the newest ENTRY on the 1-5
axis. Somebody who taps a face each morning and opens the sliders once
a week therefore carries a block stamped as current and a stress
reading six days old, and a reader taking the block stamp as covering
the dimensions states last week's answer as today's.
The block now names the precedence in one line beside the dimensions,
and each dimension keeps its own age. Present only when there are
dimensions to explain.
An entry always carries a five-point label, so it always implies a
pleasantness value: the create path derived one from a null and the
capture sheet still showed a clear control for it, which meant a button
the server undid on the way to the database.
One rule on both routes now — a null asks for a value that cannot exist
and gets the derivation, on create and on edit alike — and the control
is gone for that field. The other four are genuinely optional and keep
theirs.
Matches what syncVersion, syncedAt and the row stamps already do a few
lines above: a key the file does not carry is a key the file predates,
not a key the file says is empty. Reachable when one file addresses the
same row twice, where the second entry now adds without erasing what
the first one set; an explicit null still clears.
Worth stating plainly, because the audit that asked for this expected it
to protect live rows from an older file: it does not, and it cannot. The
restore deletes the owner's whole mood partition before it rebuilds, so
by the time the update arm runs there is nothing live left to protect. A
test now pins that contract too, so nobody reads the omission as a
guard it is not.
The single create's upsert restated all five columns on its update arm,
so a client re-posting an entry with only a label and a timestamp — the
phone, whose build has no sliders — cleared the stress, energy,
connectedness and stability somebody had answered on the web. The batch
and import paths already preserved them, so the two disagreed.
One answer now, and it is preserve: absence in a request means nothing
to say, an explicit null takes an answer back, and pleasantness still
follows the label because the request always carries one. The create
schema accepts the null that makes clearing expressible.
A re-post that omits a dimension is asserted to leave it standing, and
that assertion has been watched to fail against the old arm.
The inverse flag is read by the trend legend and by the assistant block;
the function that flips a value through it had no caller. It comes back
with the surface that needs a combined view, not before.
The pillar reads PHQ-9, GAD-7 and WHO-5 and cites the papers behind
every threshold. A daily self-rated slider is now available on nearly
every mood row, so leaving it out reads as an omission unless the
reason is written down: the two are different kinds of evidence, and
averaging them would weaken the line that makes the figure worth
showing to a doctor. Recorded at the pillar and at the columns.
- the level-A blocks ride the mood aggregate the coach and the briefing
already share, so the ages are fixed once in the data preparation
rather than once per sentence
- every block carries its newest value, the day it belongs to, how old
it is and whether that age still backs a present-tense claim, through
the same freshness rule the vitals surfaces use
- a stale dimension carries the instruction in words as well as in a
flag, because that is the sentence this exists to prevent
- the 1-5 scale line is untouched; the new axis describes itself, with
the anchors resolved from the bundle the person read
- the trend page and the assistant run the same calculator, so a number
in a sentence cannot disagree with the number on the chart
- one engine: the dimension series is a sibling calculator of the mood
aggregate hub and rides the same bounded read, so no second summary of
mood can disagree with the first
- 7 / 30 / 90-day means per dimension, plus the daily series behind them
- a dimension nobody answered is named as not recorded rather than drawn
flat through the middle of the scale, and a zero counts as an answer
- each line keeps the orientation the person answered in; the legend
says which way the inverse one runs
- the card rides the existing module-gated aggregate read and query key
- the five faces are untouched: pick one, save, done in a tap
- a closed section below them opens onto five sliders; picking a face
suggests a pleasantness value and moving that slider takes the
suggestion over, while the face selection stays where it is
- an untouched slider writes nothing, and clearing one takes the answer
back rather than leaving a middle value behind
- the list's edit dialog shows and changes the same five, so nothing is
written on one screen and invisible on the other
- correcting an entry no longer re-derives pleasantness when the label
did not move, which used to overwrite a hand-set value on any edit
- factor ratings are no longer offered on a new entry and nothing was
deleted: existing ratings still render, edit and feed the crosstab
- six locales, and the dimension table's keys are declared dynamic
- Slider wraps the Radix control the way Switch and Progress already do;
no new dependency
- SliderField adds the label, the value and the two scale anchors, which
are what make a 7 mean anything
- a null value renders as unanswered rather than as the value the parked
thumb sits on, and zero renders as the answer it is
- the announced text carries the scale, not only the number
- the backup select and the payload carry all five, on both the portable
and the disaster-recovery arm
- the restore writes what the file holds and nothing else: a file from
before these columns restores them empty rather than deriving them
- the CSV appends them after the existing columns so nothing shifts under
a spreadsheet, and an unanswered value is an empty cell rather than a
zero, which is a real answer on this scale
- the round trip asserts each of the five by value, including a zero
The resolver queried the online provider first and read the mounted
GeoLite2 databases only when that missed. Every document describing the
feature said the opposite, including the self-hosting runbook and the
admin notification that tells an operator their login IPs are leaving
the server. So an operator who mounted the databases in order to stop
that egress kept all of it, and nothing said so: the location resolved,
the admin overview filled in, and the only difference was where the
address had been.
Now the local databases answer and the provider only ever sees an
address they could not place. A host with the City database mounted and
a hit makes no request at all — including when the ASN database is
absent, because reaching out for a carrier string would send the address
off the host anyway, which is the thing the mount was for.
IP_GEO_LOOKUP_DISABLED=1 remains the full off-switch: it disables the
online step only, so an offline hit still resolves and an offline miss
returns nothing rather than going online.
The wide-event warning on the fallback path named ipwho.is even when the
operator had pointed IP_GEO_LOOKUP_URL somewhere else; it now names the
host the runtime actually queries. The admin notification copy needed no
change — it fires only when the databases are absent, and then every
lookup does go online.
The two cases in geo-asn.test.ts that pinned the old order are the
watched red for this change.
The admin "Delete all data" action carried its delete list inline. That
list named nine tables, which was most of the schema on the day it was
written, and it stayed at nine while the schema grew to a hundred and
twenty-two models. Laboratory results, documents, cycle logs, journals,
mood, workouts, sleep and Coach conversations all survived the wipe an
operator runs before handing a box on, and the toast said it was done.
The per-account wipe had the same defect and grew a derived plan for it.
The admin route now reads that plan: ADMIN_WIPE_MODELS is WIPE_MODELS
minus a single documented subtraction, so a model added to the schema
lands in both wipes at once.
- AccountGrant is the one difference, with its reason in the plan: it is
the only access path to a managed profile, and this action preserves
accounts by definition. Deleting every grant would leave records
nobody can open, administer or delete.
- The audit history now goes with everything else. It is a record of who
did what from where, which is exactly what clearing a box is for; the
receipt written after the transaction commits replaces it.
- One transaction with the same budget as the account wipe. A
half-applied wipe is worse than none, because the person is told it
succeeded either way.
- The result line reports rows and tables instead of nine fixed names,
which is how nobody noticed: it read the same whether the wipe had
cleared nine tables or ninety.
Guards, both watched red before landing:
admin-data-wipe-completeness.test.ts fails when a user-scoped model
reaches neither the wipe nor a written exemption, and when the route
names a table inline again. The integration guard seeds every table in
the schema for two accounts, runs the route, and asks the database what
is left — drop one model from the plan and it names the surviving table.
- one helper resolves the level-A values for a write; the five writers
call it instead of carrying their own copy of the derivation
- the create schema takes a1 to a5, and a value the user set wins over
the derived one; the batch request shape is untouched
- both arms of both upserts restate the values, so a re-post cannot
leave a stale reading beside a mood that has since changed
- the JSON import derives from the row's own label rather than the
file's score, which is the one place the two can disagree
- correcting an entry's label re-derives pleasantness the way it
re-derives the score; an explicit null takes an answer back
- a round trip through all five writers against real Postgres asserts
the rows agree and that nothing invents the other four values
- MOOD_DIMENSIONS carries the five self-state dimensions in capture order,
each with its column, its label and anchor keys, its orientation and its
0-10 bounds
- getA1ForMood maps the five-point label onto the pleasantness scale once,
so the quick check-in stays a full-value capture path
- only stress is inverse-oriented, and a test pins that count
- the secret scan reads a dotted message key on a *Key field as a token;
allowlist the dimension keys the way the existing message keys are
Measuring the seven `/settings/layout/<child>` routes was meant to pin the
back-link spacing. It found there was no back-link to pin.
`SettingsHubBackLink` and `settings.sections.layout.backToHub` have existed
since the sub-pages were split out, and the only thing that mounted them was
the shell's slug FALLBACK — the branch that runs when a page passes no
`heading`. These pages pass an explicit one, so the fallback never ran and no
child route has painted a back-link since. The component was written, the
string was written, the reverse-coverage guard saw a call site and was
satisfied, and the way back to the hub was not on the page. Two ends, no pipe.
It now comes from the page, where the heading it belongs to comes from.
The geometry spec grows the seven routes and a second family. A route with a
back-link carries an extra row above the title, so it is taller by
construction; comparing it against a plain route would report structure as
drift. Each family is checked for its own constant heading top, and the step
between the back-link and the title is asserted at 6 px — `PageHeader`'s
documented `space-y-1.5`, which is what the shell's hand-rolled `space-y-6`
would have drifted back to.
Measured, both languages: plain routes 24.0 px, back-link routes 51.0 px,
spread 0.0 px in both families, step 6.0 px on all seven.
The back-link is also painted twice, once per breakpoint, so the measurement
picks the copy with a box — `querySelector` returns document order, which on
a desktop width is the hidden mobile one, and reading it reported no
back-link at all.
Every item here is a check that could pass while the thing it checks is
broken, or a margin the sweep's codemod could not reach.
Guards:
- The access-payload matcher accepted `.level` on ANY object. A chart's zoom
level or a log record's level would have satisfied it and reported a
consumer that does not exist. It now names the bindings the entry actually
arrives under; a third binding is a deliberate edit rather than a silent
widening. Re-falsified: rename both readers, it goes red.
- The `pl-7` sweep skipped any line CONTAINING `//`, so a class string sharing
a line with a URL was waved through. Only the part after a real line-comment
marker is exempt now. Proven by putting `pl-7` on a line with an
`https://` comment: caught.
- `LEGIT_IDENTICAL` in the English-leak guard had no staleness check, unlike
both of its siblings, and carried two dead keys — one from a removal long
before this work, one from the duplicated version fields that came off
`/admin`. Both pruned, the check added, falsified with a made-up key.
- `split-description.mjs` split inside `z. B.`, `d. h.`, `u. a.` and `p. ex.`
and produced a head ending mid-abbreviation. Both halves have to be caught,
which is why the rule is a shape (single letter + stop) rather than a list,
and the merge re-tests its own tail. Exercised against eight cases including
the ones that used to break.
Margins the codemod could not see, because the child lives in a different
component from the card that spaces it: six form roots under managed record
settings (one of which also carried the banned `space-y-5` step), the
low-stock status line, the avatar card's explainer, and the backups restore
report. Each was adding a margin on top of the card's own gap.
Two facts the text diet cut too close, both restored inside the guideline:
that an excluded injection site wins over a medication that prefers it, and
that the privacy card's second button leads to a full health record.
The gate the diet shipped with matched `<SettingsCardHeader` in three
component folders. That is not where every card description lives, so five
real ones were outside its reach — including the full backup's, which the
sweep's own shortlist had named and which stayed at 351 German characters
across three sentences while the guard reported zero violations.
Two ways in were missing. A card header can sit in a folder the walk never
entered (`labs`), and a description can be FORWARDED to the header by a shell
— `ExportCardShell`, `ImportCardShell` — where the prop is on the shell's tag,
not the header's. The gate now matches both, over seven roots, and the
"finds what it is meant to check" floor moved with it.
`SubPageShell` is deliberately not included, with the reason written at the
list: its description renders as `text-foreground text-sm`, the body tier,
where the standards allow prose. Gating it would enforce a rule §3 does not
make for that tier.
The five, all six locales:
- full backup — the twelve-item enumeration was the description. It is now
the card's opening body paragraph, where a list of what is in the file
belongs; the description is one sentence.
- local OCR — the how-it-works sentence stays in the slot; "less accurate,
review every value" is safety copy and reads foreground in the body.
- CSV import, dose-history import — second sentence to the body.
- the ECG page — the "does not interpret the waveform" claim gets its own
paragraph rather than riding the page explainer.
Proven by reverting the full backup's text: the widened gate reports it as
both two sentences and over length, at the call site, in both languages. The
old gate could not see the key at all.
Both ends of the divider removal were green and the pipe between them was
never run. The `<hr>` came out of every integration card, all three unit
tests were updated to expect none, and this browser spec kept asserting nine.
It is the same shape as the failure this project keeps rediscovering: two
correct halves and nothing exercising the assembly.
The cost was not just a red spec. The Pixel-5 horizontal-overflow assertion
sits AFTER it in the same test, so `/settings/integrations` had no proven
mobile-overflow check for the whole sweep — the test never reached it.
The second assertion read a sentence's bytes and broke on the text diet while
the thing it was about — the card rendering its explainer at all — never
changed. `SettingsCardHeader` now names its description slot
(`data-slot="settings-card-description"`) and the spec targets that: the slot
is present, non-empty, and not a raw i18n key. The product name stays
assertable because a proper noun is not prose. The wording itself is pinned
where it belongs, in the unit test that reads the same bundle it renders from.
Every interactive path, every import, MCP and Telegram checked an incoming
measurement against VALUE_RANGES before storing it. The provider sync writers
did not, so a decode slip or a glitched sensor landed a row the application
itself calls impossible, and every mean, median and personal band derived from
it inherited the impossibility. Filtering the read side hid the row. It kept
landing.
The gate is one module reading the domain the app already declares, applied at
two kinds of seam. reconcileExternalMeasurement is the shared identity writer
for Polar, Oura, WHOOP, the Withings sleep leg, the Apple export importer and
the batch endpoint, so gating there means a provider added later inherits the
refusal rather than having to remember a check it never knew about; the new
rejected_range verdict carries no row, which is what makes the compiler force
every caller to handle it. The five writers that hold Prisma themselves gate at
their entry, before the existence probe decides insert versus overwrite, so an
impossible reading can no longer replace a good stored value.
An out-of-band value is dropped. Not clamped, because a clamp invents a reading
nobody took. Not stored behind a flag, because a quarantined row is still a row
and every consumer would have to learn the flag. Not fatal to the batch, because
one bad sample must not strand the run's other readings behind a held watermark.
Absence reads as absence.
A silent drop, though, is indistinguishable from a provider that sent nothing,
so every drop is tallied on the wide event under measurement.range_rejected with
the metric, the provider and the edge it broke. Counts only: the reading itself
is health data and never enters the log line, and the direction plus the
metric's own declared bound is what a diagnosis actually needs. The tally
accumulates rather than replacing, because a provider gates once per resource
leg and the reconciler once per row. One warning per provider lifts the event to
warn so a drop does not hide inside a green line.
A structural guard enumerates all twenty measurement writers with the reason
each is allowed to write, so a new one has to answer the question before it
ships. scripts/diagnose-implausible-measurements.ts reports the rows already
stored, aggregated per metric and source with no value beyond the bucket's own
extremes. It repairs nothing: whether a stored impossible row is a decode slip
or a band that is too narrow is not a call a script gets to make.
Thirty-four routes, both consoles, English and German, desktop and phone.
The claims this program makes are geometric, so they are checked with
geometry rather than by looking at screenshots.
What it found, which is the reason it exists:
- `SettingsCard as="section"` sat one spacing step tighter than its
neighbour on every desktop width. The `<Card>`-backed variant inherits
`gap-4 md:gap-6`; the landmark variant wrote a flat `gap-4`. Seventy-five
of eighty-six cards measured 24 px and eleven measured 16 px, directly
above and below each other. The expected value in the spec is a function
of the viewport for exactly this reason — a constant would have agreed
with whichever half it was written against.
- One German page subtitle wrapped to a second line where English fit on
one. Shortened in all six languages.
After both: heading top 24.0 px on every route, spread 0.0 px, in both
languages and at both widths — that is the no-jump claim, measured. Card
header-to-body distance equals the card's own gap on all 86 cards, and no
card body sits off the card's inner edge.
The header's HEIGHT is pinned only where one line is achievable. At 393 px a
70-character sentence needs about 380 px of column, so most explainers wrap
there; the spec asserts the wrap away at desktop width and only forbids a
third line on a phone. The alternative is truncating the sentence, which is
the defect this program removed.
A lab runs its own method on its own device, so the window a physician
evaluates a value against is the one printed next to it on that report,
not a catalog band averaged over every lab. Reading a report pulled the
value and dropped that range: the extraction asked for it, the commit
handed it to the biomarker resolver, and the resolver only uses it when
it mints a NEW marker. For every analyte the user already had — the
normal case after a first panel — the printed range was read and then
thrown away.
Add `sourceReferenceLow` / `sourceReferenceHigh` / `sourceReferenceText`
to `LabResult` and carry the range from the report to the reading. The
text is stored whether or not numbers can be read out of it, because
"bis 5,0" and "negativ" state a window too; a range that could not be
parsed is kept verbatim, never replaced with invented bounds.
`resolveEffectiveReferenceRange` is the one place the precedence lives:
the source window wins for that single reading, and the catalog stays
the net for readings without one. A printed string with no derivable
bound does not displace the catalog. Every surface that judges a
reading routes through it — the API verdict, the doctor report, the
insights blocks and per-marker assessment, the coach snapshot, the MCP
read and the FHIR `referenceRange` — so a value cannot read in range on
one surface and out on another.
The API keeps `referenceLow` / `referenceHigh` as the window in force,
so a client that reads only those two renders the right range and the
right verdict with no change of its own; the catalog band, the source
window, its origin and a divergence flag ride alongside for clients
that want to show both.
Read side: the chart draws the source window as a second, visually
distinct band with the reading it came from named beneath it, the
reading rows name the printed range and the catalog band where the two
disagree, and the doctor-report PDF prints the report's own string with
a footnote naming the saved range. Entry side: one optional field on
the lab form, folded away until asked for, and the printed range is
editable on the scan review screen.
The range parser covers the notations German and English reports
actually print — two-sided, one-sided, comma or dot decimals, thousands
separators, trailing units — reads units without ever converting them,
and refuses to guess on anything ambiguous.
`moodEntrySchema` gains `note`, `noteEncrypted`, `tz`, `syncedAt`,
`syncVersion`, `createdAt`, `updatedAt` and `structuredTags`; every one of
them optional or defaulted, so a backup file written before this parses
unchanged and restores exactly as it did.
The restore writes them. The note follows the measurement pair: ciphertext
verbatim when the file carries it, a portable file's plaintext re-encrypted
on the way in, legacy column null either way. `tz` absent still means the
legacy Europe/Berlin reading, which is what those rows meant. `syncVersion`
comes back at the value the file recorded — restoring at the default handed
the next round to any paired device holding a higher number, and the device
would then overwrite the recovered entry with what it had.
The link rebuild re-creates both arms: rated factors from `factors`, ticked
tags from `structuredTags` with a NULL rating. An unresolvable tag key is
reported under its own `moodTag` family, beside the rated one, because an
operator needs to know which of the two an entry lost.
The disaster-recovery payload dropped the free-text note, the row's
timezone and the last-writer-wins counter, and the tag-link read filtered
`rating: { not: null }` — which is the definition of a BINARY link, so
every present/absent tag a person picked was missing from the file too.
Both arms now mirror the measurement pair: the recovery arm carries the
note ciphertext verbatim plus `tz`, `syncedAt`, `syncVersion`, `createdAt`
and `updatedAt`; the portable arm carries the decrypted note, `tz` and no
ciphertext.
BINARY keys ride in their own `structuredTags` array rather than as a
nullable rating inside `factors`, because a restore resolves `factors`
against RATED tags and names what it cannot resolve — a rating-less entry
there would collide with both. Absent means empty, so a file written
before this parses and restores exactly as it did.
The two link arms partition the links exhaustively: a malformed link comes
back named as unresolvable instead of falling between them.
The mood read was an inline `include`, so nothing could compare it to the
model the way `measurement-backup-completeness.test.ts` compares the
measurement select. Name it — `MOOD_ENTRY_BACKUP_SELECT` and
`MOOD_ENTRY_TAG_LINK_SELECT` — and add the sibling guard that binds both
to `prisma/schema.prisma`.
The mapper's parameter is spelled `moodEntry` rather than `e` because the
guard proves a column reaches a payload by matching `: moodEntry.`, and `e`
is also the intake-event mapper's parameter one section above: a matcher on
`: e.` would match across both and could never fail.
The guard is red as it stands, and names the defect: `note`,
`noteEncrypted`, `tz`, `syncedAt`, `syncVersion`, `createdAt` and
`updatedAt` are selected and written to no payload. The fix follows.
Measured before the sweep: 317 description and hint strings under the
`settings` and `admin` namespaces, median 83 characters, 113 of them two
sentences or more. The rule the design standards now carry is one sentence,
guideline 120 characters, and this is the sweep that makes it true.
Almost nothing was retranslated. A second sentence is already written in
every language — it is just in the wrong slot. `scripts/split-description.mjs`
cuts a description at its first sentence boundary in all six bundles at once
and either drops the tail or files it under its own key; the twenty-one keys
that moved now render in the card body as foreground `text-sm`, which is
where multi-sentence prose belongs and where a reader can actually read it
(the slot is muted `text-xs`). Eight tails were dropped outright, because
"Off by default" under a switch, "Topics and frequency are adjustable" above
the topic switches, and "Only admins can create invitations" on an
admin-gated route are not information.
Twenty-eight keys are gone: the privacy page's intro, which was a table of
contents for the four cards under it; two About descriptions that listed
verbatim the labels and the buttons beneath them; a card description that
repeated its own title; the API-token sentence that said what the paragraph
below it said with the actual instruction; and the eight integration
secondary sentences. Six descriptions were rewritten shorter in all six
languages where trimming was honest rather than lossy.
On the strings that changed: 4391 characters of English down to 2290, and 32
multi-sentence descriptions down to zero.
The rule is now a gate. `card-description-length.test.ts` reads the keys that
actually reach a card header's description slot — statically, from the JSX —
and checks them in EN and DE, because a rule that only holds in English
produces a slot that wraps to two lines in half the locales. Its exception
list is empty and carries a reason requirement plus a test that removes a
stale entry. Verified by putting a second sentence back: it fails.
Twenty-six of twenty-eight admin files already used the Settings card and
header. What made the console read as its own system was everything on top of
that: three hand-rolled cards, a header slot that meant five different things,
and a heading the frame took but never painted.
- `SectionFrame` drops `title` and `subtitle`. Seventeen call sites passed
both into a component that has rendered neither since v1.18.6.1 — the shell
reads the same keys itself. A prop contract nothing honours is a promise the
next reader believes.
- The invite card, the four host-metric headers, and the `/admin` welcome
card stop hand-rolling `bg-card border rounded-xl` shells with their own
padding steps and their own heading weights. `text-sm` as a card title on
the first card of a page, directly above a correct one, was the first thing
the eye landed on.
- The header status slot carries status. The backups run, the invite create,
the danger-zone wipe, and the user filters come out of it; the app-log
refresh stays but reads as the secondary it is.
- Loading states paint their card and header. Encryption returned a bare
spinner row and then a card, so the section jumped when the status landed —
the fix `coach-feedback-section.tsx` already carried, applied where it was
missing. Same for the notification-status card.
- One label/value tile. `StatusItem`, the encryption section's private `Stat`,
the user-management inline panels and the backups upload box were four
fills and three paddings for one role.
- The overview stopped printing the running version, build SHA and build time
twice, thirty pixels apart. The version tile owns build identity — it is the
one that checks for a newer release; the snapshot owns what is running.
- `ai-server-key` had three paragraphs stacked in a muted `text-xs` slot;
one sentence stays there and the two that carry information move into the
body as foreground. The central-codex consent copy moves the same way, and
its disconnect is `variant="destructive"` — which `ConfirmButton` refused to
accept, so every destructive confirm in the tree had been hand-tinting an
outline instead.
- Leftovers: the `-mt-2` correcting the card's own rhythm, two alpha hovers on
text, `text-[10px]` badges, the redundant `space-y-6` wrapper, the empty
state repeating the header's CTA, and the `mt-6` a card added inside a
frame that already spaces its children.
The sweep that follows from the primitives. Nothing here is new behaviour;
every change removes a second way of saying something the surface already
said one way somewhere else.
Headers. The two collapsible security cards stop rebuilding
`SettingsCardHeader` by hand to carry a chevron — the chevron rides inside
the title button, and the description moves out of the collapsed region, so
the first paint is no longer a bare title. The import card shell, the export
and import section headings, and the four free-standing group headers stop
inventing their own type scale: a heading above a GROUP of cards is
`SectionHeading` (`text-base`), a card title is `SettingsCardHeader`
(`text-lg`), and the step between them is what says which is which. The
targets card gets a title instead of a naked icon.
Actions. Every card-level row is `SettingsCardActions`. The primary action
comes out of the header's status slot in six cards (password, web push,
pillar order, coach memory, and the two danger-zone triggers) and out of the
middle of the card in three more; the reset that hung in the dashboard-layout
header joins save as a secondary in the same row. Result and status lines
move ABOVE the row — sources' cross-link, the invite form's two status lines
— because nothing follows a card's actions. Revoking a share link is
`variant="destructive"` rather than an `outline` hand-tinted red in two
different ways. The import row had four variants for four buttons; it has
two. Export downloads are the card's primary, not an outline link.
Integrations. One description sentence with the setup-guide link inline: the
second paragraph was byte-identical across six of eight cards, which is the
tell that it said nothing about the provider it sat under. The `<hr>` under
the header — which existed only in this folder — is gone. So is the hint that
pointed at a guide 40 px above it, and the second format for "not
configured". Connect is one button everywhere: primary, `sm`, in the action
row, instead of primary-and-small in three cards and outline-and-large in
four.
Plus the standards leftovers the audit listed: two `pl-5` list gutters, one
`scroll-mt-20` anchor, a purple header icon, `text-[10px]` badges, four
hand-rolled callout shells that `SettingsInfoTile` already paints, and the
two inline buttons that never had the mobile tap floor at all.