Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0bc6f52
fix: resuelve TD-005, TD-006 y TD-007
beyondnetPeru Aug 10, 2026
ae273ee
fix(web): definir las claves de i18n que los componentes citaban y ex…
beyondnetPeru Aug 10, 2026
ca75194
fix(web): cerrar cuatro ReferenceError en producción y honrar las pro…
beyondnetPeru Aug 10, 2026
e6ac301
fix(web): corregir restricciones genéricas y el tipo del diccionario …
beyondnetPeru Aug 10, 2026
51344dc
fix(web): alinear fixtures con el contrato real (plantillas, perfiles…
beyondnetPeru Aug 10, 2026
13a84fb
fix(web): reconectar props que los componentes compartidos declaraban…
beyondnetPeru Aug 10, 2026
4d9d480
fix(web): el interruptor de configuración booleana guardaba siempre f…
beyondnetPeru Aug 10, 2026
c7382ea
fix(web): el desplegable de usuarios de ProfileForm enviaba undefined
beyondnetPeru Aug 10, 2026
58737a8
fix(web): un alias que solo existía en Vite ocultaba los errores del …
beyondnetPeru Aug 10, 2026
6750de5
fix(web): el árbol de recursos de dominio del perfil reventaba al pintar
beyondnetPeru Aug 10, 2026
ece1987
fix(web): registrar un proveedor de identidad daba 400 con la estrate…
beyondnetPeru Aug 10, 2026
a6419a6
fix(web): el filtro por estado de delegaciones no filtraba nada
beyondnetPeru Aug 10, 2026
7cf42e8
fix(web): TenantSelect ignoraba el disabled que acababa de declarar
beyondnetPeru Aug 10, 2026
963cf78
fix(web): el prefetch de delegaciones llamaba a un método inexistente
beyondnetPeru Aug 10, 2026
4ae83e3
fix(web): cascadear un permiso por un submenú enviaba un targetType q…
beyondnetPeru Aug 10, 2026
a3d7018
test(web): alinear los dobles y fixtures que describían formas que el…
beyondnetPeru Aug 10, 2026
0bb1642
test(web): las pruebas de GraphQL afirmaban sobre campos que la consu…
beyondnetPeru Aug 10, 2026
b8e44f3
test(web): tipar los dobles de store y alinear los fixtures restantes
beyondnetPeru Aug 10, 2026
321fe2d
feat(ci): encender el typecheck del front y cerrar TD-004
beyondnetPeru Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@
"runtimeArgs": ["nx", "run", "app-web:dev"],
"port": 5173,
"cwd": "/Users/beyondnet/Source/ums/src"
},
{
"name": "ums-api",
"runtimeExecutable": "dotnet",
"runtimeArgs": [
"run",
"--project",
"Ums.Presentation/Ums.Presentation.csproj",
"--no-launch-profile",
"--",
"--environment",
"Development",
"--urls",
"http://localhost:5080",
"--ConnectionStrings:DefaultConnection",
"Host=localhost;Port=5432;Database=UmsDev;Username=postgres;Password=Your_password123;"
],
"port": 5080,
"cwd": "/Users/beyondnet/Source/ums/src/apps/ums.api"
}
]
}
5 changes: 4 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,10 @@
"Bash(curl -s -m 10 -X POST -H 'Content-Type: application/json' -d '{\"tenantCode\":\"RANSA_PERU\",\"username\":\"gerente.operaciones@ransa.pe\",\"password\":\"root\"}' http://127.0.0.1:8082/api/v1/auth/login)",
"Bash(curl -s -m 10 -X POST -H 'Host: ums.local' -H 'Content-Type: application/json' -d '{\"tenantCode\":\"RANSA_PERU\",\"username\":\"gerente.operaciones@ransa.pe\",\"password\":\"root\"}' http://127.0.0.1:8081/api/v1/auth/login)",
"Bash(curl -s -m 10 -X POST -H 'Content-Type: application/json' -d '{\"tenantCode\":\"NEPTUNIA\",\"username\":\"gerente.operaciones@neptunia.pe\",\"password\":\"root\"}' http://127.0.0.1:8082/api/v1/auth/login)",
"Bash(command -v dotnet-ef)"
"Bash(command -v dotnet-ef)",
"Read(//Users/beyondnet/Source/**)",
"Bash(git -C /Users/beyondnet/Source/ums remote -v)",
"Bash(gh pr merge *)"
]
}
}
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ jobs:
- name: Run Global Linter
run: npx nx run-many --target=lint --projects=app-web

