From 5320ca708348dc9b8b36b625f553b97c10262414 Mon Sep 17 00:00:00 2001 From: limerc Date: Sat, 9 May 2026 14:23:29 +0200 Subject: [PATCH 1/2] Port demo contracts page into console Signed-off-by: limerc --- apps/web/console/README.md | 33 +- apps/web/console/src/App.css | 9 + apps/web/console/src/App.tsx | 7 + apps/web/console/src/DemoContractsPage.css | 2165 ++++++++++++++++ apps/web/console/src/DemoContractsPage.tsx | 2577 ++++++++++++++++++++ docs/ops/COMPONENTS.yaml | 2 +- docs/ops/NEXT.md | 31 +- docs/ops/STATUS.md | 6 +- 8 files changed, 4793 insertions(+), 37 deletions(-) create mode 100644 apps/web/console/src/DemoContractsPage.css create mode 100644 apps/web/console/src/DemoContractsPage.tsx diff --git a/apps/web/console/README.md b/apps/web/console/README.md index c4a80fff..f4566ba6 100644 --- a/apps/web/console/README.md +++ b/apps/web/console/README.md @@ -20,11 +20,14 @@ Current scope: - authenticated shell entry only after `/v1/me` succeeds - access and refresh tokens are held in React memory only - left navigation with Contracts, Delivery Readiness, and Proof product - surfaces; Contracts consumes authenticated, organization-scoped, read-only - Contract endpoints: `GET /v1/contracts`, `GET /v1/contracts/{id}`, and - `GET /v1/contracts/{id}/current-draft`; it renders a contract rail/list plus - selected aggregate detail, current draft body when available, and read-only - Organization / Project / Repository context metadata from + surfaces; the current authenticated Contracts entry renders the imported + `apps/web/demo-change-packet-ru` demo contracts page with local demo state + only, isolated from the rest of the Console CSS. This is a visual demo port, + not a backend-backed contract workflow claim. The existing read-only Contract + clients remain in source for the backend-bound surface: + `GET /v1/contracts`, `GET /v1/contracts/{id}`, and + `GET /v1/contracts/{id}/current-draft`, plus read-only Organization / + Project / Repository context metadata from `GET /v1/organizations/{organization_id}/repository-context`; Delivery Readiness consumes read-only `GET /v1/qualification-feed` while authenticated and renders Qualification / Clarification / Contract / @@ -78,17 +81,19 @@ Delivery rule: `draft_contract` controls; linked contract cards expose `Open contract` navigation only, which loads the selected contract through `GET /v1/contracts/{id}` -- The Contracts surface consumes authenticated, organization-scoped read-only - Contract discovery at +- The current Contracts entry is the imported local RU demo contracts UI. The + backend-bound Contracts surface code remains available in source and consumes + authenticated, organization-scoped read-only Contract discovery at `GET /v1/contracts?project_id=&repo_binding_id=&goal_id=&state=&limit=`, loads `GET /v1/contracts?limit=50` by default, supports state and repo-binding filters plus manual refresh, keeps manual ID lookup as a secondary fallback, and shows selected detail through authenticated, organization-scoped, read-only `GET /v1/contracts/{id}` as the compact public Contract aggregate only -- The Contracts surface also uses `/v1/me` to determine `organization_id` and - reads `GET /v1/organizations/{organization_id}/repository-context` for a - compact metadata-only context panel. If the selected Contract +- The backend-bound Contracts surface code also uses `/v1/me` to determine + `organization_id` and reads + `GET /v1/organizations/{organization_id}/repository-context` for a compact + metadata-only context panel. If the selected Contract `repo_binding_id` matches a returned context, that context is shown; if no Contract is selected, the first Organization repository context is shown; if the selected binding is absent from the response, the Contract stays visible @@ -204,9 +209,11 @@ wait/cursor semantics, SSE, WebSocket, a daemon, or an event stream. main user flow. - Delivery Readiness shows qualification state and handoff to Contracts, not lifecycle controls. -- The Contracts surface is read-only, lists contracts from discovery, supports - state and repo-binding filtering plus manual refresh, and can show selected - detail or a manual ID lookup result. +- The backend-bound Contracts surface code is read-only, lists contracts from + discovery, supports state and repo-binding filtering plus manual refresh, + and can show selected detail or a manual ID lookup result. The current + authenticated Contracts entry renders the imported local RU demo page + instead. D-0091 display behavior: - Delivery Readiness cards show one frontend-projected primary status instead diff --git a/apps/web/console/src/App.css b/apps/web/console/src/App.css index a5562f37..c11e913d 100644 --- a/apps/web/console/src/App.css +++ b/apps/web/console/src/App.css @@ -175,6 +175,15 @@ select:focus-visible { border: 0; } +.demoContractsOverlay { + position: fixed; + inset: 0; + z-index: 100; + min-width: 320px; + min-height: 100vh; + background: var(--bg); +} + .brand { display: flex; align-items: center; diff --git a/apps/web/console/src/App.tsx b/apps/web/console/src/App.tsx index 82e06490..502a6c5b 100644 --- a/apps/web/console/src/App.tsx +++ b/apps/web/console/src/App.tsx @@ -50,6 +50,7 @@ import type { ContractDraftResponse, ContractDraftSourceRef } from './contractDr import { READINESS_DISPLAY_LANES, projectReadinessDisplay, sortReadinessItems } from './readinessDisplay'; import { formatCalmTimestamp } from './uiTime'; import StartPage from './StartPage'; +import DemoContractsPage from './DemoContractsPage'; import './App.css'; @@ -1929,6 +1930,12 @@ function ConsoleApp() { )} + {screen === 'console' && activeSurface === 'contracts' ? ( +
+ +
+ ) : null} + {isDrawerOpen ? ( <> + ))} + + + +
+
Выбранный контракт
+

