When failure looks like success
This is the lesson that justifies the whole level. Four bugs, all real, all from this project — and all the same bug wearing different clothes: the system said it worked. Nothing crashed, nothing logged an error, every screen looked right. The data was wrong for weeks.
Why this is the first thing you learn, not the last
A crash is a gift. It has a stack trace, a line number and a moment in time. You fix it in an hour and you are done.
The bugs that cost real time are the ones that return success. There is nothing to search for, no error to paste, no moment when it started — just a number that has been subtly wrong for long enough that you now cannot tell which of your conclusions were based on it. Every one of the four below cost days, and every one was invisible until someone happened to look at the right row.
1 · A partial import silently became "the whole day"
What happened. Step and activity data arrived in chunks throughout the day. The import wrote the day's row by replacing it. So a person who walked in the morning and again in the evening ended the day with only the evening walk recorded.
Why nobody noticed. The dashboard showed a plausible number. Steps for the day looked low-ish, but days are low sometimes. There is no error state for "this number is 40% of the truth".
The fix, and the general rule. Imports aggregate, they never replace:
-- wrong: the last import of the day wins
INSERT INTO activity_daily (day, steps) VALUES (?1, ?2)
ON CONFLICT(day) DO UPDATE SET steps = ?2;
-- right: every import contributes
INSERT INTO activity_daily (day, steps) VALUES (?1, ?2)
ON CONFLICT(day) DO UPDATE SET steps = steps + ?2;
Read that difference again, because it is one character of intent and weeks of wrong data. And notice it generalises far past health: any "just import the JSON blob" pipeline — bank statements, order exports, time tracking — has exactly this failure mode waiting in it.
2 · CSS masked a real bug for weeks
What happened. A lookup table expected uppercase keys. The database held lowercase values — nutrition_preference, not NUTRITION_PREFERENCE. So the lookup silently matched nothing and the feature did nothing.
Why nobody noticed. A stylesheet rule — text-transform: uppercase — rendered the lowercase value as uppercase on screen. Every visual check confirmed the data was fine. It was not fine; it was being displayed fine.
The rule. Never verify data through rendered text. Check it where it lives:
# the value, not the rendering of the value
curl -s https://your-site/api/health/daily | jq '.rows[0]'
# or, in the browser console — the DOM, not the pixels
document.querySelector('[data-pref]').dataset.pref
CSS is a liar by design: its entire job is to make things look different from what they are.
3 · An exclude-list failed open
What happened. A score needed "minutes of real movement". The filter was written as an exclusion: count every activity except the walking-like ones. Then one mislabelled entry — 240 minutes of "Wellness/sauna" — sailed through the exclusions and blew the score apart.
The shape of the mistake. Compare how the two lists behave when they meet something the author never imagined:
| Exclude-list | Allow-list | |
|---|---|---|
| Unknown input | Included | Excluded |
| Fails | Open — towards counting nonsense | Closed — towards counting nothing |
| How you find out | A number is wrong, eventually, maybe | Something is missing, immediately, visibly |
An allow-list is self-limiting in the safe direction. When a new activity type appears you notice, because it is absent — not because a score quietly doubled. Anywhere a filter guards something that matters, write down what is allowed.
4 · Timestamps are never as uniform as the schema implies
What happened. Several import pipelines, written months apart, wrote into the same timestamp column. One wrote 2026-09-16. Another wrote 2026-09-16T07:54:17Z. A third wrote local time with an offset. The column type said nothing was wrong.
Then a query asked for a range using full timestamp strings — and silently dropped whole rows, because string comparison does not care what you meant.
The rule. Compare on the date prefix, never on a full-timestamp string range:
-- fragile: depends on every writer using the same format
WHERE ts >= '2026-09-16T00:00:00Z' AND ts < '2026-09-17T00:00:00Z'
-- robust: works whatever shape the rest of the string is
WHERE substr(ts, 1, 10) = '2026-09-16'
And store UTC everywhere, always. The day you are in Prague and your data is in New York is the day you learn this one the expensive way.
What these four have in common
Not one of them threw an error. Each one had a moment where a human looked at a screen, saw something plausible, and moved on. That is the actual skill this level is teaching, and it is worth more than any of the code:
- Plausible is not correct. Your eye accepts any number in the expected range.
- Check the store, not the view. Every layer between the table and your eye can lie, and CSS lies professionally.
- Prefer failures that are loud and closed. Allow-lists, hard errors, a marker file written into the output. A system that stops is annoying; a system that continues while wrong is expensive.
- Suspect success. After you ship a writer, go and count the rows. It takes a minute and it is the minute that pays.