# Va ANTES de las pruebas a propósito: un error de tipo es más barato de leer que el fallo
# en cascada que provoca. Hasta TD-004 no existía este paso, y la única comprobación de
# tipos era la que hace Vite al construir, que solo mira lo que entra en el bundle: ni las
# pruebas ni el código no alcanzado.
- name: Typecheck (tsc --noEmit)
run: npx nx run-many --target=typecheck --projects=app-web

- name: Run Unit Tests
# `--code-coverage` no es un flag válido de vitest/dotnet (vitest usa `--coverage`).
# Se corre el target `test` (vitest run); la cobertura se gestiona con test:coverage.
Expand Down
72 changes: 54 additions & 18 deletions docs/architecture/technical-debt.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,29 +67,47 @@

## [TD-004] The Web App Typecheck Has Never Actually Run

- **Status**: Confirmed
- **Status**: **Resolved** (2026-08-10)
- **Severity**: High
- **Component**: `src/apps/ums.web-app/tsconfig.app.json`, `src/apps/ums.web-app/tsconfig.node.json`
- **Description**: Both configs declare `tsBuildInfoFile` without `incremental` or `composite`. That combination is a **configuration error**, not a warning: `tsc` reports `TS5111` and exits *before* type-checking anything. Running `tsc --noEmit -p tsconfig.app.json` on a file containing a deliberately undefined identifier reports nothing and exits 0. The project believes it has a typecheck and does not have one.
- **How it surfaced**: During the 2026-08-09 resync a `useCallback` import was omitted from three components. The "typecheck" passed; **vitest** caught it at render time with `ReferenceError: useCallback is not defined`. A second, worse case slipped even further: a helper extracted into the wrong scope (`renderValue` declared inside an `if (!config)` guard while the consumer lived outside it) passed both the typecheck and the build, and would have been a runtime `ReferenceError` on opening the panel. No test covers that panel.
- **Inherited, not introduced**: `unimar-ums` carries the identical `tsconfig`, so its type errors are hidden the same way. This is not a regression of the resync.
- **Impact**:
- `vite build` uses esbuild and does **not** type-check, so nothing in the local loop catches type errors.
- CI never runs `tsc` either: the `build` script is `vite build`.
- Enabling `"incremental": true` surfaces **545 pre-existing type errors**, several of them substantive — e.g. `Property 'menus' does not exist` on the authorization-graph node type, which suggests the graph schema (`nodes`) and its consumers (`menus`) have drifted apart.
- **Why it is not fixed here**: Landing 545 errors inside the resync PR without a triage plan would bury them. The fix is one line; the work is the backlog behind it.
- **Suggested resolution**:
1. Add `"incremental": true` to both configs and capture the full error list as a baseline.
2. Triage it — the `menus` vs `nodes` drift in the authorization graph is the first thing to look at, since it is the code path that decides what a user can see.
3. Add a `typecheck` script and wire it into CI so the gate stops being decorative.
4. Report the same defect upstream: the source platform has it too.
- **Related**: the three-component `useCallback` omission and the `renderValue` scope bug are both fixed in the resync branch.
- **Description**: Both configs declared `tsBuildInfoFile` without `incremental` or `composite`. That combination is a **configuration error**, not a warning: `tsc` reports `TS5111` and exits *before* type-checking anything. Running `tsc --noEmit -p tsconfig.app.json` on a file containing a deliberately undefined identifier reported nothing and exited 0. The project believed it had a typecheck and did not have one.
- **How it surfaced**: During the 2026-08-09 resync a `useCallback` import was omitted from three components. The "typecheck" passed; **vitest** caught it at render time with `ReferenceError: useCallback is not defined`. A second, worse case slipped even further: a helper extracted into the wrong scope (`renderValue` declared inside an `if (!config)` guard while the consumer lived outside it) passed both the typecheck and the build, and would have been a runtime `ReferenceError` on opening the panel.
- **Inherited, not introduced**: `unimar-ums` carries the identical `tsconfig`, so its type errors are hidden the same way. This was not a regression of the resync.

### Resolution

