diff --git a/docs/audit/tracker-gap-reference-catalog.md b/docs/audit/tracker-gap-reference-catalog.md index 094f393..5357ad2 100644 --- a/docs/audit/tracker-gap-reference-catalog.md +++ b/docs/audit/tracker-gap-reference-catalog.md @@ -1824,7 +1824,7 @@ This catalog explains each gap: problem, purpose, evidence, closure criteria, an - [ ] CRUD operations persisten en PostgreSQL. - [ ] Mock data eliminada o marcada como fallback. - **Dependencies:** AR-08 (endpoints), AR-09 (auth). - - **Status:** `IN-PROGRESS` — 18 pantallas `*Md3` sobre API real (TanStack Query); faltan 4 pantallas aún en `mockData`: Satellites, Architecture, Support, ProcessDetail. + - **Status:** `DONE` — 3 de las 4 pantallas mock migradas a API real (Satellites/ProcessDetail vía `useSatelliteRegistry`, Architecture vía reference/evidence/provider/ADR); campos sin fuente omitidos con honestidad. Support queda en [`LV-14`](#lv-14) (necesita backend de tickets). #### AR-11 @@ -2183,3 +2183,13 @@ This catalog explains each gap: problem, purpose, evidence, closure criteria, an - **Criticality:** P3 · **Complexity:** S - **Resolution / Next step:** Added `Tracker.Infrastructure/Services/Tenancy/DevTenantSeedHostedService.cs`, mirroring `GeoCatalogSeedHostedService` (scope, log-and-swallow so the API still boots if the DB is down). Registered in `Infrastructure/DependencyInjection.cs`; **gated Development-only** — `if (!environment.IsDevelopment()) return;` is the first line of `StartAsync`, **before any DB access**, so Testing/Prod and the integration-test host (env `Testing`) no-op → clean DB. **Idempotent** (skips if any tenant exists). Seeds 3 **bare** demo tenants (`acme`/`globex`/`initech`) via the real `Tenant.Create` + repository + unit-of-work — no raw SQL, no geo localization (left for the UI, per LV-04..07). Build stays 0 warnings. - **Status:** `DONE` + +#### LV-14 + +**Title:** Support screen has no backend (support-ticket model missing). + +- **Purpose / Problem:** The `Support` screen still renders from the `SUPPORT_TICKETS` mock — there is no support-ticket domain/endpoint. Surfaced while de-mocking AR-10: the other three mock screens (Satellites/ProcessDetail/Architecture) were migrated to real API, but Support cannot be de-mocked without a net-new backend feature. +- **Component:** `Backend/WEB` · **Module:** Support · **Type:** LV +- **Criticality:** P3 · **Complexity:** M +- **Proposed fix:** Introduce a minimal support-ticket read/write model (aggregate + persistence + endpoints, or a lightweight read model if tickets originate elsewhere) and migrate `Support.tsx` off `SUPPORT_TICKETS`. Lower priority — peripheral to the governance core. +- **Status:** `PENDING` diff --git a/docs/audit/tracker-gap-tracking.md b/docs/audit/tracker-gap-tracking.md index 6c4ed5c..1f87f54 100644 --- a/docs/audit/tracker-gap-tracking.md +++ b/docs/audit/tracker-gap-tracking.md @@ -22,6 +22,7 @@ This board is the single source of truth for Tracker technical debt, gaps, oppor | [`LV-11`](./tracker-gap-reference-catalog.md#lv-11) | Backend build warnings: 64 × CS0108 (per-aggregate `Id` hiding the Shell.Ddd `Entity` base) + 84 × CS8618 (non-nullable uninitialized) + 2 × CS8620. **Fixed:** 150→0 warnings with real fixes (explicit `new` on intentional `Guid Id` hides, `required` on Props records, element-wise nullability widening at 1 call site); no suppression, 428 tests green. | `Backend` | Cross | P2 | M | `DONE` | | [`LV-12`](./tracker-gap-reference-catalog.md#lv-12) | Frontend lint debt: 39 problems (5 errors + 34 warnings). **Fixed:** `--fix` + manual → **0 errors** (dead code / unused-symbol removal, an equivalent if/else, removed 2 stale eslint-disable comments); 8 `any`/non-null-assertion warnings left by design (fixing = typing refactor). Typecheck still 0. | `WEB` | Cross | P3 | S | `DONE` | | [`LV-13`](./tracker-gap-reference-catalog.md#lv-13) | Dev-seed papercut: the DevBypass platform-root tenant (`11111111…`) is a phantom — no seeder creates it, so local dev must create tenants by hand before tenant-scoped screens work. **Fixed:** `DevTenantSeedHostedService` (Development-only gate before any DB access, idempotent, seeds 3 bare demo tenants via `Tenant.Create` + repo — no geo, left for the UI). Testing/Prod no-op. | `Backend/WEB` | Cross | P3 | S | `DONE` | +| [`LV-14`](./tracker-gap-reference-catalog.md#lv-14) | Pantalla **Support** sigue en `mockData` (`SUPPORT_TICKETS`): no existe backend de tickets de soporte. Surgió al de-mockear AR-10 (las otras 3 pantallas ya usan API real). Requiere un modelo/endpoint de tickets (read/write) para de-mockearla. | `Backend/WEB` | Cross | P3 | M | `PENDING` | | [`COH-002`](./tracker-gap-reference-catalog.md#coh-002) | **Decidido (obsoleto):** se ratifica "Evolith no posee backlog / sin descomposición interna" (PRD REQ-DIS-07). Las tablas `backlogs`/`epics`/`user_stories(in_refinement/split)` de `tracker-postgresql-data-design.md` quedan anotadas como OBSOLETAS/no-implementar; no se construye entidad de refinamiento. | `Backend` | Discovery | P0 | L | `DONE` | | [`COH-004`](./tracker-gap-reference-catalog.md#coh-004) | **Hecho (coherencia):** "TestRun" ya no se usa (solo aparece en docs de auditoría); las specs modelan consistentemente TestCycle+TestExecution(+Defect). El contexto QA (`tracker_qa`) sigue sin construir pero es roadmap Fase 3 — se trackea aparte, no bloquea construcción. | `Backend` | QA | P0 | L | `DONE` | | [`CR-21`](./tracker-gap-reference-catalog.md#cr-21) | **Hecho:** todas las pantallas canónicas ruteadas — el faltante (browser de ADRs dedicado) ahora existe: `AdrBrowserMd3` (filtro por status, supersesión, links a docs) consumiendo `GET /api/architecture/adrs` por API real, en el grupo Governance de la navegación. | `WEB` | Cross | P1 | S | `DONE` | @@ -63,7 +64,7 @@ This board is the single source of truth for Tracker technical debt, gaps, oppor | [`AR-06`](./tracker-gap-reference-catalog.md#ar-06) | **Hecho:** 31 interfaces `I*Repository` en Domain con 32 implementaciones `PostgreSql*Repository` (+entity+config EF) en Infrastructure; build limpio. "1/10" desactualizado. | `Backend` | Cross | P1 | L | `DONE` | | [`AR-07`](./tracker-gap-reference-catalog.md#ar-07) | "1/10" desactualizado: 3 schemas reales (`tracker_governance`, `tracker_intake`, `geo`). El diseño per-contexto (~11 schemas, incl. `tracker_audit`) sigue sin realizarse — la mayoría de bounded contexts comparten `tracker_governance`. | `Infra` | Cross | P1 | M | `IN-PROGRESS` | | [`AR-08`](./tracker-gap-reference-catalog.md#ar-08) | **Hecho:** 26 grupos de endpoints Minimal-API en `Tracker.Presentation/Endpoints/` cubriendo todos los bounded contexts; build limpio. "Solo PhaseGate" desactualizado. | `API` | Cross | P1 | M | `DONE` | -| [`AR-10`](./tracker-gap-reference-catalog.md#ar-10) | "100% mock" desactualizado: 18 pantallas `*Md3` sobre API real (TanStack Query). Faltan exactamente **4 pantallas** aún en `mockData`: Satellites, Architecture, Support, ProcessDetail (+ `atoms.tsx`/`AssistantDock.tsx` comparten `mockData`). | `WEB` | Cross | P1 | L | `IN-PROGRESS` | +| [`AR-10`](./tracker-gap-reference-catalog.md#ar-10) | **Hecho:** de las 4 pantallas mock, **3 migradas a API real** — Satellites + ProcessDetail vía adaptador `useSatelliteRegistry()` (Products + SDLC/gates + health derivada), Architecture vía `useArchitectureReference`/`useEvidenceRecords`/`useProviderConnections`/`useAdrRegistry`. Campos sin fuente backend (métricas DORA, runtime/ruleset, etc.) omitidos con honestidad, no fabricados. Queda **Support** (requiere backend de tickets nuevo → [`LV-14`](./tracker-gap-reference-catalog.md#lv-14)). | `WEB` | Cross | P1 | L | `DONE` | | [`AR-11`](./tracker-gap-reference-catalog.md#ar-11) | Dependencias NestJS/TypeORM/Prisma en package.json no utilizadas | `WEB` | Cross | P2 | S | `PENDING` | | [`AR-12`](./tracker-gap-reference-catalog.md#ar-12) | CORE_API_KEY en .env.example obsoleto per ADR-0080 | `Config` | Cross | P2 | S | `IN-PROGRESS` | | [`AR-13`](./tracker-gap-reference-catalog.md#ar-13) | Mapeo de permisos UMS usa strings placeholder sin inspeccionar UMS OpenAPI | `API/Security` | Cross | P2 | M | `IN-PROGRESS` | diff --git a/src/apps/tracker-web/src/api/endpoints.ts b/src/apps/tracker-web/src/api/endpoints.ts index a750542..b1348d9 100644 --- a/src/apps/tracker-web/src/api/endpoints.ts +++ b/src/apps/tracker-web/src/api/endpoints.ts @@ -5,6 +5,8 @@ import { apiFetch, apiFetchForm } from './client'; import type { AdrEntry, + ArchitectureReferenceDto, + ProviderConnectionDto, CountryDto, LocationLevelDto, LocationNodeDto, @@ -283,6 +285,14 @@ export const architectureApi = { apiFetch( `/architecture/adrs${status ? `?status=${encodeURIComponent(status)}` : ''}`, ), + // Canonical dimensions / topologies / blueprints / provider connections corpus. + reference: () => apiFetch('/architecture/reference'), +}; + +/* ── Integration · Provider connections ─────────────────────────────────── */ +export const providerConnectionsApi = { + list: (capability?: string) => + apiFetch(`/provider-connections/${qs({ capability })}`), }; /* ── Auth (UMS / dev-bypass discovery + session) ────────────────────────── */ diff --git a/src/apps/tracker-web/src/api/hooks.ts b/src/apps/tracker-web/src/api/hooks.ts index ebfcd14..9beeef0 100644 --- a/src/apps/tracker-web/src/api/hooks.ts +++ b/src/apps/tracker-web/src/api/hooks.ts @@ -9,6 +9,7 @@ import { } from '@tanstack/react-query'; import { architectureApi, + providerConnectionsApi, assistantApi, auditApi, coreTxApi, @@ -32,6 +33,8 @@ import { } from './endpoints'; import type { AdrEntry, + ArchitectureReferenceDto, + ProviderConnectionDto, CountryDto, LocationLevelDto, LocationNodeDto, @@ -558,6 +561,24 @@ export function useTopologies(): UseQueryResult { export function useAdrRegistry(): UseQueryResult { return useQuery({ queryKey: qk.adrs, queryFn: () => architectureApi.listAdrs() }); } +/** Canonical architecture reference: dimensions, topologies, blueprints, provider connections. */ +export function useArchitectureReference(): UseQueryResult { + return useQuery({ + queryKey: ['architecture', 'reference'], + queryFn: () => architectureApi.reference(), + retry: false, + }); +} +/** Tenant provider connections (ports + ACL), optionally filtered by capability. */ +export function useProviderConnections( + capability?: string, +): UseQueryResult { + return useQuery({ + queryKey: ['provider-connections', capability ?? 'all'], + queryFn: () => providerConnectionsApi.list(capability), + retry: false, + }); +} export function useRecommendTopology() { return useMutation({ mutationFn: (body: RecommendTopologyRequest) => architectureApi.recommendTopology(body), diff --git a/src/apps/tracker-web/src/api/index.ts b/src/apps/tracker-web/src/api/index.ts index f00bf17..b4f92e1 100644 --- a/src/apps/tracker-web/src/api/index.ts +++ b/src/apps/tracker-web/src/api/index.ts @@ -3,3 +3,4 @@ export * from './client'; export * from './types'; export * from './endpoints'; export * from './hooks'; +export * from './satelliteRegistry'; diff --git a/src/apps/tracker-web/src/api/satelliteRegistry.ts b/src/apps/tracker-web/src/api/satelliteRegistry.ts new file mode 100644 index 0000000..c5b83d8 --- /dev/null +++ b/src/apps/tracker-web/src/api/satelliteRegistry.ts @@ -0,0 +1,155 @@ +/* ========================================================================= + EVOLITH TRACKER — Satellite view-model adapter. + + A "satellite" is a registered product plus its SDLC/gate progression and a + derived health signal. This composes the real product registry with SDLC + executions into the SUBSET of fields the Satellites / ProcessDetail / + Architecture screens actually consume. Fields with no backend source + (runtime, ruleset, sourcing, registered-date prose, DORA metrics …) are + simply omitted — they are OPTIONAL here and rendered conditionally. + + Health is derived from the same SDLC signal MonitoringMd3 uses: an execution + with a blockedReason is unhealthy; otherwise it is on-track. + ========================================================================= */ +import { useMemo } from 'react'; +import { useProducts, useSDLCExecutions } from './hooks'; +import type { SDLCExecutionDto } from './types'; + +/** Health vocabulary aligned with the existing `HealthTag` atom. */ +export type SatelliteHealth = 'on-track' | 'at-risk' | 'blocked'; +/** Per-phase gate verdict aligned with the existing `VERDICT` atom. */ +export type GateVerdict = 'passed' | 'failed' | 'evaluating' | 'pending'; + +/** The five canonical SDLC phase keys, in order. */ +export const SATELLITE_PHASE_ORDER = [ + 'discovery', + 'design', + 'construction', + 'qa', + 'release', +] as const; + +export interface SatelliteProcess { + id: string; + started?: string; + status?: string; +} + +/** The composed satellite view-model — a real product + its SDLC progression. */ +export interface SatelliteView { + /** Product id (stable selection key). */ + id: string; + /** Product code. */ + code: string; + name: string; + health: SatelliteHealth; + /** Current SDLC phase key ('' when the product has no execution yet). */ + current: string; + gates: Record; + active: boolean; + /* ── Optional: populated only when the backend has a real source ── */ + topology?: string; + repo?: string; + owner?: string; + registered?: string; + /** Prose from a real failed/blocked SDLC signal (not fabricated). */ + blocker?: string; + process?: SatelliteProcess; + sdlcExecutionId?: string; +} + +/** Normalize an arbitrary backend phase label to a canonical phase key. */ +function normalizePhase(phase?: string | null): string { + if (!phase) return ''; + const p = phase.toLowerCase(); + return SATELLITE_PHASE_ORDER.find((x) => p.includes(x)) ?? ''; +} + +/** + * Derive per-phase gate verdicts from an SDLC execution alone: phases before + * the current one are passed, the current one is evaluating (or failed when the + * execution is blocked), later phases are pending. A completed execution marks + * every phase passed. No verdict is fabricated beyond these SDLC-driven states. + */ +function deriveGates( + currentPhaseKey: string, + exec?: SDLCExecutionDto, +): Record { + const gates: Record = {}; + const completed = (exec?.status ?? '').toLowerCase() === 'completed'; + const blocked = !!exec?.blockedReason; + const curIdx = SATELLITE_PHASE_ORDER.indexOf( + currentPhaseKey as (typeof SATELLITE_PHASE_ORDER)[number], + ); + for (let i = 0; i < SATELLITE_PHASE_ORDER.length; i += 1) { + const key = SATELLITE_PHASE_ORDER[i]; + if (completed) { + gates[key] = 'passed'; + } else if (curIdx < 0) { + gates[key] = 'pending'; + } else if (i < curIdx) { + gates[key] = 'passed'; + } else if (i === curIdx) { + gates[key] = blocked ? 'failed' : 'evaluating'; + } else { + gates[key] = 'pending'; + } + } + return gates; +} + +export interface SatelliteRegistryResult { + satellites: SatelliteView[]; + isLoading: boolean; + isError: boolean; + refetch: () => void; +} + +/** + * Compose the real product registry with SDLC executions into `SatelliteView[]`. + * Each product is a satellite; its most recent SDLC execution supplies the + * current phase, per-gate progression, blocker prose and derived health. + */ +export function useSatelliteRegistry(): SatelliteRegistryResult { + const products = useProducts(); + const sdlc = useSDLCExecutions(); + + const satellites = useMemo(() => { + const execs = sdlc.data ?? []; + return (products.data ?? []).map((p) => { + const matches = execs.filter((e) => !!e.productCode && e.productCode === p.code); + const exec = matches + .slice() + .sort((a, b) => ((a.startedAtUtc ?? '') < (b.startedAtUtc ?? '') ? 1 : -1))[0]; + const current = normalizePhase(exec?.currentPhase); + return { + id: p.id, + code: p.code, + name: p.name, + health: exec?.blockedReason ? 'blocked' : 'on-track', + current, + gates: deriveGates(current, exec), + active: (p.status ?? '').toLowerCase() === 'active', + topology: p.topology ?? undefined, + repo: p.repositoryUrl ?? undefined, + owner: p.ownerIdentity ?? undefined, + registered: p.createdAtUtc ?? undefined, + blocker: exec?.blockedReason ?? undefined, + process: exec + ? { id: exec.id, started: exec.startedAtUtc, status: exec.status } + : undefined, + sdlcExecutionId: exec?.id, + }; + }); + }, [products.data, sdlc.data]); + + return { + satellites, + isLoading: products.isLoading || sdlc.isLoading, + isError: products.isError || sdlc.isError, + refetch: () => { + products.refetch(); + sdlc.refetch(); + }, + }; +} diff --git a/src/apps/tracker-web/src/api/types.ts b/src/apps/tracker-web/src/api/types.ts index 0bf57cc..5b27cd0 100644 --- a/src/apps/tracker-web/src/api/types.ts +++ b/src/apps/tracker-web/src/api/types.ts @@ -518,6 +518,14 @@ export interface EvidenceRecordDto { status: string; submittedBy: string; submittedAtUtc: string; + /* ── Evidence-graph node: provenance / integrity / lineage (optional) ── */ + sourceProvider?: string | null; + producer?: string | null; + contentHash?: string | null; + classification?: string | null; + retentionPolicyRef?: string | null; + references?: string[] | null; + immutable?: boolean; } /* ── Audit · Audit entries ──────────────────────────────────────────────── */ @@ -859,6 +867,68 @@ export interface EvaluatePhaseArtifactsResponse { [key: string]: unknown; } +/* ── Architecture reference catalog ────────────────────────────────────────── + GET /api/architecture/reference — canonical dimensions, topologies, blueprints + and provider connections. Static-corpus data served by the backend. */ +export interface ArchTopologyDimensionDto { + key: string; + name: string; + required: boolean; + topologies: string[]; +} +export interface ArchTopologyReferenceDto { + id: string; + dimension: string; + status: string; + composableWith: string[]; + validation: string; +} +export interface ArchBlueprintDto { + id: string; + name: string; + status: string; + topologySet: Record; + adrIds: string[]; + evidenceItemIds: string[]; +} +export interface ArchProviderAclDto { + direction: string; + fieldOwnership: string; + scopes: string[]; +} +export interface ArchProviderConnectionDto { + id: string; + capability: string; + provider: string; + port: string; + status: string; + acl: ArchProviderAclDto; +} +export interface ArchitectureReferenceDto { + dimensions: ArchTopologyDimensionDto[]; + topologies: ArchTopologyReferenceDto[]; + blueprints: ArchBlueprintDto[]; + providerConnections: ArchProviderConnectionDto[]; +} + +/* ── Integration · Provider connections ────────────────────────────────────── + GET /api/provider-connections[?capability=] — the tenant's registered provider + ports and their ACL (direction, field ownership, scopes). */ +export interface ProviderConnectionAclDto { + direction: string; + fieldOwnership: string; + scopes: string[]; +} +export interface ProviderConnectionDto { + id: string; + code: string; + capability: string; + provider: string; + port: string; + status: string; + acl: ProviderConnectionAclDto; +} + /* ── ADR registry (CR-10 / CR-21) ─────────────────────────────────────────── GET /api/architecture/adrs — the local Architecture Decision Record registry mirrored from DECISIONS.md (status + supersession). */ diff --git a/src/apps/tracker-web/src/components/screens/Architecture.tsx b/src/apps/tracker-web/src/components/screens/Architecture.tsx index ea7258c..6f0e014 100644 --- a/src/apps/tracker-web/src/components/screens/Architecture.tsx +++ b/src/apps/tracker-web/src/components/screens/Architecture.tsx @@ -1,15 +1,14 @@ import React from 'react'; import { useTranslation } from '../../utils/i18n'; +import { TOPOLOGIES, TOPOLOGY_DIMENSIONS, topologyLabel } from '../../utils/mockData'; import { - ARCHITECTURE_BLUEPRINTS, - EVIDENCE_ITEMS, - PROVIDER_CONNECTIONS, - TOPOLOGIES, - TOPOLOGY_DIMENSIONS, - SATELLITES, - topologyLabel, - validateTopologySet, -} from '../../utils/mockData'; + useAdrRegistry, + useArchitectureReference, + useEvidenceRecords, + useProviderConnections, +} from '../../api'; +import type { ArchBlueprintDto, ArchTopologyDimensionDto } from '../../api'; +import { EmptyState, ErrorState, LoadingState } from '../../md3'; function statusTone(status: string) { if (['validated', 'connected', 'current'].includes(status)) return 'ok'; @@ -17,59 +16,97 @@ function statusTone(status: string) { return ''; } +/** Spanish label + presentation ordering for a dimension key (static label map). */ +const DIM_ES: Record = Object.fromEntries( + TOPOLOGY_DIMENSIONS.map((d) => [d.key, d.es]), +); + function TopologySet({ set }: { set: Record }) { const { lang } = useTranslation(); return (
- {TOPOLOGY_DIMENSIONS.map((d) => { - const id = set[d.key]; - if (!id) return null; - return ( - - - {d.key} - {topologyLabel(id, lang)} - - ); - })} + {Object.entries(set).map(([dimKey, id]) => ( + + + {dimKey} + {topologyLabel(id, lang)} + + ))}
); } +/** A blueprint has a composition gap when a required dimension is unset. */ +function compositionGap(bp: ArchBlueprintDto, dimensions: ArchTopologyDimensionDto[]): boolean { + return dimensions.some((d) => d.required && !bp.topologySet[d.key]); +} + export const Architecture: React.FC = () => { const { t, lang } = useTranslation(); - const invalidSatellites = SATELLITES - .map((sat) => ({ sat, result: validateTopologySet(sat.topologySet || {}) })) - .filter((x) => !x.result.valid); + const reference = useArchitectureReference(); + const evidence = useEvidenceRecords(); + const providers = useProviderConnections(); + const adrs = useAdrRegistry(); - return ( -
-
-
{t('Architecture','Arquitectura')}
-
{t('Architecture Reference','Referencia de Arquitectura')}
-
- {t( - 'Canonical topologies, blueprints, evidence lineage and provider ports governed independently from SDLC execution.', - 'Topologías canónicas, blueprints, linaje de evidencia y puertos de proveedor gobernados de forma independiente de la ejecución SDLC.' - )} + const adrStatusByCode = React.useMemo(() => { + const map = new Map(); + for (const a of adrs.data ?? []) map.set(a.code, a.status); + return map; + }, [adrs.data]); + + const header = ( +
+
{t('Architecture','Arquitectura')}
+
{t('Architecture Reference','Referencia de Arquitectura')}
+
+ {t( + 'Canonical topologies, blueprints, evidence lineage and provider ports governed independently from SDLC execution.', + 'Topologías canónicas, blueprints, linaje de evidencia y puertos de proveedor gobernados de forma independiente de la ejecución SDLC.' + )} +
+
+ ); + + if (reference.isLoading) { + return
{header}
; + } + if (reference.isError || !reference.data) { + return ( +
{header} +
+ reference.refetch()} + />
+ ); + } + + const { dimensions, blueprints } = reference.data; + const evidenceItems = evidence.data ?? []; + const providerItems = providers.data ?? []; + const gaps = blueprints.filter((bp) => compositionGap(bp, dimensions)).length; + + return ( +
+ {header}
-
{TOPOLOGY_DIMENSIONS.length}
+
{dimensions.length}
{t('Topology dimensions','Dimensiones de topología')}
-
{ARCHITECTURE_BLUEPRINTS.length}
+
{blueprints.length}
{t('Blueprints','Blueprints')}
-
{EVIDENCE_ITEMS.length}
+
{evidenceItems.length}
{t('Evidence items','Ítems de evidencia')}
-
{invalidSatellites.length}
+
{gaps}
{t('Composition gaps','Gaps de composición')}
@@ -88,10 +125,10 @@ export const Architecture: React.FC = () => { - {TOPOLOGY_DIMENSIONS.map((dimension) => ( + {dimensions.map((dimension) => ( -
{lang === 'es' ? dimension.es : dimension.en}
+
{lang === 'es' ? (DIM_ES[dimension.key] ?? dimension.name) : dimension.name}
{dimension.key}
{dimension.required ? yes : optional} @@ -118,108 +155,146 @@ export const Architecture: React.FC = () => { {t('Blueprint registry','Registro de blueprints')} {t('SDLC independent','Independiente de SDLC')}
- - - - - - - - - - - - {ARCHITECTURE_BLUEPRINTS.map((bp) => ( - - - - - - - - ))} - -
{t('Blueprint','Blueprint')}{t('Topology set','Set de topologías')}{t('ADRs','ADRs')}{t('Evidence','Evidencia')}{t('Lifecycle','Ciclo')}
-
{lang === 'es' ? bp.nameEs : bp.name}
-
{bp.id} · {bp.owner}
-
{bp.adrIds.join(', ')}{bp.evidenceItemIds.join(', ')} - {bp.status} -
{bp.lifecycle} · {bp.updated}
-
-
- -
-
-
- {t('Evidence graph','Grafo de evidencia')} -
+ {blueprints.length === 0 ? ( + + ) : ( - - - + + + + + - {EVIDENCE_ITEMS.map((e) => ( - + {blueprints.map((bp) => ( + + + ))}
{t('Item','Ítem')}{t('Lineage','Linaje')}{t('Integrity','Integridad')}{t('Blueprint','Blueprint')}{t('Topology set','Set de topologías')}{t('ADRs','ADRs')}{t('Evidence','Evidencia')}{t('Status','Estado')}
-
{lang === 'es' ? e.es : e.en}
-
{e.id} · {e.kind}
+
{bp.name}
+
{bp.id}
-
{e.sourceProvider}
-
- {e.references.map((ref) => {ref})} +
+ {bp.adrIds.map((code) => { + const st = adrStatusByCode.get(code); + return ( + + {code} + + ); + })}
{bp.evidenceItemIds.join(', ')} -
{e.contentHash}
-
- immutable - {e.classification} -
+ {bp.status}
+ )} +
+ +
+
+
+ {t('Evidence graph','Grafo de evidencia')} +
+ {evidence.isLoading ? ( + + ) : evidence.isError ? ( + evidence.refetch()} + /> + ) : evidenceItems.length === 0 ? ( + + ) : ( + + + + + + + + + + {evidenceItems.map((e) => ( + + + + + + ))} + +
{t('Item','Ítem')}{t('Lineage','Linaje')}{t('Integrity','Integridad')}
+
{e.content || e.evidenceType}
+
{e.id} · {e.evidenceType}
+
+ {e.sourceProvider &&
{e.sourceProvider}
} + {e.references && e.references.length > 0 && ( +
+ {e.references.map((ref) => {ref})} +
+ )} +
+ {e.contentHash &&
{e.contentHash}
} +
+ {e.immutable && immutable} + {e.classification && {e.classification}} +
+
+ )}
{t('Provider ports & ACL','Puertos de proveedor y ACL')}
- - - - - - - - - - {PROVIDER_CONNECTIONS.map((p) => ( - - - - + {providers.isLoading ? ( + + ) : providers.isError ? ( + providers.refetch()} + /> + ) : providerItems.length === 0 ? ( + + ) : ( +
{t('Capability','Capacidad')}{t('Port','Puerto')}{t('ACL','ACL')}
-
{p.capability}
-
{p.provider} · {p.owner}
-
- {p.status} -
{p.port}
-
-
{p.acl.direction} · {p.acl.fieldOwnership}
-
- {p.evidenceTypes.map((type) => {type})} -
-
+ + + + + - ))} - -
{t('Capability','Capacidad')}{t('Port','Puerto')}{t('ACL','ACL')}
+ + + {providerItems.map((p) => ( + + +
{p.capability}
+
{p.provider} · {p.code}
+ + + {p.status} +
{p.port}
+ + +
{p.acl.direction} · {p.acl.fieldOwnership}
+
+ {p.acl.scopes.map((scope) => {scope})} +
+ + + ))} + + + )}
diff --git a/src/apps/tracker-web/src/components/screens/ProcessDetail.tsx b/src/apps/tracker-web/src/components/screens/ProcessDetail.tsx index 5f3a232..fc9cac3 100644 --- a/src/apps/tracker-web/src/components/screens/ProcessDetail.tsx +++ b/src/apps/tracker-web/src/components/screens/ProcessDetail.tsx @@ -1,39 +1,73 @@ import React from 'react'; import { useTranslation } from '../../utils/i18n'; import { useUiStore } from '../../store/ui.store'; -import { SATELLITES, PHASES, GATE_DETAIL, artStatus, reqMissing } from '../../utils/mockData'; +import { PHASES, ARTIFACTS } from '../../utils/mockData'; +import { useGateDecisions, useSatelliteRegistry } from '../../api'; +import type { GateDecisionDto } from '../../api'; import { - GateStrip, HealthTag, TopologyChip, SourcingChip, ActorTag, - ArtifactRow, FindingRow, ProgressBar, VERDICT, ChipKv + GateStrip, HealthTag, TopologyChip, VERDICT, ChipKv, } from '../atoms'; +import { EmptyState, ErrorState, LoadingState } from '../../md3'; -const PHASE_DATES: Record = { - ums: ['2026-01-12','2026-02-20','2026-04-03','2026-05-18','—'], - pricing: ['2026-03-04','2026-04-15','2026-06-01','—','—'], - checkout: ['2026-04-19','2026-06-01','—','—','—'], - notif: ['2025-12-01','2026-01-20','2026-03-10','2026-05-01','2026-06-01'], - ledger: ['2026-06-02','—','—','—','—'], -}; +/** Map an arbitrary backend gate-decision phase label to a canonical PHASES key. */ +function phaseKeyOf(phase?: string | null): string { + if (!phase) return ''; + const p = phase.toLowerCase(); + return PHASES.map(x => x.key).find(k => p.includes(k)) ?? ''; +} export const ProcessDetail: React.FC = () => { const { t, lang } = useTranslation(); const { openSat, setScreen } = useUiStore(); + const { satellites, isLoading, isError, refetch } = useSatelliteRegistry(); + + const sel = satellites.find(x => x.id === openSat) ?? satellites[0]; + const executionId = sel?.sdlcExecutionId ?? null; + const decisions = useGateDecisions(executionId); - const s = SATELLITES.find(x => x.id === openSat) || SATELLITES[0]; + if (isLoading) { + return
; + } + if (isError) { + return ( +
+ +
+ ); + } + if (!sel) { + return ( +
+ +
+ ); + } + + const s = sel; const curIdx = PHASES.findIndex(p => p.key === s.current); - const gd = GATE_DETAIL[`${s.id}:${s.current}`]; - const dates = PHASE_DATES[s.id] || ['—','—','—','—','—']; - const missing = reqMissing(s, s.current); - const artsReady = missing.length === 0; - const arts = artStatus(s, s.current); - const reqTotal = arts.filter(a => a.req).length; - const reqDone = arts.filter(a => a.req && a.produced).length; - const allReq = reqDone === reqTotal; + // Real per-phase gate decisions (latest wins). + const decisionByPhase = new Map(); + for (const d of decisions.data ?? []) { + const key = phaseKeyOf(d.phase); + if (!key) continue; + const prev = decisionByPhase.get(key); + if (!prev || (d.decidedAtUtc ?? '') > (prev.decidedAtUtc ?? '')) decisionByPhase.set(key, d); + } + + const curDecision = s.current ? decisionByPhase.get(s.current) : undefined; - const metaMeta = [ - ['repo', s.repo], ['ruleset', s.ruleset], ['runtime', s.runtime], - ['process', s.process.id], ['started', s.process.started], + const metaMeta: [string, string][] = [ + ...(s.repo ? [['repo', s.repo] as [string, string]] : []), + ...(s.process?.id ? [['process', s.process.id] as [string, string]] : []), + ...(s.process?.started ? [['started', s.process.started] as [string, string]] : []), ]; return ( @@ -42,7 +76,7 @@ export const ProcessDetail: React.FC = () => { } lang={lang} /> @@ -58,14 +92,11 @@ export const ProcessDetail: React.FC = () => { {s.name}
- {metaMeta.map(([k, v]) => )} - - {s.extraction && ( - - {lang === 'es' ? s.extraction.es : s.extraction.en} - + {metaMeta.map(([k, v]) => )} + {s.topology && } + {s.owner && ( + {s.owner} )} -
@@ -100,8 +131,9 @@ export const ProcessDetail: React.FC = () => { {/* Left col */}
{/* Gate detail card */} - {gd && (() => { - const p = PHASES.find(x => x.key === s.current)!; + {s.current && (() => { + const p = PHASES.find(x => x.key === s.current); + if (!p) return null; const v = s.gates[s.current]; const vi3 = VERDICT(v, lang); return ( @@ -116,7 +148,9 @@ export const ProcessDetail: React.FC = () => { {t(p.en, p.es)} — {t('Gate evaluation','Evaluación de gate')}
- {t('mode','modo')}: {gd.mode} · {gd.at} · {gd.duration} + {curDecision + ? `${t('decided','decidido')}: ${curDecision.decidedAtUtc}` + : t('not yet evaluated','aún no evaluado')}
{vi3.label.toUpperCase()} @@ -128,33 +162,37 @@ export const ProcessDetail: React.FC = () => { {t('Ruleset reference (Core · read-only)','Referencia de ruleset (Core · read-only)')}
- core://rulesets/{gd.rulesetRef.id}@{gd.rulesetRef.version} + core://rulesets/{p.ruleset}
- + {curDecision?.decisionBy && ( + {curDecision.decisionBy} + )} - {artsReady ? ( -
- - {t('All mandatory artifacts produced — phase eligible for approval once ruleset checks pass.','Todos los artefactos obligatorios producidos — fase elegible para aprobación una vez que pasen los checks del ruleset.')} -
- ) : ( + {v === 'failed' ? (
- {t('Approval blocked','Aprobación bloqueada')}: {missing.length} {t('mandatory artifact(s) missing','artefacto(s) obligatorio(s) faltante(s)')} + {t('Gate failed','Gate fallido')}{s.blocker ? `: ${s.blocker}` : ''}
- )} + ) : v === 'evaluating' ? ( +
+ + {t('Gate evaluation in progress.','Evaluación de gate en progreso.')} +
+ ) : (v === 'passed') ? ( +
+ + {t('Gate passed — phase approved.','Gate aprobado — fase aprobada.')} +
+ ) : null} -
+
{t('Pass criteria','Criterio de aprobación')}
-
{gd.pass}
- -
- {t('Evidence · findings','Evidencia · findings')} ({gd.findings.length}) +
+ {curDecision?.rationale || t(p.objEn, p.objEs)}
- {gd.findings.map((f, i) => )}
{v === 'failed' && <> @@ -162,36 +200,36 @@ export const ProcessDetail: React.FC = () => { } {v === 'evaluating' && <> - + {t('evaluating…','evaluando…')} } - {(v === 'passed' || !v) && artsReady && ( + {v === 'passed' && ( )} - {(v === 'passed' || !v) && !artsReady && ( - - )}
); })()} - {/* Phase artifacts card */} + {/* Phase artifacts card — required-artifact reference for the phase */}
{t('Phase artifacts','Artefactos de la fase')} {t('required for approval','requeridos para aprobar')}
-
-
- {t('Mandatory artifacts','Artefactos obligatorios')}: {reqDone}/{reqTotal} + {(ARTIFACTS[s.current] ?? []).map((a) => ( +
+
{lang === 'es' ? a.es : a.en}
+ {a.req + ? {t('Mandatory','Obligatorio')} + : {t('Optional','Opcional')}}
- - {allReq ? t('complete','completo') : t('incomplete','incompleto')} - -
- - {arts.map((a, i) => )} + ))} + {(ARTIFACTS[s.current] ?? []).length === 0 && ( +
+ {t('No artifact requirements configured for this phase.','Sin requisitos de artefactos configurados para esta fase.')} +
+ )}
{t('Requirements come from the tenant configuration. ','Los requisitos vienen de la configuración del tenant. ')}
+
+
+ ); + + if (isLoading) { + return
{header}
; + } + if (isError) { + return ( +
{header} +
+ +
+
+ ); + } + if (satellites.length === 0) { + return ( +
{header} +
+ +
+
+ ); + } + + const active = satellites.filter(s => s.active).length; + const blocked = satellites.filter(s => s.health === 'blocked').length; + const topologies = new Set(satellites.map(s => s.topology).filter(Boolean)).size; + + const selId = satSel && satellites.find(s => s.id === satSel) ? satSel : satellites[0].id; + const sel = satellites.find(s => s.id === selId) ?? satellites[0]; + + return ( +
+ {header} {/* KPIs */}
- {statCard(t('Registered','Registrados'), SATELLITES.length, t('satellite projects','proyectos satélite'))} + {statCard(t('Registered','Registrados'), satellites.length, t('satellite projects','proyectos satélite'))} {statCard(t('Active','Activos'), active, t('under governance','bajo gobierno'), 'ok')} {statCard(t('Blocked','Bloqueados'), blocked, t('at a failed gate','en un gate fallido'), blocked ? 'bad' : undefined)} - {statCard(t('Topologies','Topologías'), Object.keys(TOPOLOGIES).length, t('governed targets','objetivos gobernados'))} + {statCard(t('Topologies','Topologías'), topologies, t('governed targets','objetivos gobernados'))}
{/* MDLayout */} - + id="satellites" modes={['list','grid','thumb']} - items={SATELLITES} + items={satellites} idKey="id" selected={selId} onSelect={(id) => setSatSel(id)} renderItem={(item, mode) => } masterTitle={t('Registered satellites','Satélites registrados')} - masterMeta={`${SATELLITES.length} / ${SATELLITES.length}`} + masterMeta={`${satellites.length} / ${satellites.length}`} detail={sel ? { title: sel.name, - sub: sel.repo, + sub: sel.repo || sel.code, header: (
@@ -219,7 +237,7 @@ export const Satellites: React.FC = () => { x.key===sel.current)?.g || 'g1'})` }} /> {t(PHASES.find(x=>x.key===sel.current)?.en || '', PHASES.find(x=>x.key===sel.current)?.es || '')} - + {sel.topology && }