Commit Graph
5349 Commits
Author SHA1 Message Date
Marc-André Bombeck f91eedb6bb Merge pull request #739 from MBombeck/fix/mood-block-name
Name the mood sliders for what they ask
2026-08-08 08:50:19 +02:00
Marc-André Bombeck d1fa0d9d52 Name the mood sliders for what they ask, not how many they are
The capture section and the insights card called the five sliders
"Five scales", a count where a name belongs. They now carry the
state-of-mind wording in all six languages, with Spanish diverging
from the first slider's own label so the block and the mood slider
stay distinguishable.
2026-08-08 08:34:53 +02:00
Marc-André Bombeck 2f68b3dc79 Merge pull request #737 from MBombeck/release/v1.37.1
Release v1.37.1
v1.37.1
2026-08-08 05:54:20 +02:00
Marc-André Bombeck 035ec054da chore(release): v1.37.1 — five mood values and the range the lab report printed 2026-08-08 05:38:56 +02:00
Marc-André Bombeck 89a749b811 Merge pull request #730 from MBombeck/refactor/settings-admin-one-piece
Settings and admin, one piece
2026-08-08 05:36:10 +02:00
Marc-André Bombeck c6246092ff Merge branch 'main' into refactor/settings-admin-one-piece 2026-08-08 05:20:04 +02:00
Marc-André Bombeck 9e89f3ec5d Merge pull request #736 from MBombeck/ci/e2e-shards
Run the browser suite as two shards
2026-08-08 05:18:30 +02:00
Marc-André Bombeck ea4d284cc9 ci: carry the report per shard and point the guard at the running job
The determinism guard reads the job that runs the browser, which is now
the shard matrix rather than the gate that only carries the required
check name. The report artifact is named per shard so the two uploads
cannot clobber each other.
2026-08-08 04:45:04 +02:00
Marc-André Bombeck 8045540b7e Merge pull request #735 from MBombeck/feat/mood-level-a
Five dimensions behind the mood check-in
2026-08-08 04:29:27 +02:00
Marc-André Bombeck 8494557a3a ci: run the browser suite as two shards
The single job passed at about eighteen minutes against a twenty-minute
cap, and the settings geometry measurements pushed it over, so the run
died at the timeout with everything to that point green. Same remedy as
the integration suite, split rather than raise: two Playwright shards,
each with its own database and production build, and a gate job carrying
the exact name branch protection requires that fails when any shard
failed or was cancelled.
2026-08-08 04:22:13 +02:00
Marc-André Bombeck 1e72b24477 Merge remote-tracking branch 'origin/feat/mood-level-a' into feat/mood-level-a 2026-08-08 04:02:53 +02:00
Marc-André Bombeck 5e1f5aa9ea refactor(mood): keep the trend windows in one place
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.
2026-08-08 04:02:34 +02:00
Marc-André Bombeck fec15e8fed Merge pull request #734 from MBombeck/fix/wipe-geo-truths
The admin wipe wipes, geo stays on the host, and two docs tell the truth
2026-08-08 03:54:02 +02:00
Marc-André Bombeck bd787daf8b Merge branch 'main' into feat/mood-level-a 2026-08-08 03:29:52 +02:00
Marc-André Bombeck cbf949d43a Merge branch 'main' into fix/wipe-geo-truths 2026-08-08 03:29:51 +02:00
Marc-André Bombeck 6dd70f532a Merge branch 'main' into refactor/settings-admin-one-piece 2026-08-08 03:05:27 +02:00
Marc-André Bombeck 9da876686d Merge pull request #725 from MBombeck/chore/transparency-ci
Scheduled CodeQL, Trivy and Scorecard workflows
2026-08-08 03:05:25 +02:00
Marc-André Bombeck 3dd79ec734 fix(mood): one spelling on the wire, one day-key helper, one written decision
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.
2026-08-08 03:03:38 +02:00
Marc-André Bombeck 4db0f3a62a fix(mood): say which age governs a claim about a dimension
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.
2026-08-08 02:57:40 +02:00
Marc-André Bombeck 0a39d50b05 fix(mood): stop offering to clear a value that cannot be cleared
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.
2026-08-08 02:55:08 +02:00
Marc-André Bombeck aa56246ba2 fix(backup): the restore states a level-A value only when the file has one
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.
2026-08-08 02:52:41 +02:00
Marc-André Bombeck d3577d2557 test(mood): cover the level-A blocks at the composition that ships
The staleness test rebuilds the block shape from the pure calculator, so
it proves the calculator and the freshness rule agree and nothing else:
hardcoding the freshness flag in the feature builder, deleting the
notice that goes with a stale value, or dropping the whole block left it
green. This calls extractFeatures against real rows instead.