`"incremental": true` on both configs turned the gate on and surfaced **538 pre-existing errors** (367 production, 171 tests). All 538 are now cleared, `npm run typecheck` exists, and CI runs it as its own step before the unit tests.

What the gate had been hiding — each of these is a live defect, not a type-annotation nicety:

| Defect | Effect |
| --- | --- |
| `permission-cascade` mapped SubMenu to `'Page'` | `ExclusiveArcTarget` has no `Page`; cascading a permission through a submenu returned **400** |
| `SmartConfigInput` read `M3Switch`'s second callback argument | A boolean parameter **always saved `false`**, whatever the switch showed |
| `ListToolbar` called `onSearchSubmit()` with no event | Pressing Enter in any search box threw `Cannot read properties of undefined` |
| `IdpPanel` defaulted `provStrategy` to `'OIDC'` | Not a member of the backend enum; registering an IdP without touching the dropdown returned **400** |
| `ProfileForm` read `u.userId` / `u.userName` | The user dropdown rendered `undefined` and submitted `undefined` |
| `ProfileDomainResourcesPanel` read `node.items` | The field is `permissions`; the domain-resource tree threw on first render |
| `M3DataView` referenced `ChevronsUp` / `ChevronsDown` unimported | The collapse-header button threw in both branches |
| `ProfileForm` called `logger.error` unimported | The error handler died before `setError`; the screen went silent exactly when it had something to report |
| `useFormValidation` and the catalog form read `ZodError.errors` | Zod 4 renamed it to `.issues`; both threw at the moment validation failed |
| `NAV_PREFETCH_CONFIG.delegations` called `delegation.service.getAll` | No such method; hovering the nav item threw |
| Eight panels passed `footerElement` to `DataList` | The prop is `telemetryInfo`; the telemetry footer never rendered |
| `M3DialogAction` declared neither `disabled` nor `loading` | Confirm stayed live during submit and accepted a second click |
| `ErrorBoundary` rendered `t.errorSupportReference` without calling it | The support reference — the only actionable thing on that screen — never appeared |
| `@infrastructure` aliased in Vite but not in tsconfig | One module and its consumers were never type-checked at all |
| Ten GraphQL tests asserted on `getXxx` keys | The resolvers return `xxx`; the mocks returned whatever key they were given, so the tests verified nothing |

- **Related**: the GraphQL-key and fixture findings are the same class as the `menus`/`nodes` authorization-graph drift — a test whose double reproduces the bug confirms it instead of catching it.
- **Still owed**: report the same `tsconfig` defect upstream; `unimar-ums` has it too.

---

## [TD-005] Notification Bodies Are Logged in Cleartext, Unconditionally

- **Status**: Confirmed
- **Status**: **Resolved** (2026-08-10)
- **Resolution**: the simulator is now registered only when the environment is not Production; outside it, `UnconfiguredNotificationAdapter` throws instead of pretending to deliver — a password reset nobody receives surfaces when a user complains, an error at the start of the operation surfaces on the first attempt. The body is no longer logged at all: channel, recipient and subject are enough to trace a flow, and the line reports the body's length instead of its content. A real adapter is still missing; that is what `UnconfiguredNotificationAdapter` documents.
- **Original status**: Confirmed
- **Severity**: High
- **Component**: `Ums.Infrastructure/Services/Notifications/SimulatedNotificationAdapter.cs`, `Ums.Infrastructure/DependencyInjection.cs:88`
- **Description**: `SimulatedNotificationAdapter` is the **only** implementation of `INotificationService`, and it is registered with `services.AddScoped<INotificationService, SimulatedNotificationAdapter>()` with **no environment gate**. Its `SendAsync` writes the full notification — recipient, subject and **body** — at `LogInformation`. Notification bodies carry password-reset links, approval tokens and account-activation URLs. Wherever this runs, those land in the log sink (Loki) in cleartext, readable by anyone with log access.
Expand All @@ -112,7 +130,9 @@

## [TD-006] Migrating a Fresh PostgreSQL Database From Scratch Fails

