Skip to content

fix(cycle): let a logged value be taken back - #554

Merged
Harshal2007vk merged 1 commit into
ishita2740:mainfrom
MOHITKOURAV01:fix/issue-549-cycle-log-clearing
Aug 29, 2026
Merged

fix(cycle): let a logged value be taken back#554
Harshal2007vk merged 1 commit into
ishita2740:mainfrom
MOHITKOURAV01:fix/issue-549-cycle-log-clearing

Conversation

@MOHITKOURAV01

Copy link
Copy Markdown
Contributor

Closes #549.

Log a day, then try to take one of the values back. You can't — and the
web app tells you it worked.

Both write routes stripped every None before touching Firestore:

fields = {k: v for k, v in log.model_dump().items() if ... v is not None}

model_dump() cannot tell an omitted field from one explicitly sent as
null — both are None — so the filter that implements "merge, don't
clobber what wasn't sent" also implements "you may never send a
clearing". PUT /cycle/{log_id} with {"notes": null} went further and
answered "No fields provided for update": a field was provided, the
handler discarded it, and then reported the discarding as the caller's
mistake.

On the web Cycle screen this is visible. The chips are toggles —
toggleSingle deselects to null and the chip un-highlights — but
save() built its payload with a truthiness check per field, so the
cleared value never left the browser. The page said "✓ Saved to your
account"
, reloaded the month, re-seeded the draft from the unchanged
server copy, and lit the chip back up.

It matters because this is a health record and the things a user most
wants to take back are the ones she can't: a wrong flow intensity feeds
compute_cycle_stats, predict() and the SMS summary; a wrong sleep
value skews the avgSleepHours on the provider's patient card; and a
note naming a partner or a clinic is in the provider view, the PDF report
and /privacy/export.

What changed

_submitted_fields reads model_fields_set instead of filtering on
value, so the two cases separate cleanly: a key that is absent is left
alone, a key that is present and null is a request to remove it. The
Home screen's quick-log tiles still merge — sending flow_intensity
alone does not disturb the mood logged an hour earlier.

CycleService._prepare_write turns a None into DELETE_FIELD. The
one asymmetry is for_new_document: Firestore's set refuses the
sentinel, correctly — there is nothing to delete in a document being
created — so a clearing of a never-written field is dropped and the
field ends up absent either way.

The 400 keeps its meaning and becomes true. It now fires only when
the body genuinely carried nothing.

The Cycle screen sends the fields it renders, null where cleared,
and Save is gated on the draft differing from the stored log rather than
on anything being selected. That second half matters as much as the
first: clearing the last chip left nothing selected, so the button went
disabled and the correction could not be sent at all.

notes and end_date are deliberately not sent from that screen. Both
exist on a log and neither has a control there, so including them would
let a screen that cannot show a note delete one.

The Firestore mock acts on the sentinel rather than storing it, and
rejects it in set() as real Firestore does. The rule for that mock is
that it may be narrower than Firestore but never looser, and silently
storing a write instruction as a value is looser — it would have made
every test below pass while a real deployment served clients an
unparseable object.

Tests

backend/tests/test_cycle_log_clearing.py — 18 cases, reading the stored
document back through GET /{user_id}/history rather than inspecting the
mock's store, because "is the field gone?" is only meaningfully answered
by what a subsequent read returns.

The merge behaviour is asserted alongside the clearing in almost every
case. The risk in this change is not that clearing stops working — it is
that clearing starts happening to fields nobody mentioned, which would
silently destroy data on every quick-log tap.

web/src/pages/CyclePage.test.tsx — 8 new cases asserting on the
payload
, not on what the page renders afterwards. What it renders after
a save is whatever the mocked reload returns, so a render assertion would
pass against the broken version too. The request is where the bug was.

Backend 1151 passed. Web 592 passed, tsc -b and oxlint clean.

Both write routes stripped every `None` before touching Firestore.
`model_dump()` cannot tell an omitted field from one explicitly sent as
`null`, so the filter that implemented "merge, don't clobber what wasn't
sent" also implemented "you may never send a clearing". A mis-tapped
flow intensity, a sleep value typed as 12 instead of 2, a note naming a
partner or a clinic: all editable, none removable — and deleting the
whole day was the only way out.

`_submitted_fields` reads `model_fields_set` instead, so an absent key is
left alone and a key sent as `null` becomes Firestore's `DELETE_FIELD`.
`{"notes": null}` is now an update rather than a 400 saying no fields
were provided.

On the web Cycle screen the chips already deselect to `null` and visibly
un-highlight; `save()` then dropped the null and the page reported
"Saved to your account" before reloading and lighting the chip back up.
It now sends the fields it renders, `null` where cleared, and the Save
button is enabled by the draft differing from the stored log rather than
by anything being selected — clearing the last chip used to disable the
button and strand the correction.

`notes` and `end_date` are deliberately not sent from that screen: it has
no control for either, and a screen that cannot show a note must not be
able to delete one.

The Firestore mock now acts on the sentinel rather than storing it, which
it had to for any of this to be testable.

Closes ishita2740#549
@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

@MOHITKOURAV01 is attempting to deploy a commit to the ishita2740's projects Team on Vercel.

A member of the Team first needs to authorize it.

@Harshal2007vk
Harshal2007vk merged commit 577a150 into ishita2740:main Aug 29, 2026
2 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Backend][Web] Nothing you have logged can ever be un-logged — deselecting a chip says "Saved to your account" and silently reverts

3 participants