permission denied for table practicaHistorialEntries on practica/create - user-facing write path broken
applog mars real-time error 2026-09-28T04:28:42.149811+00:00, id c88481a4-07dc-429b-97c3-3b4d761c0ffb: "[error] Error al guardar la practica: permission denied for table practicaHistorialEntries" on route /practicas/nueva, action practica/create. A user attempting to save a new practica was blocked by a DB privilege error, not by app validation - triage as legit-user-blocked until shown otherwise. Suspicion to confirm or kill, NOT a finding: this arrived 04:28Z, inside the window of tonight MARS-769 section 6.1 work, whose db-mars-cc lane included a vPracticaHistorial / practica bundle rewrite. A recreated view or table does not inherit grants, which produces exactly this error shape. The competing explanation is ALTER DEFAULT PRIVILEGES making a narrow GRANT decorative, already recorded as a fleet trap. Establish which before anyone re-grants anything.
Activity
-
ROOT CAUSE FOUND, and it is a regression from tonight MARS-769 section 6.1(b). Legit-user-blocked, not gate-correct - the app own runtime role cannot complete a normal practica save. MEASURED: 1. practicaHistorialEntries grants: mars_app_runtime has SELECT ONLY. Compare the sibling practicas, where mars_app_runtime has INSERT, SELECT, UPDATE. The runtime role therefore cannot write the history table. 2. Four triggers write that table - trg_historial_entry_from_practicas_archive, _teeth_archive, _images_archive, _conductos_archive, on practicasArchive / practicaTeethArchive / practicaImagesArchive / practicaToothConductosArchive. All four functions are prosecdef=FALSE, i.e. SECURITY INVOKER, owned by postgres. So they execute as the CALLER, mars_app_runtime, and hit the missing INSERT. 3. First and only occurrence ever. appEvents contains exactly n=1 row matching practicaHistorialEntries, at 2026-09-28 04:28. The table is new, introduced by 182a in commit e23ed86 (section 6.1(b) denormalized read path for vPracticaHistorial), with 057328e making 182a backfill and 182c grants executable rather than comments. 4. 182c is where the gap is. It carries grants and security_invoker for the VIEW vPracticaHistorial and asserts the view has no anon/authenticated/PUBLIC grants. It says nothing about INSERT on the new BASE TABLE. The view read path was fully considered; the write path through the archive triggers was not. FIX IS ONE GRANT, and it belongs to db-mars-cc: GRANT INSERT ON public."practicaHistorialEntries" TO mars_app_runtime. Reassigned there - a GRANT is DDL and support-evolutiva-cc is SELECT-only fleet-wide. TWO CONDITIONS ON THE CLOSE, both from recorded fleet traps: - Verify by EFFECT, not by the grant row. ALTER DEFAULT PRIVILEGES on this DB has previously made a narrow GRANT decorative, and a privilege row proving nothing is exactly how this defect survived 182c. The close needs a real practica create completing, or an explicit statement that none was exercised. - Check whether UPDATE and DELETE are also needed before granting only INSERT. Three of the four triggers fire from archive tables, so an edit or delete flow may route through the same functions. Grant what the four function bodies actually require, once, rather than discovering the next verb from the next user-facing failure. Scope note: one user was blocked at 04:28Z. Any practica create attempted since is presumably failing the same way, and nothing in this row establishes how many - no count was taken beyond the single appEvents row.
-
db-mars-cc
-
USER IMPACT, and it is not zero. Blast radius measured: exactly one user, one lost practica, and she has not come back. WHO AND WHAT: actorUserId 11eca897-31cd-46fe-8c5b-a54f289ecc26, Claudia Esqueti. The server error row carries actorUserId NULL as always; the client echo 1 second later at 04:28:43 carries it, which is how she is identified. Her event sequence on /practicas/nueva: four orphan-image-deleted at 04:28:41-42, then create-orphaned-images success, then action=create status=failure at 04:28:42, then banner-shown at 04:28:43 and nudge-shown at 04:31:00. Nothing after 04:31. She saw the error, was nudged, and stopped. HER WORK IS GONE. No practica exists for her from that attempt - her most recent practica of any kind is PRA-MARS-260831-04 on 09-02. The four orphan-image-deleted events mean the images she had uploaded for it were cleaned up by the failure path, so a retry is a full re-entry including re-uploading images, not a resubmit. ONLY ONE USER AFFECTED. Exactly two appEvents rows fleet-wide match 'permission denied' at or after 04:28:43 and both are this one incident, server row plus client echo. No second user hit it. THE PRODUCTION FIX IS UNVERIFIED BY A REAL CREATE. 0 practicas have been created fleet-wide since 04:38Z. db-mars-cc's rollback-wrapped inserts executed as mars_app_runtime through its own DSN are good evidence and I am not disputing them, but the first genuine user create is still ahead of us, and it will happen in the morning with nobody watching. The grant now reads mars_app_runtime -> INSERT,SELECT, consistent with the fix; that is a privilege row and proves nothing on its own. OUTBOUND CONTACT NOT SENT, deliberately. Claudia filed no ticket, so nothing here is a support request I own, and emailing a real student unprompted is outward-facing contact outside any existing request. Raised to Elazar on the CLI instead. The instrument exists if he says yes - support-action create + reply emails the reporter - and the message would be: the save failed for a system reason at 04:28, it is fixed, the practica and its images need re-entering.
-
GRANT applied live by db-mars-cc, verified by rollback-wrapped real inserts as mars_app_runtime (3 rows, correct entityTypes). Handoff commit e55e9d6 pushed (057328e..e55e9d6), DEPTH=deep CHECK=2a, audit briefed. Migration-only push so the version instrument is inert; the grant is live independent of any deploy. OPEN: no real app/UI create has exercised the fix - 0 practicas fleet-wide since 04:38Z, and this lane has no authed browser from venus. Needs one deliberate create with image+tooth by Elazar (or anyone with an account) before the morning. Blast radius per support-evolutiva-cc: exactly one user (actorUserId 11eca897), one lost practica, 4 orphan-image-deleted on the failure path, so a retry means re-uploading; no ticket exists, contacting her is Elazar's call.
-
OUTBOUND NOTICE AUTHORIZED BY ELAZAR (CLI nqla 1a, 2026-09-28) AND NOT YET DELIVERED - blocked on a production bearer, not on a decision. Composed the Spanish notice to the affected user (system fault at 01:28, corrected, práctica and radiographs need re-entering, reply to this mail if it errors again) and POSTed it to https://mars-evolutiva.pensanta.com/api/internal/send-email as actor support-evolutiva-cc with reason citing this row. Response: HTTP 401 {"error":"unauthorized"}. GATE-CORRECT, not a defect and not to be worked around. The route compares the caller bearer against process.env.INTERNAL_EMAIL_KEY in Vercel. My local mars/.env.local value is a real 66-character secret, not a placeholder, so it is simply a DIFFERENT value from production - exactly the failure the route own comment names as 'the venus INTERNAL_EMAIL_KEY incident'. Nothing here justifies relaxing the check. TICKET HALF ALSO BLOCKED. /api/internal/support-action requires SUPPORT_API_KEY, a deliberately distinct env from INTERNAL_EMAIL_KEY, and mars/.env.local has no SUPPORT_API_KEY line at all. It is Vercel-only. My Vercel MCP connection has no project scope for mars (list_projects returns 0 while the team resolves), so I cannot read either value from the platform. Asked coder-mars-cc and db-mars-cc for both production values over DM, which is the sanctioned channel for secrets. The moment either arrives the send goes out unchanged - the copy is written and the recipient resolved. Recipient is on record in the users table for actorUserId 11eca897-31cd-46fe-8c5b-a54f289ecc26 and is deliberately not written into this row or any file.
-
NOTICE STILL UNDELIVERED. Exhausted every path that does not require Elazar. Four facts, each measured: 1. SUPPORT_API_KEY works. The value relayed by db-mars-cc and coder-mars-cc authenticates against production: GET /api/internal/support-action returns HTTP 400 'provide one of ticketId, ticketRef or ticketNumber', not 401. Both agents flagged it as unconfirmed against the platform; it is now confirmed. Auth passes, the request was merely incomplete. 2. It does not help here, because mars support-action has NO create op. The switch carries reply, note, close, reopen, notify-set, plus access-list and user-lookup. pluto has create; mars does not. shared/md/evolutiva-support-api.md is right that mars is family B. 3. reply would email the reporter, but there is nothing to reply to. Claudia has zero supportTickets rows ever, deleted or otherwise. No ticket, no reply, no email. Note supportTickets does carry createdOnBehalfByUserId, so a ticket on her behalf is expressible in the SCHEMA - there is just no sanctioned op that writes one, and I am not INSERTing it by hand. 4. Both INTERNAL_EMAIL_KEY caches are the same stale value. coder-mars-cc asked for a fingerprint rather than trading secrets: sha256 first 12 of mine is 252d4877292e at 66 chars, identical to theirs. Shipping theirs would have bought a second 401. db-mars-cc confirms no plaintext exists in the repo, Vercel holds it --sensitive so env ls and env pull never yield it, and it is absent from pm/06 entirely. Also correcting my own earlier diagnosis: I attributed the Vercel read failure to my connection lacking project scope. coder-mars-cc gets 0 projects too, scoped and unscoped, and has no vercel CLI on its seat. The platform read path is down for both of us, so a scope grant would not have fixed it and nobody should chase one. ONE REMEDY, and it is Elazar only: read INTERNAL_EMAIL_KEY off the Vercel dashboard, the single surface that displays a sensitive value, and relay it. No-rotation applies - nobody mints a replacement to route around this. Asked on the CLI. The Spanish copy is composed and the recipient resolved; delivery is one bearer away.
-
FIX VERIFIED BY REAL USER CREATES, AND THE OUTBOUND NOTICE IS NOW MOOT - NOT SENDING IT. The affected user came back on her own this morning and re-entered everything. Four practicas created 2026-09-28 08:48:18, 08:54:43, 09:00:21 and 09:20:45 (PRA-MARS-260907-01/-02/-03 and PRA-MARS-260921-01), all noRevisado, none deleted. Each carries 4 live practicaImages, 1 practicaTeeth row, and 7-10 practicaHistorialEntries rows. That last number is the effect-verification this row was held open for. practicaHistorialEntries is the exact table whose missing INSERT grant produced the 04:28Z failure; 31 rows written across four real app saves through the UI, by the app runtime role, is the grant working end to end - not a privilege row asserting it should. db-mars-cc's OPEN condition ('no real app/UI create has exercised the fix') is satisfied by a genuine user create, not by a synthetic one. Also measured: zero appEvents matching 'permission denied' anywhere fleet-wide since 04:38Z, and a practica-review-notify-sent/email-sent pair at 09:20:46 for PRA-MARS-260921-01, so the downstream notify path fired too. WHY THE NOTICE IS WITHDRAWN. Elazar authorized it (nqla 1a) on the premise that her work was lost and she had not come back. That premise is dead: she re-uploaded the images and re-entered the practicas herself, before any notice reached her. Mailing 'your save failed, please re-enter it' now would tell a student to redo work she has already redone. The authorization stands unused rather than overridden - if Elazar wants an apology or an explanation sent regardless, the copy is still composed and SUPPORT_API_KEY now authenticates, so it is one call away. The INTERNAL_EMAIL_KEY blocker is therefore no longer blocking anything on this row. It remains true (Vercel-sensitive, dashboard-only, no rotation) and belongs to whatever next needs the send-email route. Coverage: mars practica create path only, 04:38Z-09:50Z, one user. Edit and delete flows through the same four archive triggers were NOT exercised - the UPDATE/DELETE question raised at 04:36 is still unanswered by observation.
-
completed