{selectedContract.title}

+
+
+
репозиторий
+
{selectedContract.repo}
+
+
+
текущая стадия
+
{selectedContract.stage}
+
+
+
политика
+
{selectedContract.policy}
+
+
+
решение человека
+
{selectedContract.humanDecision}
+
+
+
Стадия: {selectedContract.stage} · {selectedContract.stageProgress}
+
+ +
+ {selectedContract.detail.changePacket} + {selectedContract.detail.evidence} + {selectedContract.detail.projectContext} + {selectedContract.detail.decisionTrail} +
+ + ); +} + +function MobileReadinessSurface({ selectedRepo, onSelectRepo }: { selectedRepo: RepoId; onSelectRepo: (repo: RepoId) => void }) { + const context = REPO_CONTEXTS[selectedRepo]; + + return ( +
+
+
Готовность
+
+ + + +
+
+ +
+
+
+
Очередь репозиториев
+

Готовность репозитория

+
+ проверка +
+
+ {MOBILE_REPO_QUEUE.map((repo) => ( + + ))} +
+
+ +
+
Детали репозитория
+

{context.repo}

+
+
+
готовность
+
{context.readiness}/100
+
+
+
инициализация
+
{context.init}
+
+
+
документов
+
{context.docsIndexed}
+
+
+
тесты
+
{getCompactReadinessSignal(context.testsStatus)}
+
+
+
CI
+
{context.ciStatus}
+
+
+
Правила агента
+
{context.ownersRulesStatus.replace('Правила агента ', '')}
+
+
+
поверхность подтверждения
+
{context.proofSurfaceStatus}
+
+
+
рекомендованный режим
+
{context.recommendedMode}
+
+
+
+ +
+
Действия
+
+ + + +
+

Настройку лучше делать на десктопе. Демо-кнопки ничего не подключают и не меняют.

+
+
+ ); +} + +function MobileProofSurface({ + selectedProof, + onSelectProof, +}: { + selectedProof: ProofFeedItem; + onSelectProof: (proofId: string) => void; +}) { + const proofDecisionReady = selectedProof.contractId === 'C-0147' && selectedProof.criteriaCoverage.startsWith('5/5'); + const archivedProof = selectedProof.proofStatus === 'Принято'; + const decisionRestriction = archivedProof ? 'Архивное подтверждение: только чтение' : 'Покрытие критериев неполное'; + + return ( +
+
+
+
+
Очередь пакетов подтверждения
+

Проверка решения

+
+ безопасная проверка +
+
+ {MOBILE_PROOF_QUEUE.map((proof) => ( + + ))} +
+
+ +
+
Выбранный пакет подтверждения
+

{selectedProof.contractId}

+
+
+
контракт
+
{selectedProof.contractId}
+
+
+
репозиторий
+
{selectedProof.repo}
+
+
+
статус подтверждения
+
{selectedProof.proofStatus}
+
+
+
покрытие критериев
+
{selectedProof.criteriaCoverage}
+
+
+
решение человека
+
{selectedProof.contractId === 'C-0147' ? 'Ожидает' : selectedProof.humanApproval}
+
+
+
+ +
+ {selectedProof.changed[0]} + {selectedProof.verified[0]} + {selectedProof.linkedEvidence} + {selectedProof.decisionTrail.slice(0, 3).map(formatActivityKind).join(' · ')} + {selectedProof.archiveLine} +
+ +
+
Действие
+ {proofDecisionReady ? ( + <> + +

Только безопасная проверка. Финальное решение нельзя принять одним тапом на телефоне.

+ + ) : ( + <> + Решение недоступно +

{decisionRestriction}

+ + )} +
+
+ ); +} + +function MobileCompanionPreview() { + const [mobileSurface, setMobileSurface] = useState('proof'); + const [selectedMobileContractId, setSelectedMobileContractId] = useState('C-0147'); + const [selectedMobileRepo, setSelectedMobileRepo] = useState('trialops-demo'); + const [selectedMobileProofId, setSelectedMobileProofId] = useState('PF-0147'); + + const selectedMobileContract = MOBILE_CONTRACT_QUEUE.find((contract) => contract.id === selectedMobileContractId) ?? MOBILE_CONTRACT_QUEUE[0]; + const selectedMobileProof = PROOF_FEED.find((proof) => proof.id === selectedMobileProofId) ?? PROOF_FEED[0]; + + return ( +
+
+
Goalrail
+

Goalrail

+

Короткий режим проверки: контракты, готовность и подтверждения.

+ Полная консоль оператора доступна на десктопе. +
+ + + + {mobileSurface === 'contracts' ? ( + + ) : mobileSurface === 'readiness' ? ( + + ) : ( + + )} +
+ ); +} + +function renderStageContent({ + contract, + projectContext, + step, + approval, + visibleClarifications, + visibleEvidence, + visibleVerification, + onAdvance, + onDecision, +}: { + contract: ContractRecord; + projectContext: RepoContext; + step: StepIndex; + approval: ApprovalState; + visibleClarifications: number; + visibleEvidence: number; + visibleVerification: number; + onAdvance: () => void; + onDecision: (decision: ApprovalState) => void; +}) { + if (step === 0) { + return ( +
+
Входящий запрос
+
+
+
Входящий запрос
+

{contract.title}

+

{contract.goal}

+
    + {contract.intakeNotes.map((note) => ( +
  • {note}
  • + ))} +
+
+ +
+
Пакет запроса
+
+
+
Репозиторий
+
{contract.repo}
+
+
+
Контракт
+
{contract.id}
+
+
+
Раздел
+
{contract.scopeSurface}
+
+
+
Политика
+
{projectContext.runtimePolicy}
+
+
+
+ Привязка репозитория уже есть, но это контекст проекта, а не стадия pipeline. +
+
+
+
+ ); + } + + if (step === 1) { + return ( +
+
Уточнения · {contract.clarifications.length} из {contract.clarifications.length}
+
+ {contract.clarifications.map((card, index) => { + const pinned = index < visibleClarifications; + + return ( +
+
+ Q{index + 1} + {card.ref} +
+
{card.question}
+
{card.answer}
+
{card.note}
+
{pinned ? 'Ответ закреплен в контракте' : 'Ожидает закрепления в контракте'}
+
+ ); + })} +
+
+ ); + } + + if (step === 2) { + return ( +
+
Рабочий контракт · черновик v3
+
+
Цель
+

{contract.goal}

+
+ +
+ + + + +
+ +
+
Контекст проекта / политика
+

{contract.policyNote}

+
+ + +
+
+
+ ); + } + + if (step === 3) { + return ( +
+
Задачи
+
+ {contract.workItems.map((item) => ( +
+
+
+
{item.id}
+
{item.title}
+
+
{item.status}
+
+
+
+
Тип
+
{item.lane}
+
+
+
Область / раздел
+
{item.scope}
+
+
+
Статус
+
{item.status}
+
+
+
Обязанность по подтверждению
+
{item.proofObligation}
+
+
+
+ ))} +
+
+ ); + } + + if (step === 4) { + return ( +
+
Артефакты выполнения
+
+ Выполнение прошло вне Goalrail. Goalrail сохраняет синхронизированные артефакты для выбранного контракта, а не чат-лог. +
+
+ {contract.evidence.slice(0, visibleEvidence).map((item) => ( +
+
{item.label}
+
{item.value}
+
+ ))} +
+
+ ); + } + + if (step === 5) { + return ( +
+
Проверка
+
+ {contract.verification.slice(0, visibleVerification).map((row) => ( +
+
+
{row.criterion}
+
{row.support}
+
+
+
Итог
+
{row.outcome}
+
+
+ ))} +
+
+ ); + } + + if (step === 6) { + return ( +
+
Пакет подтверждения
+
+ + +
+ +
+ ); + } + + const approvalLabel = + approval === 'accepted' + ? 'Принято' + : approval === 'blocked' + ? 'Заблокировано' + : approval === 'rework' + ? 'Нужна доработка' + : 'Ждет решения'; + + return ( +
+
Решение
+
+
Состояние решения
+
{approvalLabel}
+
+ +
+ + + + +
+ +
+
Ручное решение
+
+ + + +
+
+
+ ); +} + +function DeliveryReadinessSurface({ selectedRepo, onSelectRepo }: { selectedRepo: RepoId; onSelectRepo: (repo: RepoId) => void }) { + return ( +
+
+
+
+
Готовность
+
Настройка репозитория и режим работы
+
+
+ Раздел рабочей области + Только демо +
+
+ +
+
+
Готовность · уровень репозитория и проекта
+

+ Этот раздел показывает подключенные репозитории, сигналы готовности, действия настройки и рекомендованный режим работы. Это не + стадия цепочки контракта. +

+
+ +
+ {WORKSPACE_REPOS.map((repo) => { + const context = REPO_CONTEXTS[repo]; + + return ( + + ); + })} + +
+
+
+
Добавить репозиторий
+
Подключить следующий репозиторий
+
+ Действие настройки +
+

Подключить репозиторий, выполнить инициализацию, просканировать контекст и посчитать готовность.

+ +
+
+
+
+
+ ); +} + +function DeliveryReadinessInspector({ selectedRepo }: { selectedRepo: RepoId }) { + const context = REPO_CONTEXTS[selectedRepo]; + + return ( + + ); +} + +function DeliveryReadinessBottomPanel({ selectedRepo }: { selectedRepo: RepoId }) { + const context = REPO_CONTEXTS[selectedRepo]; + + return ( +
+
+
+
Активность готовности
+
Демо-события настройки
+
+
+ {[ + ['09:31:02', 'repo.selected', `${context.repo} выбран для деталей готовности`, 'mauve'], + ['09:31:12', 'context.scan', context.scanStatus, getReadinessTone(context.readiness)], + ['09:31:22', 'mode.recommended', context.recommendedMode, getReadinessTone(context.readiness)], + ].map(([ts, kind, note, tone]) => ( +
+
{ts}
+
+
{formatActivityKind(kind)}
+
{note}
+
+
{tone === 'pass' ? 'готово' : tone === 'block' ? 'настройка' : 'проверка'}
+
+ ))} +
+
+ +
+
+
Управление разделом
+
Нет реальных интеграций
+
+
+ Анализ, инициализация, сканирование контекста и добавление репозитория — демо-действия. Они не вызывают бэкенд и не меняют постоянное состояние. +
+
+ Репозиторий: {context.repo} + Готовность: {context.readiness}/100 +
+
+
+ ); +} + +function ProofFeedSurface({ selectedProofId, onSelectProof }: { selectedProofId: string; onSelectProof: (proofId: string) => void }) { + return ( +
+
+
+
+
Подтверждения
+
Артефакты и решения по контрактам
+
+
+ Все репозитории по умолчанию + Не чат-лог +
+
+ +
+
+
Подтверждения · обзор по контрактам и репозиториям
+

+ Область по умолчанию — все репозитории. Чипы репозиториев остаются статическими демо-контролы, чтобы раздел читался как контроль подтверждений на уровне рабочей области. +

+
+ Все репозитории + trialops-demo + billing-api + frontend-console +
+
+ +
+ {PROOF_FEED.map((item) => ( + + ))} +
+
+
+
+ ); +} + +function ProofFeedInspector({ selectedProof }: { selectedProof: ProofFeedItem }) { + return ( + + ); +} + +function ProofFeedBottomPanel({ selectedProof }: { selectedProof: ProofFeedItem }) { + return ( +
+
+
+
Активность ленты подтверждений
+
Только артефакты и решения
+
+
+ {selectedProof.decisionTrail.map((entry, index) => ( +
+
10:{String(12 + index).padStart(2, '0')}:04
+
+
{formatActivityKind(entry)}
+
{selectedProof.contractId} · {selectedProof.repo}
+
+
{selectedProof.tone === 'block' ? 'блок' : selectedProof.tone === 'pass' ? 'готово' : 'событие'}
+
+ ))} +
+
+ +
+
+
Управление лентой
+
Статическая область
+
+
+ Чипы репозиториев — только демо. Разбор статусов находится слева в очереди подтверждений, поэтому лента остается между контрактами и между репозиториями. +
+
+ Подтверждение: {selectedProof.contractId} + Область по умолчанию: все репозитории +
+
+
+ ); +} + +function DesktopConsole() { + const [activeSurface, setActiveSurface] = useState('contracts'); + const [repoFilter, setRepoFilter] = useState('trialops-demo'); + const [repoSelectorOpen, setRepoSelectorOpen] = useState(false); + const [contractSearch, setContractSearch] = useState(''); + const [repoSearch, setRepoSearch] = useState(''); + const [proofSearch, setProofSearch] = useState(''); + const [selectedContractId, setSelectedContractId] = useState('C-0147'); + const [selectedReadinessRepo, setSelectedReadinessRepo] = useState('trialops-demo'); + const [selectedProofId, setSelectedProofId] = useState(PROOF_FEED[0].id); + const [contractSteps, setContractSteps] = useState>(INITIAL_STEPS); + const [approvalStates, setApprovalStates] = useState>(INITIAL_APPROVALS); + const [visibleClarifications, setVisibleClarifications] = useState(0); + const [visibleEvidence, setVisibleEvidence] = useState(0); + const [visibleVerification, setVisibleVerification] = useState(0); + + const repoScopedContracts = useMemo(() => { + return repoFilter === 'all' ? CONTRACTS : CONTRACTS.filter((contract) => contract.repo === repoFilter); + }, [repoFilter]); + + const visibleContracts = useMemo(() => { + return repoScopedContracts.filter((contract) => + matchesQuery([contract.id, contract.title, contract.repo, contract.owner, contract.summary, getStatus(contractSteps[contract.id] ?? contract.defaultStep, approvalStates[contract.id] ?? 'pending')], contractSearch), + ); + }, [approvalStates, contractSearch, contractSteps, repoScopedContracts]); + + const visibleRepos = useMemo(() => { + return WORKSPACE_REPOS.filter((repo) => { + const context = REPO_CONTEXTS[repo]; + return matchesQuery([context.repo, context.init, context.scanStatus, context.testsStatus, context.ciStatus, context.ownersRulesStatus, context.recommendedMode], repoSearch); + }); + }, [repoSearch]); + + const visibleProofs = useMemo(() => { + return PROOF_FEED.filter((item) => + matchesQuery([item.contractId, item.repo, item.proofStatus, item.decisionStatus, item.humanApproval, item.criteriaCoverage, item.summary], proofSearch), + ); + }, [proofSearch]); + + useEffect(() => { + if (!repoScopedContracts.some((contract) => contract.id === selectedContractId)) { + setSelectedContractId(repoScopedContracts[0]?.id ?? CONTRACTS[0].id); + } + }, [repoScopedContracts, selectedContractId]); + + const selectedContract = useMemo(() => { + return CONTRACTS.find((contract) => contract.id === selectedContractId) ?? CONTRACTS[0]; + }, [selectedContractId]); + + const step = contractSteps[selectedContract.id] ?? selectedContract.defaultStep; + const approval = approvalStates[selectedContract.id] ?? 'pending'; + const selectedStatus = getStatus(step, approval); + const projectContext = REPO_CONTEXTS[selectedContract.repo]; + const meters = getMeters(step, approval); + const activity = useMemo(() => getActivity(selectedContract, step, approval), [selectedContract, step, approval]); + const selectedProof = useMemo(() => PROOF_FEED.find((item) => item.id === selectedProofId) ?? PROOF_FEED[0], [selectedProofId]); + const averageReadiness = Math.round(WORKSPACE_REPOS.reduce((sum, repo) => sum + REPO_CONTEXTS[repo].readiness, 0) / WORKSPACE_REPOS.length); + const acceptedProofs = PROOF_FEED.filter((item) => item.proofStatus === 'Принято').length; + const blockedProofs = PROOF_FEED.filter((item) => item.proofStatus === 'Заблокировано').length; + const topbarMeters = + activeSurface === 'contracts' + ? [ + { tone: 'amber' as Tone, label: 'Контракт', value: meters.contract.label, percent: meters.contract.percent }, + { tone: 'mauve' as Tone, label: 'Выполнение', value: meters.execution.label, percent: meters.execution.percent }, + { tone: 'pass' as Tone, label: 'Подтверждение', value: meters.proof.label, percent: meters.proof.percent }, + ] + : activeSurface === 'readiness' + ? [ + { tone: 'mauve' as Tone, label: 'Рабочая область', value: `${WORKSPACE_REPOS.length} репозитория`, percent: 100 }, + { tone: 'amber' as Tone, label: 'Готовность', value: `${averageReadiness}/100 среднее`, percent: averageReadiness }, + { tone: 'pass' as Tone, label: 'Настройка', value: 'Только демо-действия', percent: 72 }, + ] + : [ + { tone: 'mauve' as Tone, label: 'Подтверждения', value: 'Все репозитории', percent: 100 }, + { tone: 'amber' as Tone, label: 'Ожидают', value: '2 активных', percent: 50 }, + { tone: 'pass' as Tone, label: 'Решения', value: `${acceptedProofs} принято · ${blockedProofs} блок`, percent: 70 }, + ]; + + useEffect(() => { + if (step === 1) { + setVisibleClarifications(0); + const timers = CLARIFICATION_DELAYS.slice(0, selectedContract.clarifications.length).map((delay, index) => + window.setTimeout(() => { + setVisibleClarifications(index + 1); + }, delay), + ); + + return () => { + timers.forEach((timer) => window.clearTimeout(timer)); + }; + } + + setVisibleClarifications(step > 1 ? selectedContract.clarifications.length : 0); + + return undefined; + }, [selectedContract, step]); + + useEffect(() => { + if (step === 4) { + setVisibleEvidence(0); + const timers = selectedContract.evidence.map((_, index) => + window.setTimeout(() => { + setVisibleEvidence(index + 1); + }, (index + 1) * EVIDENCE_DELAY), + ); + + return () => { + timers.forEach((timer) => window.clearTimeout(timer)); + }; + } + + setVisibleEvidence(step > 4 ? selectedContract.evidence.length : 0); + + return undefined; + }, [selectedContract, step]); + + useEffect(() => { + if (step === 5) { + setVisibleVerification(0); + const timers = selectedContract.verification.map((_, index) => + window.setTimeout(() => { + setVisibleVerification(index + 1); + }, (index + 1) * VERIFICATION_DELAY), + ); + + return () => { + timers.forEach((timer) => window.clearTimeout(timer)); + }; + } + + setVisibleVerification(step > 5 ? selectedContract.verification.length : 0); + + return undefined; + }, [selectedContract, step]); + + const setStepForSelected = (nextStep: StepIndex) => { + setContractSteps((current) => ({ ...current, [selectedContract.id]: nextStep })); + }; + + const goNext = () => { + if (step < 7) { + setStepForSelected((step + 1) as StepIndex); + } + }; + + const goBack = () => { + if (step > 0) { + setStepForSelected((step - 1) as StepIndex); + } + }; + + const resetSelected = () => { + setContractSteps((current) => ({ ...current, [selectedContract.id]: selectedContract.defaultStep })); + setApprovalStates((current) => ({ ...current, [selectedContract.id]: 'pending' })); + }; + + const handleDecision = (decision: ApprovalState) => { + setApprovalStates((current) => ({ ...current, [selectedContract.id]: decision })); + setStepForSelected(7); + }; + + const selectedRepoOption = REPO_OPTIONS.find((option) => option.value === repoFilter) ?? REPO_OPTIONS[0]; + + const handleRepoFilterSelect = (nextRepoFilter: RepoFilter) => { + setRepoFilter(nextRepoFilter); + setRepoSelectorOpen(false); + }; + + const primaryActionLabel = + step === 0 + ? 'Начать' + : step === 1 + ? 'К контракту' + : step === 2 + ? 'Зафиксировать контракт' + : step === 3 + ? 'К артефактам' + : step === 4 + ? 'К проверке' + : step === 5 + ? 'К подтверждению' + : step === 6 + ? 'К решению' + : approval === 'accepted' + ? 'Повторить' + : null; + + const activeSurfaceLabel = activeSurface === 'contracts' ? 'Контракты' : activeSurface === 'readiness' ? 'Готовность' : 'Подтверждения'; + const topbarStateLabel = activeSurface === 'contracts' ? 'Статус' : activeSurface === 'readiness' ? 'Репозиторий' : 'Область'; + const topbarStateValue = activeSurface === 'contracts' ? selectedStatus : activeSurface === 'readiness' ? selectedReadinessRepo : 'Все репозитории'; + + return ( +
+
+
+
+ +
+ Goalrail · консоль +
+
+ +
+ {topbarMeters.map((meter) => ( +
+
+
{meter.label}
+
{meter.value}
+
+
+ +
+
+ ))} +
+ +
+
+ Раздел + + {activeSurfaceLabel} + +
+
+ {topbarStateLabel} + + {topbarStateValue} + +
+
+
+ +