vibeapp.win

What broke

Every save reset every other setting

In the monitoring app, saving one setting wrote all fifteen, so untouched settings went back to their defaults. Turning off one Claude switch turned four others back on. Henning found it by saying the switches seemed linked, and the database confirmed it: all fifteen rows had the same timestamp.

The short version

What went wrongThe settings route used zod's .partial(), which still filled defaults, so saving one key wrote all fifteen and reset untouched settings to defaults. Turning off one Claude switch turned four others back on.

How we found itHenning said the switches seemed linked, and the database showed all fifteen rows with the same timestamp. The optimistic UI had hidden the server's answer until a reload.

The rule nowWrite only the keys the caller actually sent, using settingsPatch() and not settingsSchema.partial(). Read the server's answer, not the component's state, before calling it saved.

This post documents something Claude got wrong. The claim rests on a commit or a file, not on memory. All of Claude's mistakes

On 23 August 2026 we added five switches to the System tab of the monitoring app. Each one turns off one feature that calls Claude. The same day, Henning reported a problem. The commit quotes him in Norwegian: “knappen virker ikke, de henger sammen”, which means “the button doesn’t work, they’re linked.”

He was right. The bug turned out to be older and worse than the switches.

What was wrong

The settings form validates its input with zod, a TypeScript validation library. The settings schema gives each key a default value. To accept a save that changes only some keys, the server route ran the schema through .partial(), which makes every field optional.

The commit message records what that actually does: parsing an empty body through the partial schema returns all fifteen keys filled with their defaults, because .partial() makes fields optional but does not turn off .default().

So when the browser sent one changed key, the parsed result held all fifteen keys. Every key it had not sent was filled with its default. The route passed that whole object to the store function, which, in the commit’s words, “faithfully wrote every row.”

The effect: every save wrote the whole settings set, and every setting you had not touched went back to its default. The commit says thresholds “have been silently reverting to defaults on every save since the form shipped.” The switches only made the problem visible.

What it did in practice

With the switches it stopped being silent. Henning turned one Claude switch off, and that request reset every other setting to its default. The other four switches went back to on.

The commit puts it plainly: a click meant to stop Claude started it.

How it was found

The report came from Henning, not from a test. The evidence came from the live database. The commit records that all fifteen rows carried the same update timestamp and the same author. If one click had changed one setting, one row would have a fresh timestamp. Fifteen identical timestamps meant one request had written everything.

A comment in the new integration test, written in Norwegian, explains why that was the only place to see it: the bug was invisible in the code and obvious in the table. The types passed the whole way through.

A second bug hiding the first

The switches had a problem of their own. All five shared one lock, so all five were disabled while any one of them was saving. The commit says that meant “the second click of a quick pair was swallowed silently and the budget field was reset mid-typing.” A comment in the component calls this the first attempt and notes that switching off two things in a row is the most common use.

The optimistic UI also hid the server bug. A switch flipped to “off” on screen straight away, while the server had already put it back to “on”. The session log in the repository is written in the first person, and it says the optimistic view added in that session hid the bug at first. Only a reload showed the real state.

The material records the lock in the first version of the switches, and the optimistic display that hid the server’s answer.

The fix

The route now uses a helper called settingsPatch(). According to the commit, it keeps only the keys the caller actually sent. The set of keys comes from the request body as sent, while the values still go through zod, so validation and bounds work as before. One key in, one key written.

The switches were changed too. Each switch now has its own pending state. It flips at once and rolls back if the save fails. After a save, only the saved key is read back from the server’s response. The response contains the full settings set, and applying all of it could undo another switch the operator flipped while the request was in flight.

How it was checked

The guard sits at two levels. The session log records eight tests for the helper and three integration tests against real database rows.

The first helper test is not about the helper at all. It records the zod behaviour itself: a partial schema still fills in defaults. The commit gives the reason: the test is there “so nobody deletes the helper as redundant.” A future reader who thinks .partial() already does the job will meet a failing test that shows otherwise.

The integration tests seed rows with values that are not the defaults, such as a threshold of 0.71. They then save one unrelated key and check that everything else is unchanged. A second case saves two keys one after the other and checks that both changes stay, instead of the second save undoing the first.

We also checked it in a real browser: headless Edge against a development server with rows in the database, before and after the fix. The commit records:

  • two quick clicks landed one change before the fix and two after, and both were still there after a reload
  • a budget draft of 40 survived a switch click
  • a threshold set to 0.71 stayed put across two saves; before the fix it would have become 0.62, the default

At that commit the session log shows 250 unit tests and 91 integration tests passing, with lint and typecheck clean. The app was built and deployed, and the switches were set back to off.

The rule

When saving part of a settings object, write only the keys the caller actually sent. Never take the key set from the output of a schema that has defaults. In this app that means settingsPatch(), not settingsSchema.partial(). The first test in its test file records why, so the helper does not get removed as redundant.

The commit adds a second lesson: “Read the server’s answer, not the component’s state, before calling it saved.”