- **Status**: Confirmed
- **Status**: **Resolved** (2026-08-10)
- **Resolution**: `20260607033700_UpdatePostgresMassTransitOutbox` now opens with an idempotent `CREATE EXTENSION IF NOT EXISTS pgcrypto;`, so the extension exists before the first `gen_random_bytes` call. The migrations are **not** reordered: reordering rewrites a history already applied in live environments. Verified the only way that counts — a genuinely empty database (`plpgsql` only) migrated all **32** migrations, the API started, and login against the seeded data worked.
- **Original status**: Confirmed
- **Severity**: Medium
- **Component**: `Ums.Infrastructure/Persistence/Migrations/PostgreSql/20260607033700_UpdatePostgresMassTransitOutbox.cs`, `…/20260607044815_AddPgCryptoExtension.cs`
- **Description**: `20260607033700_UpdatePostgresMassTransitOutbox` uses `gen_random_bytes(...)`, a **pgcrypto** function. The migration that declares the `pgcrypto` extension is `20260607044815_AddPgCryptoExtension` — over an hour **later** in the ordering. On an empty database the migrator therefore reaches the first migration before the extension exists and aborts with `42883: function gen_random_bytes(integer) does not exist`. The API never finishes starting.
Expand All @@ -126,10 +146,26 @@

## [TD-007] The Dev Connection String Points at a Port the Compose File Does Not Publish

- **Status**: Confirmed
- **Status**: **Resolved** (2026-08-10)
- **Resolution**: `appsettings.Development.json` now uses `Port=5432` and the compose password, so the documented path — bring up the local stack, run the API — works as written. The compose file wins as source of truth because CI and the container path already use it.
- **Original status**: Confirmed
- **Severity**: Low
- **Component**: `Ums.Presentation/appsettings.Development.json`, `src/infra/local/compose/docker-compose.yml`
- **Description**: `appsettings.Development.json` connects to `Host=localhost;Port=5433`, while the local compose publishes PostgreSQL on `5432:5432`. Running the API against the documented local stack fails to connect until the port or the connection string is overridden by hand. The passwords disagree too: `root` in appsettings, `Your_password123` in compose.
- **Inherited, not introduced**: identical in the source platform.
- **Impact**: the documented "start the local stack, run the API" path does not work as written. Everyone who has it working has an undocumented local override.
- **Suggested resolution**: pick one port and one password and make both files agree; the compose file is the better source of truth because CI and the container path already use it.

---

## [TD-008] Two Major Versions of Vite in the Same Tree