Both failures were watched. With the flag hardcoded true the unit test
still reported four passing while this one named the six-day-old stress;
with the notice removed it named the missing instruction.
2026-08-08 02:48:53 +02:00
Marc-André Bombeck 370d703d03 Merge branch 'main' into chore/transparency-ci 2026-08-08 02:46:18 +02:00
Marc-André Bombeck 1c135dfe48 Merge pull request #726 from MBombeck/feat/lab-source-reference-ranges
Carry the reference range printed on the lab report
2026-08-08 02:46:16 +02:00
Marc-André Bombeck b9a6eeb1d7 fix(mood): a re-post no longer blanks the values it says nothing about
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.
2026-08-08 02:45:31 +02:00
Marc-André Bombeck 94816d31a8 Merge branch 'main' into feat/lab-source-reference-ranges 2026-08-08 02:27:10 +02:00
Marc-André Bombeck abb0f2e82c Merge pull request #728 from MBombeck/fix/sync-range-validation
Sync writers refuse readings the app itself calls impossible
2026-08-08 02:27:08 +02:00
Marc-André Bombeck d764a084a7 Merge branch 'main' into fix/sync-range-validation 2026-08-08 02:08:02 +02:00
Marc-André Bombeck 3c6b1b7001 Merge pull request #727 from MBombeck/fix/mood-backup-completeness
The backup carries the whole mood entry
2026-08-08 02:07:59 +02:00
Marc-André Bombeck 5d98a98c21 refactor(mood): drop the orientation helper until something needs it
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.
2026-08-08 02:07:21 +02:00
Marc-André Bombeck 0c017ba335 docs(changelog): note the five mood values 2026-08-08 02:05:50 +02:00
Marc-André Bombeck 30f35cfcaf Merge branch 'main' into fix/mood-backup-completeness 2026-08-08 01:48:51 +02:00
Marc-André Bombeck 77b3eaf10f Merge pull request #731 from MBombeck/fix/nanoid-advisory
Float nanoid past an infinite-loop flaw
2026-08-08 01:48:47 +02:00
Marc-André Bombeck 8084c97472 docs(score): state why daily mood stays out of the wellbeing pillar
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.
2026-08-08 01:44:50 +02:00
Marc-André Bombeck a6bb4f6577 feat(mood): give the assistant all five values, each with its own age
- 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
2026-08-08 01:43:32 +02:00
Marc-André Bombeck b4b39e9088 feat(mood): trend the five values on the insights page
- 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
2026-08-08 01:34:59 +02:00
Marc-André Bombeck 42c2d8d140 Merge branch 'main' into fix/nanoid-advisory 2026-08-08 01:25:40 +02:00
Marc-André Bombeck 97582dedc0 feat(mood): capture the five values without lengthening the quick check-in
- 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
2026-08-08 01:24:04 +02:00
Marc-André Bombeck 1533afdc9d Merge pull request #733 from MBombeck/fix/cycle-test-evening-bomb
Pin the cycle fixtures' clock to the suite's clock
2026-08-08 01:18:34 +02:00
Marc-André Bombeck 7eaede257e feat(ui): add a slider primitive for bounded numeric entry
- 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
2026-08-08 01:12:52 +02:00
Marc-André Bombeck 9e44c8754e feat(mood): carry the five level-A values through backup, restore and CSV
- 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
2026-08-08 01:08:27 +02:00
Marc-André Bombeck cb12adcccf docs(security): correct the image publish trigger
The supply-chain section said the publish workflow runs on every push to
main. That trigger was removed in v1.4.34, when the squash-merge and the
release tag each kicked off their own build of the same SHA; the
workflow has fired on v* tags only since. Pull requests still build the
image, and do not publish it.
2026-08-08 01:06:24 +02:00
Marc-André Bombeck 0eac84aa95 fix(geo): read the offline databases first
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.
2026-08-08 01:05:44 +02:00
Marc-André Bombeck ef35fe681d fix(admin): wipe every account's data, not nine tables
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.
2026-08-08 01:05:31 +02:00
Marc-André Bombeck 643d8a5a00 feat(mood): map existing entries onto the pleasantness scale
- the migration fills mood_a1 from the five-point label for every row
  that has none, and leaves the other four columns empty because there
  is nothing in a label to derive them from
- guarded on the column being empty, so re-applying the migration is a
  no-op rather than an overwrite of an answer somebody gave since
- the test reads the CASE arms back out of the .sql and compares them to
  the runtime map in both directions, so neither can drift alone
- rows are asserted by value after the statement runs, and again after a
  second run, including one hand-set value that must survive it
2026-08-08 01:03:05 +02:00
Marc-André Bombeck 0849d59ecf test(cycle): pin the fixture clock to the suite's clock
Two cycle suites compute their relative day keys with local Date arithmetic
under TZ=UTC while the routes resolve today in the user's zone, falling back
to Europe/Berlin when the fixture sets none. For two hours every evening the
two clocks disagree about the date and every day-of-cycle assertion is off
by one — the suite was red from 22:00 UTC to midnight and green the rest of
the day. The fixtures now carry an explicit UTC timezone so the test's clock
and the route's clock are the same clock at any hour. Reproduced red at
23:07 UTC, green with the pin at 23:19 UTC.
2026-08-08 01:02:08 +02:00
Marc-André Bombeck a1c78eceef feat(mood): derive pleasantness on every write path
- 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
2026-08-08 00:49:15 +02:00
Marc-André Bombeck 4bbf4c6d10 ci: three integration shards, not two
Two shards cleared in about fourteen minutes on a quiet day, but with six
pull requests running side by side the larger shard hit the same 25-minute
guard the split was meant to escape. Three shards keep the biggest slice
around sixty files, which clears the guard with real headroom on the
slowest runner observed. The gate job is unchanged.
2026-08-08 00:38:51 +02:00
Marc-André Bombeck ad4f5c04f0 feat(mood): add the five level-A columns to the mood entry
- migration 0310 adds mood_a1, stress_a2, energy_a3, connection_a4 and
  stability_a5, all nullable, no table rewrite
- the schema comment states the four things a later reader needs: score
  keeps its 1-5 axis, the columns are queryable plaintext on purpose,
  stress is stored literally, and a read delegate sees them by design
- the backup guard is red until the carriers land alongside
2026-08-08 00:37:41 +02:00
Marc-André Bombeck 9a390aacea feat(mood): add the level-A dimension table and the A1 derivation map
- 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
2026-08-08 00:36:09 +02:00