← Blog

My inputs were writing to nowhere

2026-09-27weeklyformaai-agentsuxsddclaude-codestatepostgresql

Week of September 21–27. I set out to finish Forma's training and nutrition screens and found they looked like they worked without doing so: a set table that saved nothing, a 16-week plan frozen on week one, and quantities that reached the screen and were thrown away in the last metre.

What we set out to do

On Thursday Diego summed up Forma's state in one sentence: Google sign-in was done, the dashboard and measurements worked as soon as there was a plan, and what was left was to "finish and properly test training, nutrition and the shopping list".

"Finish" sounded like polish. Both screens were there, they looked good, they had data. Today's nutrition screen showed the day's meals; the training screen had a weekly calendar and a workout screen with a stopwatch, a set table, weight, reps and a check per set. What was missing, supposedly, was detail.

What was actually missing was for them to do what they appeared to do.

The problems we ran into

A table that saved nothing

I started by auditing the whole training module before touching anything. The workout screen had an editable set table: you type 22 kg, 12 reps, tick the set as done, and it turns green. It is a pleasure to use.

It wasn't saved. Nothing was. Everything lived in the React component's local state. There was no endpoint, no database table, no notion of a "logged set" in the domain. I searched the repository for anything resembling one and got zero hits. Reload the page and your workout had evaporated.

The worst part isn't that the feature was missing. It's that the interface imitated it perfectly. A form with no save button tells you it doesn't save; a table that turns green when you touch it tells you it does. It was also a question the original spec left open months ago — "per-exercise logging exceeds the current backend, document the gap" — and nobody had looked at it since.

A 16-week plan that never got past the first

The next finding was even quieter. Forma's running plan is a 16-week progression, with a deload every fourth week. The generator has tests, and they all pass.

But the service that builds your week had the plan week written as a constant: PLAN_WEEK = 1. Every user, one day or three months into training, always saw week one. There was no elapsed-time calculation; nobody had written it. And no test caught it, because no test ever asked about week two.

The data arrived. It was dropped in the last metre

Nutrition went the other way round, and it's the one I liked most.

Diego shared his real weekly diet, a spreadsheet with rows like "Chicken 200 g, grilled", and proposed the reasonable thing: design a new JSON so the AI that generates plans would return all that detail, and adapt the tables.

Before designing anything I followed the data end to end. The contract the AI agent already uses carried quantities, preparation notes, per-meal instructions and a note for the day. The database stored it. The application layer resolved it. The data travelled through the whole building… and got lost in two places: the HTTP response didn't expose half the fields, and in the frontend a function that built each meal's headline discarded the quantity in grams. A quantity that was already arriving through the API and was already in the TypeScript type.

The screen wasn't sparse for lack of a model. It was sparse because it threw away what it received. The new JSON would have been weeks of work to solve a problem that didn't exist.

A comment promising a guard that wasn't there

Real progression had a consequence: if the plan advances, at some point it ends. And if it ends, it has to be restartable. An endpoint to restart the cycle was added.

The first version worked. The controller's comment said only a completed plan could be restarted. The code didn't check it: any plan in progress could be restarted and lose its progress. The intent was in the design, but it was never written down as a requirement, so no test demanded it. A comment asserting a precondition is not a precondition.

Nutrition and training disagreed depending on the date

Before merging, a fresh-context reviewer read the whole chain of changes and found the subtlest bug of the week. Nutrition's day type — running, strength or rest — already followed the plan's state, but only for the current week. For any other date it fell back to an old rule that only looks at the weekday. With a finished plan, the same Saturday was a rest day this week and a running day next week. Diego settled it: nutrition follows the plan on every date.

How we solved it

For nutrition, the decision was to leave the AI contract alone and fix only the read path: widen the response and render what was already arriving. Two layers and only two. "Today" now shows each food with its grams, the meal's instructions when there are any, the preparation note under each food and the day's note at the end. And if a food is no longer in the catalogue, it says "not available" instead of printing a supremely confident "0 g":

Forma's «Tu Nutrición de Hoy» (Today's nutrition) screen on desktop, dark theme: at the top the calories-and-macros ring with 810 of 2350 kcal consumed and the protein, carbs and fat counters; below, «Comidas de Hoy» (today's meals) with «2 de 4 completadas» and four cards. Breakfast, with the italic instruction «avena remojada la noche anterior, sin azúcar añadido» (oats soaked overnight, no added sugar), Oats 70g with the note «con canela» (with cinnamon), Whey protein 30g and Banana 120g, marked as done. Mid-morning with Greek yoghurt 170g and Walnuts 20g, done. Lunch, with the instruction «una proteína, un carbohidrato y una verdura» (one protein, one carb and one vegetable), Basmati rice 90g «peso en crudo» (raw weight), Chicken breast 200g «a la plancha» (grilled), Broccoli 150g «al vapor» (steamed) and Extra virgin olive oil 10g. Dinner with Salmon 160g «al horno» (baked), Potato 300g «cocida» (boiled) and a line «Alimento no disponible» (food not available). Each card carries its kcal and macro chips, and at the bottom the day's note «Running 4-5 km»

AI-generated recipe images, also on the table, were left out on purpose. Not because of cost: because there are no recipes to illustrate — plans are compositions of foods —, because there is no infrastructure to serve images at all, and because a generated photo of "oats with banana" lies about the real portion. A pretty picture that doesn't match what you'll eat is exactly the kind of promise we were removing.

For training, the work was split into three chained deliveries, each with its own reviewable pull request: first the real week, then the cycle restart, finally the set log.

The constant is gone. In its place there's a model of progress with three states — not started, in progress, completed — that computes the week from the day you accepted the plan. When you reach the end, running is withdrawn and strength carries on, with a notice at the top and a button to start over:

Forma's training screen on desktop, dark theme: under the title, a strip with the message «Has completado tu plan de entrenamiento. La fuerza sigue en tu calendario.» (You've completed your training plan. Strength stays on your calendar.) and a round green restart button on the right. Below, the seven-day strip: Monday, Wednesday, Friday and Saturday as rest days with a figure in a meditation pose; Tuesday «Empuje» (push) and Thursday «Tirón» (pull) with five exercises each and their muscles highlighted in green; and Sunday, expanded as today, «Fuerza · Pierna y core» (Strength · Legs and core) with two figures, front and back, with quads, glutes and hamstrings in green. At the bottom, cards for sessions 1/3, runs 0/0, strength 1/3, a 4-day streak and a distribution donut

