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 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 |
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 | — |
Global navigation¶
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 inlocalStoragenamespaced 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 fromlib/utils/navItems.js, so a favorite whose permission is withdrawn disappears on its own. TheNAV_ITEMScatalog and its RBAC filter are the single source shared bySidebarNavand 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 infrontend/src/lib/components/ui/and use--op-*design tokens (colors/spacing/typography/radii/shadows/motion), never loose hex values (ADR-020). The single source issrc/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-primaryonly 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-*-fgpairs. 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.scssdeclares a globalprefers-reduced-motionrule that neutralizes every transition and animation site-wide; no screen needs to repeat it. DataTableis the system's table: it exposes eight capabilities opt-in per prop (per-column filters, sorting witharia-sort, CSV export, configurable columns, saved views, multiple selection, render-level virtualization and in-cell editing) with the simple behavior as the default. Virtualization usescontent-visibilityrather 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,/enviosand the four panels of/reportes;/bandejaand/archivo-fisicostill 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.