Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 19 additions & 15 deletions apps/web/console/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ 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 shell, isolated from
the rest of the Console CSS, and backs its Contract rows/detail/current draft
data with the existing authenticated read-only Contract endpoints:
`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 /
Expand Down Expand Up @@ -78,17 +80,18 @@ 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 RU demo contracts UI backed by
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,
repo-binding filters plus manual refresh, 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 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
`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
Expand Down Expand Up @@ -204,9 +207,10 @@ 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 current imported RU demo Contracts shell is read-only, lists contracts
from backend discovery, supports state and repo-binding filtering plus manual
refresh, and shows selected detail/current draft from the backend-backed
selected Contract state.

D-0091 display behavior:
- Delivery Readiness cards show one frontend-projected primary status instead
Expand Down
9 changes: 9 additions & 0 deletions apps/web/console/src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
57 changes: 57 additions & 0 deletions apps/web/console/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,15 @@ function expectNoWorkflowMutationRequests() {
))).toBe(false);
}

async function findDemoContractsShadowRoot() {
await waitFor(() => {
const host = document.querySelector('.demoContractsHost') as HTMLDivElement | null;
expect(host?.shadowRoot).toBeTruthy();
});

return (document.querySelector('.demoContractsHost') as HTMLDivElement).shadowRoot as ShadowRoot;
}

async function setLocale(locale: 'en' | 'ru') {
await i18n.changeLanguage(locale);
document.documentElement.lang = locale;
Expand Down Expand Up @@ -1502,6 +1511,54 @@ describe('App', () => {
);
});

it('renders backend contract data inside the imported contracts page shell', async () => {
await loginSuccessfully('ru', 'owner', [
contractResponse({
id: 'backend-live-contract',
goal_id: 'backend-live-goal',
repo_binding_id: 'backend-live-repo-binding',
state: 'ready_for_approval',
current_seed_id: 'backend-live-seed',
current_draft_id: 'backend-live-draft',
updated_at: '2026-05-09T10:15:00Z',
}),
], {
id: 'backend-live-draft',
contract_id: 'backend-live-contract',
contract_seed_id: 'backend-live-seed',
goal_id: 'backend-live-goal',
repo_binding_id: 'backend-live-repo-binding',
title: 'Backend live draft title',
intent_summary: 'Backend live draft intent summary.',
proposed_scope: ['Backend live scope item'],
proposed_non_goals: ['No browser lifecycle mutation'],
proposed_acceptance_criteria: ['Backend page shows current draft'],
proposed_expected_checks: ['Backend read-only smoke'],
proposed_proof_expectations: ['Backend proof expectation text'],
state: 'ready_for_approval',
}, [
repositoryContextRecord({
repoBindingId: 'backend-live-repo-binding',
repositoryFullName: 'heurema/backend-live',
projectDisplayName: 'Backend Live Project',
}),
]);

const shadowRoot = await findDemoContractsShadowRoot();

await waitFor(() => {
expect(shadowRoot.textContent).toContain('backend-live-contract');
expect(shadowRoot.textContent).toContain('Backend live draft title');
});
expect(shadowRoot.textContent).toContain('heurema/backend-live');
expect(shadowRoot.textContent).toContain('Backend live draft intent summary.');
expect(shadowRoot.textContent).toContain('Backend live scope item');
expect(shadowRoot.textContent).toContain('No browser lifecycle mutation');
expect(shadowRoot.textContent).toContain('Read-only API');
expect(fetchMock.mock.calls.map(([url]) => String(url))).toContain('/v1/contracts/backend-live-contract/current-draft');
expectNoWorkflowMutationRequests();
});

it('does not call current draft detail when the selected Contract has no current_draft_id', async () => {
await loginSuccessfully('en', 'owner', [
contractResponse({
Expand Down
30 changes: 30 additions & 0 deletions apps/web/console/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -1929,6 +1930,35 @@ function ConsoleApp() {
</section>
)}

{screen === 'console' && activeSurface === 'contracts' ? (
<div className="demoContractsOverlay">
<DemoContractsPage
liveContracts={{
contracts: contractList.contracts,
selectedContract: contract,
selectedDraft: contractDraft,
contractListLoadStatus,
contractListError,
contractLoadStatus,
contractError,
contractDraftLoadStatus,
contractDraftError,
repositoryContext,
repositoryContextLoadStatus,
repositoryContextError,
repoBindingFilter: contractListRepoBindingFilter,
stateFilter: contractListStateFilter,
onContractSelect: handleContractListSelection,
onRefresh: () => {
void refreshContractsSurface(true);
},
onRepoBindingFilterChange: setContractListRepoBindingFilter,
onStateFilterChange: setContractListStateFilter,
}}
/>
</div>
) : null}

{isDrawerOpen ? (
<>
<button aria-label={translate('users.closeForm')} className="drawerScrim" onClick={closeDrawer} type="button" />
Expand Down
Loading
Loading