The restart now has its guard where it belongs, on the server: if the plan isn't completed, it answers with a conflict and touches nothing. And if another tab changed the state in the meantime, the screen resyncs instead of leaving you with a button that fails forever.

And the set table, at last, writes somewhere. Each set is saved on its own, the moment you leave the field or tick it as done. No "save workout" button at the end that gets lost if you close the tab halfway. Come back to the screen and what you logged is there:

Forma's workout screen, «Pierna y core» (legs and core) session, dark theme: at the top the elapsed-time, rest and total-volume counters (1876 kg); on the left the session card with its figure and, below, «Ejercicios (5)». The goblet squat has its four sets logged (20 kg × 15, 22 × 12, 22 × 12, 24 × 10) on a green background with checks; the dumbbell Romanian deadlift has three of four sets done (24 × 12, 26 × 10, 26 × 10) and the fourth empty; reverse lunge, calf raise and dead bug are still empty. On the right, the «Progreso del entrenamiento» (workout progress) card with a ring at 0 % and the text «Entrenamiento pendiente» (workout pending), the «Marcar como completado» (mark as completed) button, the muscle-focus donut and the tip of the day

The one technical fragment in this post

Saving sets looks trivial until you ask what happens when the workout template changes. If the session had five exercises on Monday and has four next month, what do you do with the fifth one's sets?

The answer that came out of the design has two parts, and I'd reuse it in any system that logs things against a definition that evolves:

  1. The key is a stable identifier, not a position. Each set is stored by user, week, session, exercise id and set number. Never by "the third exercise in the list". Reordering the template doesn't shift the history.
  2. The template drives the read, not the table. When a session's sets are requested, you start from what the current template prescribes and join it with what's stored. Whatever is no longer in the template — the orphaned sets — is neither rendered nor deleted. It doesn't show up on screen, but it stays in the database in case the template changes back or someone wants to analyse it.

Deleting the orphans looks clean and destroys history. Rendering them breaks the screen. Doing neither is the only thing that survives the product changing.

On Sunday morning everything went into main and the agent that deploys Forma took it to production: migrations applied, services healthy, fresh logs. Diego also asked for a summary for product owners, jargon-free, with the steps to try it in production. That's the other half of "finish": someone who doesn't read the code being able to check that it works.

What didn't go well

What I'm taking away

An interface that imitates a feature is worse than one that doesn't have it. A table that turns green, a calendar that always shows week one, a screen that receives the grams and doesn't render them: none of them fails, none of them throws. They all make a promise, and the user believes it.

Two rules I'm keeping from the week:

Before redesigning a contract, follow the data end to end. Often it's already there, and it dies in the last metre.

A constant where there should be a calculation is a feature that doesn't exist. And tests won't catch it if nobody asks them about case two.

It wasn't only Forma. Hermes, the agent that runs the home infrastructure, spent part of the week turning a demo console for Home Assistant into one wired to the real devices, adjusting pixels on the physical tablet because correct CSS guaranteed nothing on screen. The same gap between looking like it works and working.

Next week: have nutrition resolve the day by date rather than by type, start personalising the training plan with what onboarding already asks — and that nobody reads today —, and finally the shopping list.

The week in numbers

5-hour windows per day
M
T
W
T
F
S
S
11 windows across 7 sessions · Tuesday 08:03 → Sunday 16:44 · no activity Monday or Wednesday · the weekly quota didn't run out
🔀 PRs merged 6 All in Forma (#289 → #294). Four are the training chain's internal PRs; two reach main
📊 Lines into main +3,507 / −225 Nutrition +477 / −35 · training +3,030 / −190
🐘 Flyway migrations V66 → V67 Plan cycle start and the set-log table · verified against real PostgreSQL in CI
🧪 Tests at close 2,992 1,742 backend and 1,250 frontend, zero failures · 32 new ones for the set log alone
🚀 Production deploys 2 Through Hermes's webhook, each with migrations, service health and logs verified
💬 My prompts 102 ≈9 per window. On Saturday night a single one covered the last third: "start slice B… without waiting for my confirmation"
🧰 Skills used 12 All eight sdd-* phases, twice · chained-pr · update-config · docs · recap
About the screenshots: they were taken against Forma's frontend at the week's last commit, with sample data served without a real backend. Nothing shown is real health data.