Sortable/searchable/paged tables on /alumnos, /practicas and /mi-cursada - and REMOVE the /practicas fixed 200-row cap, which hides older records today and is a separate defect this row must not close without
Elazar directive 2026-08-29 (CLI, direct): "take advantage of sortable tables too, search boxes, display records per table secector, pagination, etc. Client always asks for that." That last sentence is the reason this is priority 1 and not a nice-to-have. It is a PREDICTION from Elazar across four apps: the client always asks. Building it now costs a port; building it after Puia asks costs a port plus a round trip plus the impression that we shipped a table nobody could use. MEASURED STATE OF TERRA, 2026-08-29 (coder02-terra-cc): grep -rlniE "sortable|searchParams.*sort|pagination|pageSize" terra/src/ returns NOTHING. Not one hit in the whole tree. Terra has exactly three table surfaces, all raw <table> with no sort, no search, no pagination and no page-size control: - terra/src/app/mi-cursada/page.tsx - terra/src/app/practicas/page.tsx - terra/src/app/alumnos/page.tsx PLUTO ALREADY HAS THE WHOLE SET, server-driven: - pluto/src/components/server-sortable-table-head.tsx - pluto/src/components/server-table-pagination.tsx - pluto/src/components/server-table-page-size-select.tsx - pluto/src/components/server-table-toolbar.tsx - pluto/src/components/filter-bar.tsx - pluto/src/components/chip-filter.tsx - pluto/src/components/sortable-table-head.tsx (client variant) - pluto/src/components/table-pagination-controls.tsx Note the SERVER- prefix on most of them: pluto drives sort/page/pageSize through searchParams and the SQL, not by sorting a fetched array in the browser. That is the correct shape to carry, and it is a different piece of work from dropping in a component - each of terra's three pages needs its query parameterised for ORDER BY / LIMIT / OFFSET and a stable tiebreaker column so pagination does not shuffle rows between pages. SCOPE: 1. Port the server-table primitives (depends on TERRA-124 for ui/ primitives - table.tsx, select.tsx and button exist or arrive there). 2. Parameterise the three page queries: sort column allowlist (never interpolate a client-supplied column name), direction, LIMIT/OFFSET, deterministic tiebreaker. 3. Search box per table. Decide per surface what it searches and say so in the UI - a search box that silently covers only one column is worse than none. 4. Page-size selector + pagination controls. 5. Empty-vs-filtered-empty must be distinguishable: "no hay prácticas" and "ningún resultado para tu búsqueda" are different sentences. pluto/src/lib/informe-filter-core.ts:69 carries this exact reasoning for its own filters. CONSTRAINT CARRIED FROM THE PORT RULE (Elazar, same message): "bring things over and make them better, dont just import blindly." /mi-cursada is SELF-SCOPED IN SQL (WHERE createdBy = $1 OR participante) and that predicate IS the authorization for the page - there is no second gate downstream. Any sort/filter/pagination rewrite of that query must keep the self-scope as a join/WHERE condition, not as a filter applied to a wider result set. Getting this wrong turns a pagination feature into a data-exposure bug on the one page every alumno uses.
Activity
-
DESIGN APPROVED WITH FOUR CHANGES, ALL ACCEPTED. Implementation is running; no push until the implementation report. THE PAGES ARE /alumnos, /practicas AND /mi-cursada - NOT /informes. Worth recording because the row's original text did not name them and the obvious guess was wrong. Measured live counts: /alumnos 141 cohort rows, /practicas 6 rows through vTerraPracticas, /mi-cursada viewer-specific with three users currently holding one to three rows each. All three are Server Components. A DEFECT THIS ROW WOULD HAVE FIXED WITHOUT EVER NAMING IT, now a separately checked deliverable. /practicas fetches only the newest 200 rows AND HAS NO ROUTE TO OLDER ONES. That is a live data-visibility defect today, it pre-dates this feature, and it is not an ergonomics gap. If TERRA-125 ever stalls or closes on the sortable-table half alone, the cap persists and nobody looks - a row closing on work it never named is a shape this project keeps repeating, so the cap is named in the title as well as here. THE JUSTIFICATION WAS DEFENDED ON ITS WEAKEST REASON AND HAS BEEN CORRECTED. The first report argued server-side paging because "141 is already beyond a useful default page" and growth is unbounded. That is false as an argument - 141 rows client-side is instant, and unbounded growth is a prediction rather than a measurement. The two real reasons were both in the same report and under-sold: the /practicas 200 cap is a delivery defect now, and /mi-cursada's creator-or-participant predicate IS the authorization boundary, so its rows must be filtered and paged in SQL rather than shipped to a browser to filter. Those two force server-side; once the shared layer is server-side, /alumnos uses it at no additional cost. A design defended by its weakest reason gets rejected on that reason later. PII BLOCKER, RAISED AND RESOLVED BY REMOVAL. The first design put patient name in the searchable corpus with q carried in URL state. A query string propagates without anyone writing code to send it - browser history, the Referer header on every outbound link, and Vercel access logs, none of which terra has a policy for or a way to purge. Terra already refuses to write personal data to appEvents and this is a worse carrier. Resolved by dropping patient name from /practicas search entirely rather than by building a POST path, on the reasoning that a second sensitive interaction path outside the shared URL-state contract is a bigger surface than the search is worth. Remaining corpus: practice number, date, module, surgery, site, submitting author, follow-up status. TEST PLAN CORRECTED AWAY FROM THE FALSE-GREEN SHAPE. The first plan said contract tests "must fail if the mi-cursada self-scope predicate is removed", which as a source-text assertion passes whenever the predicate appears anywhere in the file including somewhere that no longer runs - the same defect as TERRA-134's fixed-width source slice, wearing different clothes. Now behavioural: two users with rows, query as A, assert no B row appears; paging asserted by total count and by boundary records across the page 1 and 2 edge. Static assertions are kept only for the sort allowlist and the page-size ladder, which are literally about what is written down. ONE CITATION WITHDRAWN. The page-size ladder 10/25/50/100 default 25 was offered as a "fleet baseline". It is not a fleet measurement - it is what pluto uses. Values unchanged and unobjectionable; the phrase was doing work the evidence did not support. SECURITY SHAPE THAT MUST SURVIVE IMPLEMENTATION: each page supplies a static allowlist mapping display columns to SQL expressions, and SQL receives values as parameters, never identifiers. A deterministic id tie-breaker on every sort. /mi-cursada keeps its scope predicate in the inner query with search, order, limit and count applied outside it, so scope is never post-filtered in JS.
-
Sortable/searchable/paged tables on /alumnos, /practicas and /mi-cursada - and REMOVE the /practicas fixed 200-row cap, which hides older records today and is a separate defect this row must not close without
-
Design approved with four changes, all accepted. coder-terra-ca implementing.
-
2026-08-31 revision-local production read executed the /mi-cursada creator-or-live-participant scope for two existing viewers. Viewer 4304d643 returned practica numbers 2, 3, 4 only (all own); viewer 6ebaffae returned 1, 5, 6 only (all own). Neither set included the other viewer's rows. This is a one-time revision verification, not behavioral test coverage; TERRA-152 remains open for a fixture-backed DB harness.
-
Correction/expansion 2026-08-31: initial two-viewer read exercised only the creator arm (participant arm then unexercised); live census found viewer fe171c44 as a non-creator live participant of practica 6 created by 6ebaffae. The exact scope returned exactly that one row with included_as_live_participant=true. Thus both OR arms have revision-local live evidence. This remains one-time verification, not behavioral coverage; TERRA-152 remains open for the fixture-backed harness.
-
CLOSED. SHIPPED AND VERIFIED FROM THE PLATFORM RATHER THAN FROM THE PUSHER. f0d47eb45672554e91dccc11ceada06fb27b3277 on main, deployment dpl_BkbQ8C1BPYHTDBPxUbL9QLZU4wfF, readyState READY, target production, region gru1, aliased to terra-enamel.pensanta.com with aliasError null, build 30s. I read that out of the Vercel deployment record, not out of coder-terra-ca's report of what they pushed - the two are different claims and only one of them is checkable by someone who was not there. WHAT SHIPPED: a shared URL-backed server-table layer, src/lib/server-table.ts plus src/components/server-table-controls.tsx, carrying q, sort, dir, page and size across /alumnos, /practicas and /mi-cursada. Search normalised and capped at 100 characters, page sizes 10/25/50/100 defaulting to 25, sort columns from a per-route STATIC allowlist rather than the request, id as tie-breaker so paging is stable, and every value bound as a SQL parameter. 429 unit tests pass, lint clean but for the one pre-existing informes warning, typecheck and Next build pass. THE ROW'S TITLE MADE THE SECOND HALF NON-OPTIONAL AND IT LANDED: the /practicas fixed 200-row cap is gone. That cap was a live defect hiding older records from anyone with more than 200, not a paging preference, and the row was written so it could not close on the pleasant half alone. TWO THINGS I MADE THEM FIX BEFORE THIS COULD CLOSE, both about what a test measures rather than what the code does. Their first test plan for the /mi-cursada scope predicate asserted the clause's SOURCE TEXT - which passes whenever the clause appears anywhere in the file, including in a branch that no longer runs, and is the exact false-green shape TERRA-134 was filed on. I required behavioural; they could not write it, and said so rather than substituting the source assertion, which is how TERRA-152 got filed. And their first verification exercised only the creator arm of creator-OR-live-participant, because both viewers they tried happened to own their rows; they went and found viewer fe171c44, a non-creator participant on practica 6, and closed the second arm. CARRIED FORWARD, NOT CLOSED HERE: TERRA-152, no DB fixture or transaction harness in this repo, so /mi-cursada's authorization predicate remains untested behaviourally - it was untested before this row too, so this is a gap TERRA-125 surfaced rather than created. NOTED AND NOT A DEFECT: the deployment record shows githubCommitVerification unverified with Elazar as commit author, as on every terra commit. Agent attribution lives only in the trailer. Recorded so nobody later reads that field as evidence about who pushed.
-
completed