milibrodeobra
The resident engineer enters the day once, standing on site, with photos. The WhatsApp message comes out identical to the one they used to type by hand, followers open the page by link without installing anything, and the logbook writes itself.
The paper site diary is read by whoever holds it. This one is written from a phone, on the job site, and opened by people who never installed anything.
The daily report is entered once. From that same state come the WhatsApp message the group already knows how to read, the page shared by link and the site diary. In real use on job sites since June 2026.
A resident engineer's day, from the job site to the diary
Tap the steps or let it play. Each step lights up the piece it talks about.
What the group receives. The format is the one they already read: only who types it changes.
One frontend, one state and a database that is also the dev database
No separate API and no cron. Pages read with the anon key under RLS; mutations are server actions that validate with Zod and write with the service role.
actividades as the progress engine, eventos for usage auditing, reportes_pago with a status. The site diary has no table: it is built by reading the reports.A day on site, from 7 in the morning to the diary
A made-up case with small numbers, to see what happens at each stage before getting to the challenges.
**Cemento 20 sacos to pending material, high priority. Saving validates the report and writes it as the day's state.Five times the obvious solution was wrong
Each challenge follows the same pattern: what anyone would do first, why it does not work, what was done and the number that proves it. The technical detail stays folded.
The filter for every decision was the same: does this save the engineer typing while standing on site, or does it change something for the people who already read the report? The first gets in. The second is dropped even if it is "better".
The dev database is production, and the rules follow from that
An engineer entering real reports while the product is being built. Additive migrations, fixtures prefixed test-* that are created and deleted by script, a list of protected sites that aborts the cleanup if one is named, and a JSON backup before mutating data.
Go deeper
Fourteen tables in total, with the snapshot in just one. A flag like silenciado on a consumable travels in the next day's clone because it is in the JSON, not because an FK guarantees it. It is desired behaviour that you have to know about.
revalidatePath on what changed.Go deeper
Public reports are force-dynamic: renewing a subscription or silencing an alert shows on the next reload without a redeploy. There is no cron or scheduler; execution days, stock alerts and the diary are computed on read.
Go deeper
Google Identity Services returns an id_token that is passed to signInWithIdToken. If Google's script fails to load, it falls back on its own to the redirect path. The rule for who can sign in lives apart from the OAuth transport (lib/altaGoogle.ts, with the data gate injected) so it can be tested without Google: if a profile with that email already exists and is not linked, it links itself; if it points to another Auth user, it is not overwritten.
Gotcha: locale in renderButton translates nothing; what translates the button is ?hl=es in the script URL. And the button fails silently if the origin is not authorized, so the code checks that Google drew something before replacing the fallback.
Go deeper
A new account starts with a 30-day trial, no card. In /suscripcion the user sees how many days are left, where to pay and a form to report the payment: reference, bank, date and a mandatory screenshot. The admin reviews in /admin and confirms or rejects with a reason the user reads in their history.
Confirming adds a month on top of the current expiry, not on top of today: whoever pays early does not lose the days they had left. proximoVencimiento also handles the January 31 plus one month case, which JavaScript sends to March.