- **Status**: Confirmed
- **Severity**: Medium
- **Component**: `src/package-lock.json`, `src/apps/ums.web-app/package.json`
- **Description**: `npm ls vite` reports `vite@6.4.3` pinned locally by `app-web` and `vite@8.1.5` hoisted to the workspace root, pulled by `@vitejs/plugin-react`, `vitest@4.1.7` and the Storybook 10 builder. The build runs the local one (`npx vite --version` → 6.4.3) and the pairing with `@vitejs/plugin-react@5.2.0` is supported (its peer range covers `^6`), so this is not currently breaking anything at runtime.
- **How it surfaced**: TD-004. With the typecheck finally running, `tsconfig.node.json` failed because `@vitejs/plugin-react`'s types resolve `vite` to the hoisted 8.1.5 while `defineConfig` comes from the local 6.4.3, and `Plugin<any>` from one is not assignable to the other.
- **Interim measure**: `tsconfig.node.json` maps `vite` to the app's own copy — the one that actually builds — so the typecheck describes what runs. The mapping is a statement about resolution, not a silencer: it does not hide the duplication.
- **Impact**: the test runner and every plugin are compiled against a different major than the bundler that produces `dist/`. Nothing observable today; it is the kind of skew that produces an unreproducible failure later.
- **Suggested resolution**: converge on one major. `@vitejs/plugin-react@5.2.0` and `vitest@4.1.7` both accept 8, so raising the `app-web` pin is the smaller move — but it is a bundler major bump and deserves its own change, its own build verification and its own rollback story, not a corner of a type-cleanup branch.
- **Caveat**: this monorepo's `package-lock.json` must not be regenerated from scratch (it drops transitive deps); reconcile against the existing lock.
16 changes: 14 additions & 2 deletions src/apps/ums.api/Ums.Infrastructure/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,20 @@ public static IServiceCollection AddInfrastructure(
services.AddScoped<IFeatureFlagEvaluator, FeatureFlagEvaluator>();
services.AddScoped<IIdpConfigurationResolver, IdpConfigurationResolver>();
services.AddScoped<INotificationRecipientResolver, NotificationRecipientResolver>();
services.AddScoped<INotificationService, SimulatedNotificationAdapter>();
var isNotProduction = environment is null || !environment.IsProduction();

// TD-005: el simulador registra la traza en vez de entregar, así que fuera de desarrollo
// sería una entrega fantasma. Ahí se registra un adaptador que falla ruidosamente: un
// restablecimiento de contraseña que nadie recibe se descubre cuando el usuario reclama;
// un error en el arranque de la operación, en el primer intento.
if (isNotProduction)
{
services.AddScoped<INotificationService, SimulatedNotificationAdapter>();
}
else
{
services.AddScoped<INotificationService, UnconfiguredNotificationAdapter>();
}
services.AddScoped<IApprovalRequestCreationPolicyResolver, ApprovalRequestCreationPolicyResolver>();
// G-040: la auditoría automática se encola por el Transactional Outbox de MassTransit
// (entrega POST-commit al AuditTrailPersistenceConsumer), no por un canal en memoria con
Expand All @@ -96,7 +109,6 @@ public static IServiceCollection AddInfrastructure(
services.AddScoped<Ums.Application.Common.Interfaces.IIntegrationEventPublisher,
Ums.Infrastructure.Messaging.MassTransitIntegrationEventPublisher>();

var isNotProduction = environment is null || !environment.IsProduction();
services.AddUmsFactories(isNotProduction);

services.AddScoped<ITenantParameterProvider, TenantParameterProvider>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ public partial class UpdatePostgresMassTransitOutbox : Migration
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// TD-006: esta migración usa `gen_random_bytes`, que vive en pgcrypto, y la migración
// que declara la extensión (20260607044815_AddPgCryptoExtension) va DESPUÉS. Sobre una
// base vacía el migrador llegaba aquí sin la extensión y abortaba con
// «42883: function gen_random_bytes(integer) does not exist», dejando la API sin
// arrancar. No se nota en entornos existentes, que ya la tienen instalada, ni en las
// suites, que usan el proveedor InMemory: nada migra jamás un PostgreSQL vacío.
//
// Es idempotente y se queda aquí en vez de reordenar las migraciones: reordenar
// reescribiría un historial ya aplicado en entornos vivos.
migrationBuilder.Sql("CREATE EXTENSION IF NOT EXISTS pgcrypto;");

migrationBuilder.DropTable(
name: "OutboxMessage",
schema: "ums_platform");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@
namespace Ums.Infrastructure.Services.Notifications;

/// <summary>
/// Simulated notification adapter for development. Logs messages to the console
/// instead of dispatching to a real channel. Replace with SmtpNotificationAdapter,
/// SendGridNotificationAdapter, etc. by swapping the DI registration.
/// Adaptador de notificaciones simulado, EXCLUSIVO de desarrollo: registra la traza en vez de
/// entregar por un canal real. Se sustituye por un adaptador de verdad (SMTP, SendGrid…)
/// cambiando el registro de DI.
///
/// <para>El cuerpo NO se registra (TD-005). Los cuerpos llevan enlaces de restablecimiento de
/// contraseña y tokens de aprobación: en el log son credenciales que funcionan, y durante todo el
/// tiempo de retención, que sobrevive de largo al token. Canal, destinatario y asunto bastan para
/// seguir un flujo; quien necesite el cuerpo en local que lo obtenga del propio flujo, no del
/// registro.</para>
/// </summary>
public sealed class SimulatedNotificationAdapter : INotificationService
{
Expand All @@ -21,11 +27,11 @@ public SimulatedNotificationAdapter(ILogger<SimulatedNotificationAdapter> logger
public Task SendAsync(UmsNotification notification, CancellationToken cancellationToken = default)
{
_logger.LogInformation(
"[SIMULATED NOTIFICATION] Channel={Channel} | To={Recipient} | Subject={Subject}\n{Body}",
"[NOTIFICACIÓN SIMULADA] Canal={Channel} | Para={Recipient} | Asunto={Subject} | Cuerpo omitido ({BodyLength} caracteres)",
notification.Channel,
notification.Recipient,
notification.Subject,
notification.Body);
notification.Body?.Length ?? 0);

return Task.CompletedTask;
}
Expand Down
Loading
Loading