Skip to content

System screens

OrpycaMCP's frontend (frontend/, SvelteKit 2 + Svelte 4 SSR, E22) is the only official presentation layer. No screen accesses data directly: every view has its own api/ subfolder acting as a Backend-for-Frontend (BFF) — it receives the browser's action, attaches the JWT (stored in an httpOnly cookie, never visible to client-side JS) and forwards the request to the api-gateway. See the full detail of this architecture in Architecture, "Authentication flow" section.

The navigation and the actions shown on each screen mirror the user's real RBAC (PERM_RADI, USUA_PERM_EXPEDIENTE, USUA_PERM_ADMIN, PERM_RADI_SALIDA, PERM_FIRMA, PERM_DLQ_ADMIN, …), but that restriction is only defense-in-depth in the UI — the backend always revalidates the permission and the clearance level (RF-SEG-08) per request.

What of this has been verified from the browser

This document is a functional inventory: it says what each screen does, not what has been checked. The distinction matters, because until August 2026 the project's guarantee was one of unit and contract, not of use: the 36 (app)/ screens had zero browser coverage.

In August 2026 the full walkthrough was executed —blocks B0 to B13, with the real acts, the irreversible ones included, against tenant_demo—. The procedure, the cases and the declared blind spots live in the test plan (Spanish only); the detail of each defect found and how it was closed, in the changelog.

The dominant pattern of that review is worth reading before the table that follows: the system already held the right value and the screen displayed a different one. None of those defects was one of calculation or of design, and none was caught by a test of a single side — which is why the buttons had to be pressed.

The negative sweep with a reduced-permission user (B13) confirmed what mattered most: the access controls do not live in the browser alone. Everything the interface hides was also invoked through the BFF proxy, and the backend answered 403 in every write case. The product decision it left open —TRD and PINAR reads requiring no permission, while the interface hid those screens— was resolved on 2026-08-28, and in parts: the classification vocabulary stays open to any authenticated user of the tenant (/radicar consumes it), the instrument's version circuit now requires USUA_PERM_TRD and PINAR as a whole USUA_PERM_ADMIN — see the test plan.

Public screens ((public)/)

Route Purpose
(public)/ Public landing page: what OrpycaMCP is, the regulatory framework (Ley 594/2000, Acuerdo AGN 060/2001), capabilities, a comparison with Orfeo and the AGPL v3 license. If locals.user is populated (an active session), its +page.server.js redirects 302 to /dashboard — an authenticated user never sees it
(public)/eliminacion · (public)/eliminacion/[id] Prior publicity of elimination. The inventory of what the institution is about to eliminate, visible without a session (RF-TRD-07 phase 2). It states until when one may object and through where: the existing single window, citing the batch code — a separate objection form would be a second inbox nobody watches. A classified expediente's denomination is not published and the page says so ("denomination reserved by law", citing the rule) instead of leaving a mute gap: the reader must be able to tell "it has no name" from "its name is reserved", because whether they have something to object to depends on it. The row is still there, with series, extreme dates and volume, because omitting it would fail the publicity duty precisely over what matters most to object to. An executed batch remains consultable as a record of what was eliminated, and a withdrawn one too, with its reason: the record that something was not eliminated matters to whoever read that it would be (2026-08-29)

Citizen procedures ((public)/, no account)

Two screens for someone who is not a user of the system. Requiring registration to file a right of petition would put up a barrier the law does not (Ley 1755/2015), so there is no session, no account and no mandatory identification.

They do not replace the counter window: /radicar remains the screen of the correspondence officer who registers whatever arrives in person, by post or by courier. They are two doors into the same procedure.

Route Purpose
(public)/radicacion Online single window. A procedure type from the catalogue the institution declares it accepts (admite_ventanilla_publica), sender —who may be a person or an entity—, subject, body and attachments. A single window receives everything that comes from outside (tutelas, official letters, invoices), not just PQRS: the vocabulary comes from the document-type catalogue and is opt-in, so silence never means "all". Routed by depe_rad_tp2, and 503 if the institution declared no single window rather than letting a document in with no destination. T13: it carries a mandatory personal-data authorisation checkbox (Law 1581; showing the version and link of the policy the entity publishes —served by GET /public/{tenant}/ventanilla/politica; the POLITICA_TRATAMIENTO_* variables are gone—; with no published policy filing is blocked, if it changes mid-form (409 politica_desactualizada) it reloads and asks again; a fixed note says deletion does not apply while the legal retention period runs, linking #supresion), an off-screen trap field (honeypot) and an Idempotency-Key per content, so retrying a submission returns the same number. Anonymous filing is legitimate
(public)/consulta Tracking by code. Returns number, type, state, date and responsible dependencia — not the content. It keeps a neutral answer for a code that does not exist and for one the caller may not see: an anonymous caller carries the lowest clearance

Authentication

Route Purpose
(public)/login Starts the OAuth2 Authorization Code + PKCE flow against Keycloak. Split composition: on mobile only the sign-in card; from 960px the brand panel appears. If Keycloak returns ?error=, it shows a message mapped from a closed table in Spanish (never the IdP's raw string) with a "Retry" link — which breaks the redirect loop
(public)/login/callback Receives the authorization code, exchanges it for tokens (server-side) and establishes the session
(public)/logout Closes the local session and the Keycloak session
/403 Access-denied page when the user lacks the required permission
+error.svelte (root) Application-wide error page — it did not exist: a 404 or an SSR failure fell through to SvelteKit's default screen, with no branding and no way out. Its own copy for 404, 403 and 503; for anything else it uses the message handleError set, which no longer includes the raw error text (it used to return things like connect ECONNREFUSED 172.18.0.7:8080). It publishes an 8-character correlation reference that is also written to the server log: it is the only technical datum shown, and it does not describe the failure. Deliberately self-contained (it does not use AppLayout): if the error is that the layout failed to load, an error page that depends on the layout cannot be rendered

Operational screens ((app)/)

Route Screen Purpose Typical role Main endpoints (via gateway)
/dashboard Personal panel Four personal modules visible without scrolling: 01 pending radicados (the inbox steps, GET /workflows/inbox, same source as the board), 02 recent activity, 03 alerts (open radicados — registered included — by due_date, RF-RAD-04), 04 favorites. The aggregate tenant metrics live in a secondary collapsible section (its «Dónde está el trámite» card reads by_ubicacion —current location of what is in process— not the registration dependencia), closed by default and gated by SGD_PERM_ESTADISTICA All authenticated users (metrics: SGD_PERM_ESTADISTICA) GET /api/v1/workflows/inbox, GET /api/v1/documents, GET /api/v1/reports/radicados (metrics section only)
/bandeja Inbox / case tray View, process, return, annul, or reply to assigned radicados; the list and detail say where the radicado is NOW (ADR-029): an «Ubicación actual» column (the flow-derived one; without it, the registration one — never one invented in place of the other; at ≤768 px, where the column is hidden, it moves to an «En X» line under the subject), and in the detail «Ubicación actual» —the first, highlighted row— with the person responsible when the flow has one (named by username, «Tú» if it is the viewer; never the raw identifier; if the profile cannot read the user catalog —PERM_RADI— it reads «A cargo de un responsable de la dependencia», with no error notice and no repeated call), «Cerrado en X» / «Anulado en X» for terminal states —which freeze the location— and, separately, the «Dependencia de radicación» (who it was addressed to; it does not change with the case). A radicado the flow has not moved yet says so («Sin repartir — en X»; an outgoing/internal one with no destination, «en la dependencia productora: X»; a terminal one, «Anulado/Cerrado sin pasar por el flujo — en X»); radicado status is never edited by hand (ADR-028): the detail drawer offers «Marcar en trámite», «Finalizar trámite» (mandatory closing reason —Respondido, No requiere respuesta (NRR), Traslado por competencia or Otro—, comment and confirmation: closes the radicado, irreversible; requires PERM_TRAMITAR at level ≥3 and holding the active step, or having held the last completed step when there is no active step) «Devolver» only to the holder of the active step (including a department's ownerless step) and «Asignar a flujo» only with no active step and to whoever held the last completed step or, with no steps, whoever registered the radicado (registered_by; if null —imported or system ingest— it is not offered and distribution is automatic or by cascade; R16); on the Board, «En trámite», Trasladar, Trámite, Completar, Revertir (only while the step is pending, R19) and dragging are for the step holder only (someone who sees it but does not hold it gets none); request/grant vistos buenos (individual or chained); adjust the security level; view/set the final disposition (TRD) of the individual radicado, with explicit confirmation for the deletion action; the detail drawer includes a "Respuestas" section with the linked Salidas Correspondence operator, dependencia officer GET/PATCH /api/v1/documents, /api/v1/workflows, /api/v1/documents/{id}/anulacion, /api/v1/documents/{id}/respuesta, GET /api/v1/documents/{id}/respuestas, GET/PATCH /api/v1/documents/{id}/disposition
/radicar Document registration Register an incoming, outgoing, or internal radicado with its attachments; a "Suggest from precedents" button (RAG with citations, F6) for the body of Salida/Interno documents, and suggested tipo documental chips (never auto-applied) next to the classification selector Front desk / correspondence operator (PERM_RADI, PERM_RADI_SALIDA) Recipients (RF-RAD-05, 2026-08-30): an outgoing document takes one or many, picked from the third-party catalogue. Search comes FIRST and the create form only appears when nothing matched — duplicates are not created by bypassing a constraint, they are created by not finding the record that already exists. Contact data can be corrected for that dispatch, and the screen says this does not change the catalogue. The channel only offers what the contact allows (no email, no electronic dispatch; no address, no physical) and warns naming the missing datum. The municipality is CHOSEN, not typed (DIVIPOLA cascade, 2026-08-30): department first, municipality second; changing the department clears the previous municipality instead of leaving it dangling; and if the catalogue does not answer you can carry on without a municipality, because what physical dispatch requires is the address. POST /api/v1/documents, POST /api/v1/storage/upload, POST /api/v1/knowledge/rag, POST /api/v1/knowledge/antecedentes
/borradores Drafts Prepare a radicado before assigning it a number; approve it or register it definitively; a "Suggest from precedents" button (RAG with citations, F6) in the body editor Drafting officer GET/POST /api/v1/documents/borradores, "register" action, POST /api/v1/knowledge/rag
/busqueda Document search Three deep-linkable tabs (?tab=): Exact (PostgreSQL FTS full-text with filters) with a column (on mobile, an «En X» line under the subject) and a filter by «Ubicación actual», a department-catalog selector («name — code»; it filters by ubicacion_dept_code, so a radicado with an ambiguous name, hence no code, does not appear; the «Código de dependencia de radicación» filter is the old one and they are distinct), Semantic (documents similar in meaning, vector similarity) and Precedents (related matters, by text or by a verified pivot radicado) — F6. Semantic/Precedent results are always presented as "similar", never as an exact match; the access-permission trimming is invisible by design Any user with query permission GET /api/v1/search, POST /api/v1/knowledge/search, POST /api/v1/knowledge/antecedentes, GET /api/v1/documents/by-tracking/{tracking}
/expedientes Expediente listing View open/closed/transferred expedientes, create a new one Archivist, officer with USUA_PERM_EXPEDIENTE GET/POST /api/v1/expedientes
/expedientes/[id] Expediente detail Add/remove radicados, view attachments, close the expediente, download the signed electronic index XML, export a ZIP, start a transfer; view (read-only) the final disposition materialized at closure or, while still open, a preview of the assigned TRD series rule Archivist GET/PATCH /api/v1/expedientes/{id}, GET /api/v1/expedientes/{id}/indice, GET /api/v1/expedientes/{id}/export, GET /api/v1/trd/{serie_id}
/archivo-fisico Physical archive Manage locations (recursive addresses), conservation units, loans and their FUID Central archive custodian GET/POST /api/v1/ubicaciones, /api/v1/unidades, /api/v1/prestamos
/transferencias Document transfers Send/receive/reject transfers between the active archive and the central archive, generate the FUID Archivist, dependencia head POST /api/v1/transferencias, send/receive/reject actions
/transferencias/[id] Transfer detail (acta as certificate) View the acta de entrega presented as a certificate (what / between whom / when), the institutional XAdES seal, and sign it personally as elaborador/remitente/receptor — each person's role is derived from their identity, never chosen; verify seal and signatures on demand Signature unobtainable (AREA-07-10, ADR-030): under the acta, the "Firmas inobtenibles" section lists the records (valid and revoked told apart, with seal status, the salted commitment of the grounds, chain ordinal, resolved support and people, WORM state and chain-link check; the grounds text is visible only to a level-3 administrator) and offers declare (a party with no signature or valid exception —after a revocation it can be declared again—, catalogue reason, 20–2000-character grounds, supporting radicado and attachment picked from lists (both required; "support not visible at your level" when clipped), TOTP code always offered (the server requires it from anyone holding a signing certificate), and an explicit confirmation that it is irreversible except by revocation) and revoke (closed-vocabulary cause, grounds, TOTP). Level-3 administrators also get "Show grounds" (loads the text on demand; each read is logged), "Verify grounds", "Download envelope" (the signed XML is downloaded, never rendered) and "Retry protection" (WORM) when a link is not protected. The gate is USUA_PERM_ADMIN ≥ 3 and PERM_FIRMA ≥ 2, hidden from whoever created, sent or received the transfer. Completeness tells firmada_con_excepcion from sellada_con_excepcion ("no party signed; only the entity seal"), counts unsigned records (not valid) and shows advertencias[] and the XAdES policy. A party with a valid exception does not see "Firmar como …". Archivist, dependencia head with USUA_PERM_EXPEDIENTE (personal signing additionally requires PERM_FIRMA de facto, the backend gate) GET /api/v1/transferencias/{id}, .../acta, .../acta/verificacion, POST .../acta/firmar-personal, POST .../acta/firmar POST .../acta/firma-inobtenible, POST .../acta/firma-inobtenible/revocar, GET .../firma-inobtenible/{id}/fundamento/verificacion, GET .../firma-inobtenible/{id}/fundamento?acto=, GET .../firma-inobtenible/{id}/sobre, POST .../firma-inobtenible/{id}[/revocacion]/worm/reintentar
/firmas Electronic signatures Sign (individually or in batch) pending radicados/expedientes, reject a signature Authorized signer (PERM_FIRMA) GET/POST /api/v1/signature, /api/v1/signature/cadena
/envios Postal shipments Manage dispatches to the external postal operator, their delivery confirmation, and "Update tracking" (a PULL query against the operator, F5) which states explicitly when there is no live integration (operador_conectado=false) instead of faking a real query. T13: moving a shipment by hand to "Delivered" asks you to attach the delivery receipt (uploaded to the store and tied to the shipment by its SHA-256) Correspondence operator POST /api/v1/documents/{id}/envios, GET /api/v1/envios, GET /api/v1/envios/{id}/tracking, incoming operator webhook
/anulaciones Resolve annulments The work queue of the annulment approver (a role distinct from the requester, who files the request from /bandeja with PERM_SOL_ANULAR): search by tracking number or subject, review the request in the drawer (causal, motivation, dates, remarks) and approve or reject with a mandatory motivation. Approving is the destructive, irreversible act (the radicado becomes anulado) and therefore uses variant="danger" with an explicit "this cannot be undone" warning, while rejecting is the conservative one. The motivation is mandatory even though the backend schema makes it optional — it is an act with legal effect and an audit trail (Ley 594/2000 art. 28) — and the proxy rejects with 422 instead of silently truncating at 2000 characters. Limitation stated on screen (a permanent banner, not glossed over): the backend exposes no queue of pending annulments, only per-radicado state, so the screen forces a radicado-by-radicado search; no pagination (20 results per search) and no bulk actions. Interface honesty: "the search server could not be queried" is a distinct state from "no radicado matches", and the legitimately-empty state adds that "this does not mean there are no pending annulments"; each row resolves its own state, with dedicated chips for Sin solicitud / No se pudo consultar and a per-row retry button Annulment approver (PERM_PANU_CODI, the backend's real gate on POST /anulacion/{aprobar,rechazar}). UI↔backend mismatch: GET /documents/{id}/anulacion and GET /search, which the screen also consumes, carry no require_permission in the backend today (only tenant isolation and clearance no-read-up) — for those two the screen's gate is the only restriction GET /api/v1/search, GET /api/v1/documents/{id}/anulacion, POST /api/v1/documents/{id}/anulacion/aprobar, POST /api/v1/documents/{id}/anulacion/rechazar
/perfil My profile Four sections in a deliberate order: identity (user, email, tenant, roles and effective permissions, read-only, with no extra call); TOTP second factor (Stepper generate secret → verify code → active, with replacement and revocation behind a step-up with the current code); PKI signing credential (request a CSR, view validity, replace or revoke), and active dependencia (dependencia switcher). The TOTP secret and the CSR are shown once only, and the screen says so: if they are lost, the only way out is generating new ones. The signing section is blocked until TOTP is active, with an in-page link to the required section, instead of letting the user crash into the backend error. Replacing an active credential revokes it immediately, leaving it unusable until an offline re-issuance, so it requires the TOTP code and typing an exact confirmation phrase — a deliberately higher bar than the TOTP replacement. Stated limitations: no cryptography in the browser (the private key is generated and held by auth-service), no QR code for TOTP, certificate issuance is out of band, and there is no password change, identity editing or session management. Explicit legal note: server custody means there is no sole control by the signer — functional equivalence of an electronic signature (Ley 527/1999 art. 7), not an accredited digital signature (art. 28). The two remote facets load via Promise.allSettled, each carrying its own error notice It includes the second factor recovery codes (shown exactly once on activation, with a warning at ≤2 left and an option to regenerate) and hides the signing-credential actions from users without PERM_FIRMA. Any authenticated user (own identity: the /auth/me/totp, /auth/me/signing-key and /auth/context routers authorize by identity, with no permission). UI↔backend mismatch: POST /auth/me/signing-key/enroll does require PERM_FIRMA in the backend and the UI does not mirror it — a user with TOTP active but without PERM_FIRMA sees an enabled button and gets a 403 surfaced as a generic failure; the navItems.js comment claiming that every consumed router "only requires get_caller" is false for that endpoint. Further asymmetry: self-revocation (DELETE /auth/me/signing-key) does not require PERM_FIRMA, only the TOTP step-up GET /api/v1/auth/me/totp, POST /api/v1/auth/me/totp/{enroll,activate}, DELETE /api/v1/auth/me/totp, GET /api/v1/auth/me/signing-key, POST /api/v1/auth/me/signing-key/enroll, DELETE /api/v1/auth/me/signing-key, GET /api/v1/auth/context, POST /api/v1/auth/context/switch
/reportes Reports Generate and export (CSV) records-management reports; the summary separates «Por ubicación actual (en trámite)» (by_ubicacion: where what is still in process is now, excluding closed and annulled) from «Por dependencia de radicación» (by_dependencia: who it was addressed to); the server CSV renames the dependencia section to dependencia_radicacion and adds ubicacion_actual; "Classified index" tab (Ley 1712 art. 20) — a content-free register of reserved/classified radicados with their legal grounds, without exposing subject or content Dependencia head, administrator (classified index: PERM_RECLASIFICAR) GET /api/v1/reports/radicados, GET /api/v1/reports/indice-reservado[.csv]
AssistantDock (not a route) Conversational assistant Natural-language chat that queries the system (RAG with citations), translating the question into read-only MCP calls. It is a floating panel mounted in AppLayout.svelte, available from any screen: there is no /asistente page, only the BFF proxy (app)/asistente/api/message Any authenticated user POST /api/v1/assistant/message (via mcp-server)
/admin Administration index Card map of the administration panel (18 areas today). Each card declares the real require_permission of the router it consumes, not a generic USUA_PERM_ADMIN — hence TRD / CCD, TVD and Metadatos require USUA_PERM_TRD, Colas requires PERM_DLQ_ADMIN, Preservación requires USUA_PERM_EXPEDIENTE, and Interoperabilidad opens with USUA_PERM_EXPEDIENTE or PERM_RADI. A card the user cannot use is shown disabled, not hidden. Corrected in batch 1 of the UX review of 2026-08-02, which also linked the two screens that existed but appeared nowhere Administrator (USUA_PERM_ADMIN for the panel; each card by its own permission) —
/admin/preservacion Digital preservation administration View and update the (versioned) preservation plan and its history; declare the formats at risk of obsolescence (formatos_origen, RF-PRE-04) — the policy consumed by the periodic migration sweep, which lives here rather than in a deployment environment variable precisely because the institution declares it, it stays versioned and it records who set it; operational visibility of electronic indexes pending XAdES sealing (with manual retry) and of WORM artefacts (index/acta/AIP) pending renewal of a Conservación Total retention. It does not offer manual AIP packaging or manual WORM protection of an arbitrary artefact: both need data (documentos[]/file_id) that only archive-service knows in the context of an expediente, and retention renewal is by design system-triggered only. It was not linked — reachable only by typing the URL — until batch 1 of the UX review of 2026-08-02, which added it to the /admin card index Archivist, administrator (USUA_PERM_EXPEDIENTE; sealing retry also needs PERM_FIRMA) GET/PUT /api/v1/preservacion/plan, GET /api/v1/preservacion/plan/versions, GET /api/v1/expedientes/indices/pendientes-firma, GET /api/v1/expedientes/indices/pendientes-renovacion, POST /api/v1/expedientes/{id}/indice/firmar
/admin/plantillas Template administration CRUD of reusable body templates for outbound documents, with a rich text editor. It is not a form builder: metadata templates (a JSON Schema per TRD series / tipo documental) are administered in /admin/metadatos Administrator (USUA_PERM_ADMIN) GET/POST /api/v1/plantillas
/admin/trd TRD / CCD administration Two tabs: TRD/CCD series in an indented tree (the real parent_id hierarchy, never a flat list) with create/edit (immutable code, append-only outside borrador); tipos documentales (third level), with create/edit/delete and a per-series filter. Convalidation circuit (approve → convalidate → register RUSD → repeal, plus return) via the shared InstrumentLifecycleActions component (ADR-026) — every irreversible act (convalidate, repeal) warns inside the confirmation dialog, before confirming Fourth tab «Disposición vencida» (RF-TRD-08, 2026-08-28): expedientes whose central archive has come due per the calendar frozen at closing time, with the due action per row. The screen states plainly that coming due is not deleting — under Conservación Total it means transfer to the historical archive and preserve forever — and that the system executes no act yet. A deletion row whose frozen instrument is not validated says so, instead of letting the officer find out by hitting the gate. The same tab is where the decision is made (RF-TRD-06): confirming records that the TRD action is due — with ejecutada: false and a warning that nothing has been deleted — and deferring requires a reason and a return date. The buttons mirror the real gate (USUA_PERM_TRD with crud≥3) and fail closed. TRD administrator (USUA_PERM_TRD, the backend's real gate — more specific than USUA_PERM_ADMIN, which only controls the visibility of the /admin panel) GET/POST /api/v1/trd, PATCH /api/v1/trd/{id}, GET/POST /api/v1/trd/{code}/versiones, POST /api/v1/trd/{code}/versiones/{version}/{aprobar,devolver,convalidar,registrar-rusd,derogar}, GET/POST/PATCH/DELETE /api/v1/tipos-documentales
/admin/tvd TVD administration (accumulated fonds) Structural mirror of /admin/trd (ADR-026): a tree of valuation groupings with its own columns — fonds/producing unit, extreme dates, instrument state — and no active-archive column (there is none for a TVD: an accumulated fonds is already in the central/historical archive). The creation form requires extreme dates and treats the valuation rationale as the main field, not a side note. Same convalidation circuit as TRD, same shared component TRD administrator (USUA_PERM_TRD — ADR-026 deliberately reuses the TRD permission: same archival authority, same Comité) GET/POST /api/v1/tvd, PATCH /api/v1/tvd/{id}, GET/POST /api/v1/tvd/{code}/versiones, POST /api/v1/tvd/{code}/versiones/{version}/{aprobar,devolver,convalidar,registrar-rusd,derogar}
/admin/eliminacion Documentary elimination (batches) The page that was missing (RF-TRD-07, phases 1-3, 2026-08-29): a batch could be proposed from the due queue and there was nowhere to see it afterwards. Lists batches with their state, opens the frozen inventory — narrowed by the viewer's clearance, with the difference explained rather than left as a mute subtraction — and offers the three acts: publish the inventory, record the Committee's endorsement (minutes, date and administrative act) and withdraw the batch with a reason. The five phases are always on screen and the fifth is declared not built: whoever publishes an inventory must know three steps remain before anything is destroyed. The endorse button is not offered while the publicity period is still open, and the page says until when — this is not a technical limit, it is that a third party may still object. From the endorsement onwards, the elimination record section (phase 4): produce it — once, declaring the deletion method —, see its SHA-256 and sealing state, and retry the signature with the last failure reason in view. The record is shown whole or not at all: if it covers expedientes above the viewer's clearance the page says so ("cannot be shown trimmed: it would break its signature") instead of leaving a gap. And it states the signature is Law 527 art. 7, not an accredited digital signature. Once the record is signed, Execute the elimination appears (phase 5) — the only act in the system that destroys archival documents: the warning states what remains (headstone, record, audit) and what does not (content and denomination), and the button requires typing the batch code, because an irreversible act must not be one click away. The result stays in the modal, not in a toast: one must be able to read what was destroyed and, above all, the list of those that were NOT — left intact and without a headstone. An executed batch shows the headstones. A 403 on load is not presented as "no batches" TRD administrator (USUA_PERM_TRD, crud≥3 for the three acts — same permission as TRD/TVD: eliminating is not a separate authority, it is executing the convalidated instrument) GET /api/v1/eliminacion/lotes, GET /api/v1/eliminacion/lotes/{id}, POST /api/v1/eliminacion/lotes/{id}/{publicar,avalar,anular}
/admin/metadatos Metadata administration Three tabs: expediente metadata templates (per TRD series, archive-service) and document ones (per tipo documental, document-service) — both create-only (immutable; the JSON Schema is edited as text with syntax validation before submitting; a new version = a new record); reusable metadata elements (full CRUD). Split with /admin/trd: TRD administers the classification, this screen administers the fields of those schemas Administrator (USUA_PERM_ADMIN client-side; document-service's template/element routers do not gate by permission in the backend today — a domain finding documented in the code, not papered over in the frontend) GET/POST /api/v1/expediente-metadata/templates, GET/POST /api/v1/metadata/templates, GET/POST/PATCH/DELETE /api/v1/metadata/elements
/admin/catalogos Catalog administration Master-detail: the tenant's reference catalogs on the left, the selected catalog's items on the right (full CRUD); on mobile it degrades to two navigable levels with a "Back" button, never to cramped columns Administrator (USUA_PERM_ADMIN client-side; catalogos.py does not gate by permission in the backend today either) GET /api/v1/catalogos, GET/POST /api/v1/catalogos/{catalogo}, PATCH/DELETE /api/v1/catalogos/{catalogo}/{id}
/admin/parametros Parameter administration Three sections: tenant parameters (key→free JSON value), the holiday calendar (individual entries + a Ley 51/1983 preload with no external call), and a business-day calculator that shows the breakdown of weekends/holidays subtracted in the range (the backend only returns the resulting date; the breakdown is reconstructed client-side from the same already-loaded holiday catalog) Parameters: writing requires USUA_PERM_ADMIN at level 3 (below it the screen offers no edit or create, and a 403 is explained); firma_personal_require_xades is edited with an off/clasificados/todos selector (null shows as "inherited from the environment") and, when relaxing the policy, asks for a reason of at least 10 characters (PUT with {value, motivo}; 422 motivo_requerido). Administrator (USUA_PERM_ADMIN client-side; config.py explicitly documents that the permission gate is pending) GET/PUT /api/v1/config/params[/{key}], GET/POST/DELETE /api/v1/config/holidays, POST /api/v1/config/holidays/seed, GET /api/v1/business-days/calculate
/admin/usuarios User administration Paginated list of the tenant's users (username, email, state, ROOT chip) with creation and a detail drawer showing the user's effective permissions with their CRUD level and their URD (dependencia + role/group + a "principal" marker), with URD assignments added and removed. There is no editing, deactivating or deleting a user: rbac.py only exposes create + read + URD management, and the screen states this as a backend absence, not a UI omission. The "Grant ROOT" checkbox is shown only to someone who is already ROOT (ADR-024: the backend 403s otherwise) and fails closed when the caller's ROOT status could not be determined — a control that can only fail is not an option — and the value is recomputed on submit anyway, because hiding the checkbox is UI, not control. Deleting a URD is confirmed inline inside the drawer, never by stacking a Modal on top of it. Interface honesty: if the dependencia or group catalogs fail, the user list does not go down; only the URD form selects go empty, with their own "the dependencia catalog could not be loaded" hint Administrator (USUA_PERM_ADMIN, router-level gate in auth-service's rbac.py and urd.py). UI↔backend mismatch: GET /api/v1/dependencias, which the screen consumes as a catalog, has no permission gate in tenant-service (its writes do require USUA_PERM_ADMIN). It is also the only one of the four new /admin screens without its own lock EmptyState: someone entering without the permission sees the page shell plus a load error instead of a "no permission" message GET/POST /api/v1/auth/users, GET /api/v1/auth/users/{id}/permissions, GET/POST /api/v1/auth/users/{id}/urd, DELETE /api/v1/auth/users/{id}/urd/{urd_id}, GET /api/v1/auth/groups, GET /api/v1/dependencias?activo=true
/admin/grupos Group administration List/create groups; a detail drawer with the CURRENT members, the permissions already granted with their CRUD level, and the group's live clearance (RF-SEG-08), all read from the server when it opens and reloaded after every change. There is no group editing or deletion (the backend only has create+read). Until 2026-08-31 the drawer administered blind —it showed a "this session" list of what it had itself done— because a comment claimed the backend exposed no such reads: it did Administrator (USUA_PERM_ADMIN) GET/POST /api/v1/auth/groups, GET /api/v1/auth/permissions, GET /api/v1/auth/security-levels, POST/DELETE /api/v1/auth/groups/{id}/members/{user_id}, PUT /api/v1/auth/groups/{id}/permissions/{permission_id}, PUT /api/v1/auth/groups/{id}/clearance
/admin/dependencias Dependencia administration Tree view of the institutional org chart (GET /dependencias/tree); create, edit and activate/deactivate. Renaming or deactivating triggers an explicit confirmation modal (not a tooltip): dependencias are correlated by name, not by id/code, in workflow_rules.assign_to_dept and in in-flight flow_steps — both can be orphaned. It does not offer hard deletion (the backend has it, blocked when there are children, but this screen's scope is create/edit/deactivate) Administrator (USUA_PERM_ADMIN) GET /api/v1/dependencias, GET /api/v1/dependencias/tree, POST /api/v1/dependencias, PATCH /api/v1/dependencias/{id}
/admin/flujos/reglas Routing rules CRUD of the rules that automatically assign a radicado to a dependencia (E05): a table with sortable priority, state, destination and condition count, and a create/edit Modal with dynamic conditions (field/operator/value) and a match_mode (all AND / any OR). Three honesty decisions: (1) an incomplete or out-of-catalog condition blocks the save and names which one it is, replacing the previous behavior that dropped them silently — an administrator could believe three conditions had been saved when two were, or none, making it a catch-all rule; (2) saving a rule with no conditions requires a separate confirmation warning that it will apply to every incoming radicado; (3) a priority outside 1..999 returns an explicit 422 instead of being silently clamped to a value other than the one typed. PATCH is true partial editing (only the keys present are forwarded, because in the backend "present" means "replace"). Stated limitations: the four legacy columns (doc_type, doc_class, dest_dept, subject_contains) are deliberately not exposed — conditions[] is strictly more expressive and takes precedence in the engine — and since the backend returns no total for the listing, the screen requests one wide page and filters client-side instead of faking server pagination Administrator (USUA_PERM_ADMIN, per-endpoint gate on all seven in workflow-service/app/routers/rules.py). No mismatches GET/POST /api/v1/workflow/rules, GET/PATCH/DELETE /api/v1/workflow/rules/{id}
/admin/flujos/reasignacion Cascade reassignment Bulk transfer of every pending flow step of a user or a dependencia to another user, in four numbered sections: source (by user or by dependencia, with a selector over the real catalog — it used to be free text over a backend column name, where a typo silently reassigned zero or the wrong steps), destination, preview, and confirmation by typing REASIGNAR, enforced again server-side in the proxy (422) because the client is potentially hostile. A permanent warning banner: this is a bulk, irreversible action with no automatic undo. The preview is declared an estimate and the reason is explained to the user, not just in the code: workflow-service has no simulation endpoint, so the count comes from GET /workflows/pending — the same predicate as the reassignment, but with the caller's clearance filter (RF-SEG-08), which the reassignment does not apply since it is an act of custody — so the number can only fall short, never overshoot: radicados classified above the caller's clearance are not counted but are reassigned all the same. Changing the source after estimating invalidates the preview; after execution the exact step count is reported and the form is cleared to prevent a double submit Administrator (USUA_PERM_ADMIN, the real gate on POST /workflows/reassign/cascade). UI↔backend mismatch on all three reads: GET /workflows/pending has no require_permission (only tenant isolation and the clearance filter), GET /auth/users/pickable is gated by PERM_RADI — not USUA_PERM_ADMIN — and GET /api/v1/dependencias has no gate. Only the write act is genuinely reserved to administrators GET /api/v1/auth/users/pickable, GET /api/v1/dependencias?activo=true, GET /api/v1/workflows/pending, POST /api/v1/workflows/reassign/cascade
/admin/auditoria Audit log query Read access to the immutable audit_log with exact-equality filters (action, object type, reference, actor) resolved server-side, a drawer with the full entry (including the hash and the raw payload), and an integrity panel that verifies the hash chain against real PostgreSQL, uncached, both on load and via a "Re-verify chain" button. This is the screen that defines the inventory's interface honesty: the verdict has four states, never two — checking / intact / compromised / could not be checked; the fourth states literally that "this does NOT mean the chain is intact, it means it is not yet known", and there is a specific guard so that a 200 with an unreadable body does not resolve to "compromised", because asserting a break nobody verified is as false as asserting integrity, and it additionally raises an incident alarm with no incident. Read-only, no mutations. Two stated limitations: (1) AuditQueryRepository applies no clearance filter on object_ref, so an administrator with low clearance can see, in the log itself, the existence and metadata of actions on classified documents — the screen refuses to add a client-side filter ("it would be a false sense of security: the data has already reached the browser") and reports it as an open backend finding; (2) the api-gateway does not forward the X-Total-Count header (it is absent from FORWARDED_RESPONSE_HEADERS), so the total collapses to the page size and pagination beyond page 1 is unreachable today — the filters do work Administrator (USUA_PERM_ADMIN, router-level gate in auth-service/app/routers/audit.py, covering both listing and verification). No mismatches GET /api/v1/audit, GET /api/v1/audit/verify
/admin/notificaciones Notifications and webhooks Two independent facets loaded with Promise.allSettled — each with its own loading, error and empty state, so one failure never hides what did work: delivery history (date, recipient, subject, state, event, attempts, with a refresh button) and outbound webhooks (create with URL, event types and an optional HMAC secret; delete with confirmation). The history has no real pagination and the screen says so: the backend only accepts limit, with no total and no cursor, so an explicit note warns that the list "is NOT all notifications, it is a recent window" (the last 50). Risk-proportional framing: webhooks are presented as a data egress channel to a third party, and deletion warns about irreversibility by naming the URL — the destination stops receiving events, and going back requires creating a new subscription with a new secret Administrator (USUA_PERM_ADMIN: GET /notifications/history gated per endpoint and the webhooks router gated at router level in notification-service). No mismatches and no ungated endpoint GET /api/v1/notifications/history, GET/POST /api/v1/webhooks, DELETE /api/v1/webhooks/{id}
/admin/colas Error queues (dead-letter) Console for the dead-letter events of the three services that have one (ADR-021), in three tabs: Flujos (workflow), Notificaciones and Firma. Filtering by state and by origin_stream, a table with a state chip and a detail drawer with the failure and the full event envelope. Two actions, with confirmation proportional to damage: "Retry" is a plain confirmation warning that re-execution triggers the real side effect (it may include an actual email or a real distribution) and that a second failure leaves it unchanged; "Discard" is danger, requires a justification of at least 10 characters that lands in the immutable audit_log, and spells out exactly what is lost: the original message will never be reprocessed, that effect will never happen. The 10-character floor is enforced in the proxy for all three services (two of them only require 1) so the requirement does not silently change with the selected tab. Interface honesty: the backend's 404 is ambiguous by design (no-read-up, RF-SEG-08) and the proxy refuses to disambiguate it — the UI shows the honest union, "the event was not found, or your clearance does not allow you to see it"; a listing error takes precedence over a legitimately empty state. Real limitation: the screen shows a warning banner when the backend truncates the candidate set (X-Truncated), but the api-gateway forwards neither that header nor X-Total-Count, so that banner is dead code today and the total collapses to the page size PERM_DLQ_ADMIN (router-level gate in all three admin_deadletter.py of workflow, notification and signature), not USUA_PERM_ADMIN. No mismatches. It is not in the sidebar: the only way in is the /admin card GET /api/v1/{workflow,notification,signature}/admin/deadletter/, GET .../{event_id}, POST .../{event_id}/replay, POST .../{event_id}/discard
/admin/pinar PINAR plan listing MVP (Phase 7): list plans with a state filter, create a new version (always in borrador; reformulating = creating another version, history is preserved). There is no editing or deletion — a plan only changes through its life cycle in the workspace Without the permission, a lock that names it: the screen says which permission is missing instead of leaving a load error, and it obeys the backend even when the session says otherwise — the 403 travels as sinPermiso, not mixed in with network failures. Administrator (USUA_PERM_ADMIN), the real gate on every PINAR endpoint since 2026-08-28, reads included: previously the 11 GETs were authenticated only while the UI hid the screen GET/POST /api/v1/pinar/planes
/admin/pinar/[id] PINAR plan workspace An informational life-cycle Stepper (borrador→aprobado→en_ejecucion→cerrado, not clickable) + Summary and Dashboard tabs (progress by objective / by axis). Approve / move to execution / close actions with a confirmation Modal that warns about irreversibility BEFORE executing — approval permanently freezes the plan's structural content (an entry in the immutable audit_log); closing is terminal. The remaining gap was closed on 2026-08-29 (the screen consumed 6 of the 31 endpoints and the dashboard was always empty, declaring itself "a later PINAR phase" — a dashboard that can never be filled is not an empty state, it is a false green): three more tabs — Aspects and prioritization (the aspect×axis matrix, 1 to 10; priority is the sum and the backend computes it, the table is shown already sorted by it, and "not prioritized" is not painted as priority zero); Objectives and projects with append-only follow-up (a project's progress comes from its cut-offs, it is never typed in; a project with no objective is flagged because it does not reach the dashboard); and instruments articulated by reference. The screen mirrors the service's two lifecycles without overlapping them: structure is editable only in borrador, follow-up only when aprobado or en_ejecucion — never both — and the frozen-plan 409 is shown as-is instead of collapsing into "could not save" Administrator (USUA_PERM_ADMIN; the detail/dashboard is read-only for any authenticated user) GET /api/v1/pinar/planes/{id}, POST .../aprobar, POST .../ejecutar, POST .../cerrar, GET .../tablero
/correo Mailboxes The door that was missing. A short list of the mailboxes the person can work, each linking to its inbox. It exists because the inbox is gated with PERM_RADI — whoever works incoming mail is the correspondence unit — yet the only way into /correo/{id} was a button inside /admin/correo, which requires USUA_PERM_ADMIN: the very officer the screen is aimed at could neither reach it nor learn their mailbox's identifier. It distinguishes the three endings — could not ask, no mailboxes assigned, here they are — which without it all looked the same: a URL nobody knew how to type Registrar (PERM_RADI). Which mailboxes appear is narrowed by the backend's WHERE, not by the permission: personal ones for their owner, institutional ones for the assigned officers GET /api/v1/email/cuentas
/correo/[cuentaId] Inbox of an institutional mailbox The shared inbox of the mailroom (E19): the emails fetched from the mailbox, pending registration, ordered by date of receipt — not by the poll that brought them in: a mailroom serves in order of arrival. Each email is opened, the officer fills in what the email cannot know (competent dependencia, document type) and registers it with its attachments and the original message preserved; or sets it aside with a reason if it should not have been registered — and that decision leaves an entry in the immutable audit trail, because it is the one that makes the document disappear from the archive. An email another officer is working on shows as busy and cannot be opened: the backend already prevents it, but without showing it the second officer would find out by clicking. A poll every 12 s asks only for the summary — pending, busy, and the marker of the last change — and reloads the list only if it changed; if the operator has an email open it warns instead of reloading, so as not to wipe what they typed, and if the poll fails it says so instead of leaving a screen that looks alive. Closing the form releases the reservation, and the officer's own row offers "Release it": looking at an email and deciding it is not yours cannot lock it for fifteen minutes for the whole mailroom. Opening the screen does not sync: fetching from the mailbox is an act the operator triggers Registrar (PERM_RADI, the real gate on all seven inbox endpoints). Which mailbox each person reaches is narrowed by the backend in the WHERE — a personal account belongs to its owner alone, not even the administrator sees it — and the 404 does not distinguish "does not exist" from "out of your reach" GET /api/v1/email/cuentas/{id}/bandeja[/estado], POST .../bandeja/sincronizar, POST .../bandeja/{mensaje}/{reservar,liberar,radicar,descartar}
/admin/correo Email ingestion Configuration of the institution's mailboxes (E19), a capability that existed in the backend since F2 with no screen at all. Three things: the mailboxes, with creation of all three kinds — shared institutional, an officer's personal one, and several receiving at once — and for each one its filtering folder (the one someone drags into what actually gets registered) and its destination folder after registration; test connection, which returns the mailbox folders so they can be picked from a list instead of typed — a typo would surface as "ingestion brings nothing", indistinguishable from "no mail has arrived"; and the institution's OAuth application for Google or Microsoft, with the authorization flow and its return route. The screen explains why and not just what: it states that Google and Microsoft no longer accept a password over IMAP and since when, and flags an OAuth2 account still unauthorized — without that, the symptom would be "nothing arrives", indistinguishable from an empty mailbox. Nothing is registered here: each card links to its mailbox's inbox, which is where it is decided email by email Administrator (USUA_PERM_ADMIN, the real gate on the email/* endpoints). The narrowing of WHICH accounts each person sees lives in the backend: a personal one is seen only by its owner — not even the administrator — an institutional one by its assigned officers GET/POST /api/v1/email/cuentas, POST /api/v1/email/cuentas/{id}/{probar,oauth/iniciar,oauth/canjear}, GET/POST /api/v1/email/apps-oauth
/admin/correo/oauth/callback Mail OAuth2 return Not a working screen: it is the landing point where Google/Microsoft return the code after consent. It exchanges it, stores the custodied credentials and redirects to /admin/correo. Documented because a route with no row in this inventory looks like an oversight, and this one exists on purpose and is never navigated by hand Mail administrator (USUA_PERM_ADMIN) GET /api/v1/email/oauth/callback
/admin/seguridad/claves Administrative revocation of signing credentials Lets an administrator revoke another user's personal PKI signing credential (E17/F4), requiring an exact confirmation phrase to be typed. It is blind revocation, and the screen says so: no endpoint lists the signing state per user (only GET /auth/me/signing-key, one's own), so the tenant's users are loaded as a wide page and filtered client-side, and the backend answers 404 no_signing_key when the target has none. It was missing from the /admin card index — which presents itself as the complete administration map — until batch 1 of the UX review of 2026-08-02 Administrator (USUA_PERM_ADMIN) GET /api/v1/auth/users, POST /api/v1/auth/admin/users/{user_id}/signing-key/revoke
/admin/terceros Third-party directory Senders and addressees in the institution's catalogue. It lists and searches ignoring accents and case (pena finds Peña), shows which radicados each record appears in, and detects likely duplicates along two axes that are not worth the same: the same ID number is certainty — they are the same person — while the same normalised name is suspicion, because two namesakes may exist; that is why it shows and never merges on its own. The merge is irreversible and confirmed by typing a phrase. Merging neither deletes nor rewrites: the absorbed record's radicados keep naming it — two records existed on the day of the dispatch and one was used, and a database trigger prevents falsifying that — while the survivor inherits the history on read. The screen says so BEFORE confirming. It exists since 2026-09: until then a third party could only be born, as a side effect of picking an addressee while registering, with nowhere to review or consolidate it Whoever registers (PERM_RADI); the merge additionally requires CRUD level 3 GET/POST /api/v1/terceros, GET /api/v1/terceros/duplicados, GET /api/v1/terceros/{id}/radicados, POST /api/v1/terceros/{id}/fusionar
/admin/interoperabilidad Interoperability and batch loading Four tabs (Phase 8, last of the API↔UI drift closure): Export (interoperable ZIP package, downloaded client-side, with included/excluded counters by security level); Import (Stepper upload→validation→confirm→result; fixity/schema validation is atomic in the backend, and a 422 shows the EXACT file/checksum that failed, never a generic message); Batch document loading (up to 1000) and Batch expediente loading (up to 100), both with a confirmation modal stating the exact item count and 2s progress polling that distinguishes "could not query" from "no results yet". All four tabs work end to end; batch loading used to report no result (UX review 2026-08-02: the poller had kept the URL shape from before the gateway routing fix, and the progress panel was not reactive), fixed in batch 1 together with a contract test that pins the URL shape Batch with errors (2026-10-02): if the batch ends in completado_con_errores or failed, the documents tab says "El lote terminó con errores" with the count, lists the failed items (subject, reason in Spanish, technical text as secondary detail, attempts) and, only if the backend returns reprocesable and the user holds PERM_RADI, offers Procesar los faltantes with a confirmation (how many are processed; already-registered ones keep their number). After the 202 it polls again until done; 404/409 errors appear in Spanish. Proxy POST /admin/interoperabilidad/api/lotes/documentos/{jobId}/reprocesar → POST /api/v1/batch/documents/{job_id}/reprocesar. Administrator with USUA_PERM_EXPEDIENTE or registration operator with PERM_RADI: the gate is heterogeneous per tab — document batch loading requires PERM_RADI — so the screen opens with either one and each tab filters with its own permission, instead of offering what it will deny or hiding from someone who can use it POST /api/v1/export, POST /api/v1/import, POST/GET /api/v1/batch/documents[/{job_id}/status], POST/GET /api/v1/batch/expedientes[/{job_id}/status]
/_kit UI component kit Internal showcase of the Orpyca design system's components (development environment only, not production) Developers —

Registering a draft (B6): the confirmation modal now captures the minimum radicado data (AGN Agreement 060/2001 §5.2): the document type from the catalogue (pre-filled from the template only when it is in the catalogue), the origin department among the user's active ones for an outgoing document (with none, "Radicar" is not offered and the reason is explained), the draft's destination for an internal one, and the type's metadata (SchemaField). A 422 (metadata_validation_failed, datos_minimos_incompletos, invalid_dependencia) keeps the modal open with the error next to the field. The proxy forwards only tipo_documental, origin_dept_code, dest_dept_code and metadata. Stored metadata is read-only in /bandeja (detail) and /expedientes/[id] (SchemaValue); EditarMetadatosModal and TransaccionModal are not migrated to SchemaField because the backend does not allow editing those fields. In /envios, the delivery-receipt error is tied to the file field (aria-describedby).

Present on every (app)/ screen, provided by AppLayout and SidebarNav:

  • Global search (role="search" in the top bar): navigates to /busqueda?q=…, the same full-text search view with its advanced filters. Keyboard shortcut /, suppressed when focus is already in an editable field. On narrow screens it expands from an icon. Because the term travels in the URL, every search is linkable and shareable.
  • Favorites: every sidebar entry has a pin/unpin control (aria-pressed, never signaled by color alone). Pinned items head the menu and feed module 04 of the /dashboard. They are persisted in localStorage namespaced per tenant + user (lib/stores/favorites.js, with an SSR guard). The store does not filter by itself: it receives the already permission-filtered list from lib/utils/navItems.js, so a favorite whose permission is withdrawn disappears on its own. The NAV_ITEMS catalog and its RBAC filter are the single source shared by SidebarNav and the /dashboard.

Design notes

  • Orpyca design system v1.0: all reusable components (DataTable, Drawer, Modal, StatusChip, FormField, Button, Loader, Stepper, Card, EmptyState, Toast, UserPicker) live in frontend/src/lib/components/ui/ and use --op-* design tokens (colors/spacing/typography/radii/shadows/motion), never loose hex values (ADR-020). The single source is src/styles/_tokens.scss.
  • Contrast and use of the brand green: the v1.0 primary (--op-primary, #2A8C3A) measures 4.28:1 on white, below the 4.5:1 WCAG AA requires for normal text. The token is therefore split by role: --op-primary only for non-textual elements (borders, icons, focus rings, ≥3:1 threshold) and --op-primary-dark (#1C6B33, 6.56:1) for all text — including Bulma's $primary/$link — or the verified --op-*-fg pairs. The raw semantic colors (--op-warning, --op-error, …) are border/icon colors; their text version is --op-*-fg. --op-accent (#8DBF3C, 2.18:1) is never used as text on a light background.
  • Typography: Space Grotesk (display, headings, figures) + Public Sans (UI and body), self-hosted via @fontsource — no external CDN, for sovereignty and so that users' browsing is not leaked to a third party.
  • Motion: _tokens.scss declares a global prefers-reduced-motion rule that neutralizes every transition and animation site-wide; no screen needs to repeat it.
  • DataTable is the system's table: it exposes eight capabilities opt-in per prop (per-column filters, sorting with aria-sort, CSV export, configurable columns, saved views, multiple selection, render-level virtualization and in-cell editing) with the simple behavior as the default. Virtualization uses content-visibility rather than removing rows from the DOM: less aggressive, but it does not break the native <table> semantics for screen readers. It is used by /busqueda, /expedientes, /firmas, /envios and the four panels of /reportes; /bandeja and /archivo-fisico still use their own table (see the Roadmap).
  • Rich-text editing (replies, observations): uses TipTap 3, with the HTML sanitized server-side before saving and before rendering — raw user HTML is never interpolated.
  • CSV export from the browser neutralizes formula injection (prepends ' to cells starting with = + - @).
  • There are no screenshots in this documentation — the tables above describe the actual implemented functionality; to see it running, start the platform (see Getting Started) and navigate to http://localhost:19300.

See also

  • Architecture — how each screen connects to the microservices through the gateway.
  • API Reference — the full contract for each endpoint listed above.

Administrative writes at level 3. In /admin/dependencias, /admin/catalogos and /admin/pinar (list and workspace) write controls are offered only with USUA_PERM_ADMIN at CRUD level >= 3, like the backend; at level 1 or 2 the screen is read-only and a 403 is explained as insufficient level.