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.

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

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

Operational screens ((app)/)

Route Screen Purpose Typical role Main endpoints (via gateway)
/dashboard Personal panel Four personal modules visible without scrolling: 01 pending radicados, 02 recent activity, 03 alerts (open radicados by due_date, RF-RAD-04), 04 favorites. The aggregate tenant metrics live in a secondary collapsible section, closed by default and gated by SGD_PERM_ESTADISTICA All authenticated users (metrics: SGD_PERM_ESTADISTICA) 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; 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) 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), 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 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
/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 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 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; "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; 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 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/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) 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 members, per-group permissions and clearance (RF-SEG-08). There is no group editing or deletion (the backend only has create+read). The backend does not expose reads of the memberships/permissions/clearance already assigned to a group either (only add/remove), which the drawer states explicitly — the actions apply immediately with no way to show "the current state" 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 exposedconditions[] 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 twochecking / 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 Administrator (USUA_PERM_ADMIN) 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. Out of MVP scope (no UI, endpoints exist): critical aspects, prioritization, objectives, projects, follow-up, instruments, road map 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
/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/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 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

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.