Skip to content

feat: add effective-dated nutrition goal history - #243

Open
kgenerozov wants to merge 1 commit into
TraceApps:devfrom
kgenerozov:feat/historical-nutrition-goals
Open

kgenerozov wants to merge 1 commit into
TraceApps:devfrom
kgenerozov:feat/historical-nutrition-goals

Conversation

@kgenerozov

@kgenerozov kgenerozov commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Goal history keeps the nutrition-goal configuration that was in effect on each calendar day, so a report on a past week compares intake with the target it actually had instead of today's. Trace, MCP get_goals and GET /api/v1/goals share one implementation; the response without a date is unchanged.

What changes

  • One snapshot per person per local calendar day (goal_history), resolved as the newest snapshot on or before the requested date. Dates before the first snapshot are unknown, never filled in from today's goals.
  • Settings and goal history use one merge primitive (server/lib/setting-merge.js): the direct settings routes, a phone's sync push, a bulk/template, a reset and a day's history all apply an edit by the same rule. Edits are ordered in milliseconds, a tie goes to the arriving edit, a delete wins a tie, and an edit brings a deleted setting back only when it is strictly later. History knows the tombstone too, so a snapshot arriving at the exact instant of a reset can no longer revive a value the live settings keep deleted.
  • A day merges field by field: each snapshot field carries the time it last changed, a new day inherits the previous day's values and their times, and a deleted setting's default is dated at the moment it was deleted — not at the later capture that noticed it — so an edit made between a reset and a later change to another field still wins. A correction to an earlier day carries forward to the days that inherited it, up to the first day that set that field itself later.
  • The browser queue merges a day's changes together with the value each claim belongs to: two tabs reaching the shared outbox out of order cannot hand one tab's stale copy to a field the other tab changed.
  • One user edit is one native write, with its time and account day captured before it is queued (the store and the wl:setting listener no longer both write it). A queued write belongs to the account generation captured with the edit and stops if the account moves on; an account switch waits for the queue before counting or clearing the copy.
  • Goal writes record the day in the same transaction: single setting, bulk/template (one snapshot, one moment for the whole batch) and reset. The day is the client's effective_date, else the account's day in its timezone.
  • A pre-feature database gets its baseline on the first start after the upgrade, once the columns it reads exist. Full backups keep the history; a restore starts it at the restore moment for a backup made before this feature.
  • Goal history is cleared with the rest of the phone's copy on an account/server switch and counted as unsent before anything is discarded.

Historical semantics

  • Dates are the account's calendar days (its timezone). With no timezone set yet (the app records one on first launch), the dated forms use UTC on the phone and the server alike, so one edit resolves to one day everywhere. The response without a date keeps the day it always used — the account's zone, else the server's own — and is unchanged.
  • One row per account day; a date resolves to the newest row on or before it; each row is a complete configuration (missing fields take the app's defaults).
  • Same-day changes merge per setting by edit time in milliseconds, mirroring settings merge; values and effective times are inherited across days, so a claim older than the configuration in effect loses, and a claim at the exact instant a setting was deleted loses too.
  • Corrections to a past day propagate forward until a day that made its own, later decision for that field.
  • A goal template is one bulk write, one moment, one snapshot: its food and water goals never appear apart.
  • Resolution reuses the app's own rules (goal-resolver.js, effective-goal.js, adaptive-tdee.js) and only the observations available up to that date; fields are described in docs/public-api.md.
  • No query parameters → the current-goals response, unchanged.
  • Limit: history and GET /api/goals/effective need a signed-in account. In single-user mode (no user management) they are unavailable, as before.

Validation

  • npm test: 2010 tests, 0 fail (branch base 1981): behavioural tests for a delayed direct save, a conflict inside one second in settings and history, a reset whose instant ties an incoming claim, a reset followed by a change to another field and a late edit between them, two browser tabs merging a day with the queue order inverted, one native write per edit, an account switch with a queued write, two native goal edits at once, one moment for a native batch, the real browser queue path, a pre-feature database upgraded in one start, two devices on one day, carried claims, forward propagation and the clock corrections.
  • npm run build, npm run i18n:check: pass.
  • git diff --check clean; the rebased patch applies cleanly to 312b9b2.

@thebigjoe1

Copy link
Copy Markdown
Contributor

Thanks, this is a good idea. Dev has moved a lot since you opened it, especially sync and settings, and most of the files now conflict. Could you rebase it onto current dev? I'll review it properly then.

@kgenerozov

Copy link
Copy Markdown
Contributor Author

Thanks, this is a good idea. Dev has moved a lot since you opened it, especially sync and settings, and most of the files now conflict. Could you rebase it onto current dev? I'll review it properly then.

Sure

Goal history keeps the complete goal configuration in effect on each local
calendar day, so a report on a past week compares intake with the target it
actually had instead of today's. Several changes in a day collapse to that
day's final configuration, and a date before the first snapshot is reported
as unknown rather than filled in from today's goals.

Resolution reuses the app's own rules (goal-resolver.js, effective-goal.js,
adaptive-tdee.js): weekday goals, percent macros, Dynamic and Adaptive
calories, and the activity adjustment, read from that day's settings and
from the data available up to that date. The no-argument response is
unchanged.

The app records the day a goal change was made for, so an offline change
carries its own date up through the sync push; the server dates a change
from an older app in the account's time zone, and only for the newest day
history knows. Full backups keep the history. Applying a goal template is
one bulk write, which makes one snapshot.
@kgenerozov
kgenerozov force-pushed the feat/historical-nutrition-goals branch from 6884e12 to a5b2dcf Compare November 13, 2026 06:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants