diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58fcc1d8..457e6458 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Tenant 10 Bulk result layout repair
+
+- Fix long resource locations and error details widening the Bulk result
+ grid at narrow viewports. Bound its flexible tracks with minmax(0,1fr)
+ and reuse CopyableField for clipped, tooltip-accessible, full-value copying.
+ Missing locations and empty details do not expose meaningless copy buttons.
+- Preserve actual bulk execution, duplicate rejection, failure CSV content,
+ corrected-row retry and owned-fixture cleanup. Add a deterministic
+ long-value regression with 1440 px / 900 px bounds, actual truncation and exact
+ clipboard checks, alongside bulk/manual execution coverage.
+- RED: actual Azure browser run 347/348, no skips/flakes, with one measured
+ layout failure; deterministic long-value case also fails on unchanged
+ Azure image. GREEN: Bulk component 9 to 10 tests, five built-loopback
+ browser flows, production build and strict added-spec type check.
+ Updated full web coverage passes 1,662 tests across 125 files (previously
+ 1,661): statements 83.06%, branches 75.86%, functions 75.53%, lines 85.74%.
+ Configured floors pass; branch coverage was previously 75.89%.
+ Reviewed immutable-image publication remains pending. API unit/E2E
+ remain unchanged by this frontend-only delta; no
+ new API run count is claimed here.
+- Full web tsc is not green: executable virtual-HEAD comparison verifies
+ the same 66 diagnostics/two production diagnostics and zero added errors.
+ No visual baseline was regenerated. Version remains 0.55.67 for the
+ operator-authorized migration repair; performance remains unqualified
+ and customer production must not change.
+- Self-improvement applied: long-value fixtures catch environment-dependent
+ grid sizing before deployment. Design disposition accepted: existing
+ copy/truncate composition; no speculative service or strategy extraction.
+
### 0.55.67 tenant 10 migration release under a scoped exception
- Record explicit operator authorization
diff --git a/Session_starter.md b/Session_starter.md
index c010b8a8..66b03e39 100644
--- a/Session_starter.md
+++ b/Session_starter.md
@@ -1,5 +1,30 @@
## SCIMServer - Condensed Session Memory
+**Current tenant 10 checkpoint:** PR270 merged normally as6102aff4 and its
+authoritative scanned image is deployed on dev. Both actual database carries
+passed full nine-table fidelity/schema checks. Available-data authorization
+supersedes waiting for source freeze; performance remains unqualified.
+Dev environment repair has actual-AST RED/GREEN and complete Azure readback.
+Live acceptance reports1,841 passes/zero failures, including four explicitly
+skipped retired-API placeholders. Browser acceptance has347 passes/one
+unintended Bulk layout failure, zero skips/flakes. Canary app remains absent.
+
+Separate repair branch `fix/tenant10-bulk-result-overflow-20261103` uses
+bounded fractional grid tracks and existing CopyableField truncation.
+Deterministic long values reproduce RED on unchanged Azure image. Ten
+component tests, five built-loopback browser flows with migrated-dev backend,
+strict added-spec compilation and production build pass. Actual1440/900px
+bounds, clipping and complete clipboard values are asserted; failed and GREEN
+screenshots were inspected. No baseline regeneration. Virtual HEAD compiler
+comparison verifies unchanged66 errors/two production errors, no additions.
+Updated whole-web coverage passes1,662/125 files, with statements83.06%,
+branches75.86%, functions75.53%, lines85.74%; all size budgets pass.
+Web ESLint remains N/A because no config exists, not a passing lint run.
+Review, corrected-image publication,
+full actual target acceptance, operator visual confirmation and cutover remain
+pending. Customer production is untouched. Older no-app/source-freeze/pending
+release statements below are preparation history, not the current state.
+
**2026-11-03 authorized migration release:** the operator explicitly authorized
`T10-0.55.67-PERF-EXCEPTION-2026-11-03` for this 0.55.67 tenant 10 dev/canary
migration only. Performance remains unqualified; all other security,
diff --git a/docs/CONTEXT_INSTRUCTIONS.md b/docs/CONTEXT_INSTRUCTIONS.md
index 5ae0af27..35ad7b3d 100644
--- a/docs/CONTEXT_INSTRUCTIONS.md
+++ b/docs/CONTEXT_INSTRUCTIONS.md
@@ -16,15 +16,32 @@ data-integrity, functional, browser and migration gate remains mandatory;
customer production must not change. The existing migration execution document
records the exact authorization, scope and required merged-commit/image-digest
binding. Historical no-waiver holds below are superseded, not failed receipts.
-Use release-only branch `ops/tenant10-release-exception-20261103`; keep
-qualification research PR251 draft. Complete API/web trees match integrated
-candidate5823a225 after importing four unchanged version-bearing blobs.
-No runtime, schema, dependency graph, workflow or normal qualification logic
-changes. Root/web dependency restore, unchanged-lock verification, API Prisma
-generation and public-registry/sha512 provenance pass. Normal protected review,
-final immutable artifact, actual target acceptance, source freeze/final carry
-and cutover remain pending. Self-improvement applied: bounded authorization
-and honest pending evidence. Design disposition accepted: no general bypass.
+PR270 merged normally as6102aff4; authoritative41714210347 produced the
+scanned immutable image. Both actual target databases passed complete
+nine-table content/schema/constraint/index equality. The operator explicitly
+accepts available point-in-time data; no source freeze or synchronized
+zero-loss claim is required. Dev is deployed with corrected environment
+values/references, a Healthy revision and authenticated0.55.67/Prisma readback.
+Live acceptance reports1,841 passes/zero failures, including four explicit
+retired-API placeholders that are not exercised checks. Full dev browser
+acceptance has347 passes/one unintended Bulk layout failure, zero skips/flakes.
+
+Current repair branch `fix/tenant10-bulk-result-overflow-20261103` bounds the
+Bulk result grid and reuses existing copy/truncate fields. Unit RED/GREEN,
+five built-loopback frontend/browser flows with the migrated-dev backend,
+strict added-spec compilation and production build pass. Full web tsc remains
+non-green at66 diagnostics/two production diagnostics; executable HEAD
+comparison proves zero added diagnostics. Updated full coverage passes
+1,662 tests / 125 files, with statements83.06%, branches75.86%,
+functions75.53%, lines85.74%; all size budgets pass. Web ESLint remains N/A
+because no config exists. Protected publication, review, a corrected
+immutable image and actual target reruns
+remain mandatory. Canary application, operator visual confirmation and role
+cutover remain pending. No customer-production changes or performance campaign.
+Keep PR251 research draft. Self-improvement applied: deterministic long values,
+actual bounds/clipping/full clipboard assertions and immutable scoped reports.
+Design disposition accepted: existing primitive composition; no general bypass
+or speculative domain-service extraction.
**Authoritative current state:** PR267 is merged at84099293570af5ab06f7d8e2f93e7e6ae2c0236f.
Reviewed883fb6c6 passes29 protected gates, and exact-tip5757623103 reports
diff --git a/docs/GLOBAL_NAVIGATION_AND_WORKFLOW_CONTEXT.md b/docs/GLOBAL_NAVIGATION_AND_WORKFLOW_CONTEXT.md
index 583efe8f..d1eab14c 100644
--- a/docs/GLOBAL_NAVIGATION_AND_WORKFLOW_CONTEXT.md
+++ b/docs/GLOBAL_NAVIGATION_AND_WORKFLOW_CONTEXT.md
@@ -1,6 +1,6 @@
# Global navigation and workflow context
-> **Status:** User-facing reference - **Last verified:** 2026-10-16 - **Product version:** `0.55.67`
+> **Status:** User-facing reference - **Last verified:** 2026-11-03 - **Product version:** `0.55.67`
See [current release notes](RELEASE_NOTES.md) for deployment and acceptance
boundaries. Endpoints now uses available window height, Dashboard uses compact
@@ -17,6 +17,13 @@ remains under each trust's More menu. See the
[isolated integration checkpoint](auth/ENDPOINT_TOKEN_LIFETIME_TDD_HANDOFF.md#current-master-integration-preparation-2026-10-21);
this is not a deployed navigation change.
+**Tested but unreleased Bulk layout repair (2026-11-03):** copying a result
+identifier, location or error detail does not navigate or change endpoint
+selection. Long values are bounded and retain their complete clipboard text.
+The change remains inside the URL-owned Bulk tab; global Back/Forward and
+workflow selection are unchanged. Built loopback frontend checks pass, but
+the corrected Azure image and operator visual acceptance remain pending.
+
## Purpose
The admin UI now exposes one global Back/Forward pair and uses endpoint identity according to workflow intent. The top-level workflows remain separate because they answer different operational questions.
diff --git a/docs/PHASE_M2_BULK_OPERATIONS.md b/docs/PHASE_M2_BULK_OPERATIONS.md
index cd0bd082..75157dc3 100644
--- a/docs/PHASE_M2_BULK_OPERATIONS.md
+++ b/docs/PHASE_M2_BULK_OPERATIONS.md
@@ -1,11 +1,47 @@
# Phase M2 - Bulk Operations UI
-> **Date:** 2026-05-15 - **Version:** 0.51.0-alpha.2 - **Predecessor:** v0.51.0-alpha.1 (Phase M1 SCIM Workbench)
+> **Original release:** 2026-05-15 - **Version:** 0.51.0-alpha.2 - **Predecessor:** v0.51.0-alpha.1 (Phase M1 SCIM Workbench)
> **Origin:** [docs/UI_NEXT_GAPS_LATERAL_ANALYSIS_2026.md](UI_NEXT_GAPS_LATERAL_ANALYSIS_2026.md) S4.3
> **Scope:** Frontend-only. New per-endpoint Bulk tab + 3 pure modules + 1 free-form mutation hook + the M1-deferred "Save as live-test" emitter wired to Workbench history rows. New live section `9z-AH` adds Bulk UI contract.
---
+## Current layout repair: tested, not yet deployed
+
+Tenant 10 browser acceptance against 0.55.67 exposed result cells overflowing
+the card at a 900 px viewport. Long absolute resource locations expanded CSS
+Grid's `1fr` tracks because their minimum size was content-driven. This was
+an unintended defect; the failure screenshot was inspected and no visual
+baseline was regenerated.
+
+The repair keeps the identifier/status tracks at 80 px and bounds both text
+tracks with `minmax(0, 1fr)`. Result identifiers, actual locations and actual
+error details use [CopyableField](../web/src/components/primitives/CopyableField.tsx)
+with truncation. Full values remain available through tooltips and copying;
+missing locations and empty details retain non-copyable placeholders.
+Bulk request/response semantics, success/failure badges, CSV failure export
+and retry behavior are unchanged.
+
+| Claim | Verification |
+|---|---|
+| Complete DOM values and copy affordances | Component regression observed RED, then 10/10 component tests passed |
+| Bounded layout independent of hostname | Deterministic long URL/error fixture fails on unchanged Azure image, then passes on built loopback frontend at 1440 px and 900 px |
+| Actual truncation, not CSS presence alone | Browser asserts positive client width and scrollWidth greater than clientWidth on each truncation element |
+| Full values remain copyable | Browser reads clipboard and compares the entire location and error detail |
+| Execution and cleanup still work | Five bulk/manual browser flows pass with migrated-dev backend; real creation, duplicate, retry, CSV content and owned-fixture deletion remain asserted |
+
+The passing frontend is an owned loopback production build, not a repaired
+Azure deployment. Reviewed publication, a new immutable image and complete
+actual dev/canary acceptance remain mandatory. Performance remains unqualified
+under the migration-only exception; customer production is excluded.
+
+Self-improvement applied: deterministic long-value coverage catches the
+deployment-hostname gap before another migration. Design disposition accepted:
+reuse the existing primitive and keep rendering inside the cohesive Bulk tab;
+no new domain service, strategy or dependency is justified.
+
+---
+
## 1. Why this exists
[docs/UI_NEXT_GAPS_LATERAL_ANALYSIS_2026.md](UI_NEXT_GAPS_LATERAL_ANALYSIS_2026.md) S4.3 names the Bulk Operations UI as a Tier 1 Operational Completeness gap. The backend has shipped the full surface since v0.18.0:
diff --git a/docs/TENANT_10_MIGRATION_EXECUTION_2026_10_30.md b/docs/TENANT_10_MIGRATION_EXECUTION_2026_10_30.md
index f5543418..93a4d6f5 100644
--- a/docs/TENANT_10_MIGRATION_EXECUTION_2026_10_30.md
+++ b/docs/TENANT_10_MIGRATION_EXECUTION_2026_10_30.md
@@ -9,6 +9,111 @@
## Executive summary
+### Available-data deployment checkpoint (2026-11-03)
+
+The operator explicitly updated the migration scope at
+`2026-11-03T20:50:41Z`:
+
+> please proceed with whatever data is available. its alright of we do not have the latest . right now the main point is to have the dev and canary deployed and working
+
+This permits available point-in-time data rather than a synchronized latest-data
+cutover. No source-writer freeze or source mutation is performed, and later
+source changes may be omitted. Complete carried-snapshot fidelity, resource
+IDs, credentials/settings and all target acceptance gates remain mandatory.
+Customer production is untouched; performance remains unqualified under the
+separately documented migration-only exception.
+
+PR270 merged normally as
+`6102aff4a1687d2873b1349af3f95e68c3c6aeb5` after all exact-tip hosted checks
+passed. Independent review5759073624 reports no open issues, approval
+recommended, formal state COMMENTED. Protected Validate29/29 passes in935.5s.
+Authoritative merged-source41714210347 passes actual PostgreSQL copy/snapshot
+fixtures, build/push, smoke and HIGH/CRITICAL Trivy. Merged CodeQL41714183722
+and dependency-pins41714184178 pass.
+
+Decision `T10-0.55.67-PERF-EXCEPTION-2026-11-03` is publicly bound in PR270
+comment6614992034 to version0.55.67, that merged source and image
+`ghcr.io/pranems/scimserver@sha256:4a00b5421a8d5290506091fe856f3dbb4cc185e0bb799d272cb724651de9aeeb`.
+Producer and independent smoke-pull digests agree. The unique tag is
+`test-tenant10-release-0.55.67-6102aff4a168`; mutable `latest` was not promoted.
+
+Both actual target databases now pass complete nine-table content, schema,
+constraint and index equality against their carried read-only snapshots.
+Native PostgreSQL custom archives and logs are privately protected with
+Windows CurrentUser DPAPI. Dev retained all historical request logs in its
+760,214,637-byte archive rather than discarding them to shorten validation.
+The private receipts are `dev-carry.json` under
+`available-dev-aggregate-20261103` and `canary-prod-carry.json` under
+`available-canary-20261103T2050`, both below the existing protected tenant 10
+bootstrap directory. These are snapshot receipts, not freshest-data guarantees.
+
+Dev is created from the exact digest. Its initial environment transformation
+was corrected after semantic RED/GREEN, and all actual environment values and
+secret references now match the intended preserved configuration. The corrected
+revision is Healthy and the unhealthy initial owned revision was deactivated.
+Direct HTTP health returns200; the frontend opens its authentication dialog.
+Actual version/Prisma-backend preflight passes. Full dev live acceptance
+completed with 1,841 reported passing checks and zero failures across 169
+sections and 1,906 flow steps in 793.377s. Four reported passes are explicitly
+skipped placeholders for removed Admin Schema/ResourceType APIs (9m-A/B/C
+and 9y), not exercised checks. The orphan guard captured 76 pre-existing
+endpoints and reported no leaked endpoints. Full actual dev browser
+acceptance completed with 347 passes, one failure, zero skips, zero flakes
+and no runner errors in 1,037.046s. Its sole failure measured four result
+cells overflowing the Bulk card at a narrow viewport. The failure screenshot
+was inspected: this is an unintended layout defect, not intended visual drift.
+No visual baseline diff was produced and no baseline was regenerated.
+
+The default minimum width of `1fr` grid columns let long absolute resource
+URLs expand the result grid. A separate fix branch,
+`fix/tenant10-bulk-result-overflow-20261103`, bounds the two text columns
+with `minmax(0, 1fr)` and uses the existing copy/truncate primitive for result
+values. The component regression has RED/GREEN (9 to 10 tests), and a
+deterministic long-URL/error-message browser case reproduces RED against
+the unchanged Azure image. All five bulk/manual browser flows then pass on
+the patched, built loopback frontend proxied to migrated dev. They measure
+1440/900-pixel layouts, prove actual text overflow is clipped, verify complete
+clipboard values, and retain fixture cleanup. The GREEN screenshot was also
+inspected. This is tested but unreleased frontend work, not a corrected Azure
+image or completed target acceptance.
+
+Both added browser specs compile under a strict, explicitly owned type-root
+configuration. Full web type checking still has 66 diagnostics, including two
+production diagnostics. An executable virtual-HEAD comparison against
+6102aff4 verifies the same baseline and no added diagnostics without changing
+files or borrowing dependencies. The production build and all size budgets
+pass. Updated whole-web coverage passes 1,662 tests across 125 files in
+619.76s: statements83.06%, branches75.86%, functions75.53%, lines85.74%;
+the configured floors pass. Canary creation/acceptance, operator visual
+acceptance and registry cutover remain pending. No claim that dev is fully
+validated or ready for customer production is made.
+
+The independent web coverage gate also passed in the owned release worktree:
+1,661 tests across 125 files, with statements 83.06%, branches 75.89%,
+functions 75.53% and lines 85.74%. The configured floors passed. This is
+unit/coverage evidence, not browser layout or operator visual acceptance.
+
+#### Confirmed execution issues and prevention
+
+| ID | Type / severity | Symptom and mechanism | Resolution and why it works | Detection / earliest possible gate |
+|---|---|---|---|---|
+| AD-1 | Azure service / Medium | Two copy jobs reached Failed provisioning; executions API returned InternalServerError although the managed environment itself was Succeeded. | Stop only owned commands, delete only the two owned failed jobs and verify absence. Use direct native PostgreSQL clients without changing source state. Native carries pass complete fidelity/schema checks. | Actual provider/runtime check caught this; the first job readiness result was the earliest possible detection. No repeated blind allocation. |
+| AD-2 | Transfer scalability / Medium | Full-row JSON sorting transferred and sorted the large dev request-log corpus rather than using compact row fingerprints. The first attempt was stopped before target writes. | Reuse the existing production server-side nested-MD5 content fingerprint and hash-bind the combined manifest, while retaining full schema/constraint/index equality and the exported snapshot. | Actual dev transfer latency exposed this; corpus-size-aware manifest review could have caught it before execution. Preserve all logs, not a silently smaller data set. |
+| AD-3 | Orchestration budget / Low | The40-minute deployment receipt wait expired while the760MB dev transfer continued and progressed through dump/restore/final verification. | Extend only the receipt wait, not rerun the dump or restore. The existing transfer subsequently passes and creation proceeds from its verified receipt. | Wait deadline caught this; actual data-volume/phase budgeting was the earlier opportunity. A wait timeout is not proof that its dependency failed. |
+| AD-4 | Deployment configuration / High | PowerShell `switch` changed automatic `$_` from the environment entry to its name string. The default branch read `.value` from the string, emitting empty non-secret values, includingPORT. Ingress8080 therefore did not match dynamic listening port42145. | Capture the original entry before `switch`; preserve its value in the default branch. Actual-AST regression initially fails with `RED: actual deployment transformation did not preserve PORT`, then passes all preserved values, override and secret reference. Repair only the owned exact-image dev app and independently read back every environment value/reference. | Actual startup/health gate caught this. An actual-transformation preservation test before creation was the earliest possible gate and is now applied. Data restoration and secret references were unaffected. |
+| AD-5 | Client routing / Medium | Repaired revision was Healthy on8080, but proxy-routed health still timed out. Direct routing returnedHTTP200 in0.131s. | Scope direct routing to the acceptance process; restore request defaults afterwards. No machine-wide proxy or corporate registry setting changes. Actual version/backend preflight then passes and live tests start. | Actual HTTP comparison distinguished client routing from service health. A paired direct/proxy reachability preflight could have found it earlier. |
+| AD-6 | UI layout / High | Actual dev browser acceptance has one failure: four bulk-result cells exceed the card at900px. Short loopback URLs had passed earlier measured coverage, while Azure's longer URLs expose the intrinsic minimum width of fractional grid columns. | Unintended defect. Bound flexible tracks with minmax(0,1fr), compose existing CopyableField truncation, and keep placeholders non-copyable. Unit RED/GREEN and five built-frontend browser flows pass, including deterministic long values, two viewport bounds, actual clipping and exact clipboard contents. Review, immutable image production and actual Azure rerun remain pending. | Actual Azure bounded-layout assertion caught it. A deliberately long URL/error fixture in the earliest local browser run could have caught it before release; that fixture is now applied and the general pattern is recorded. |
+| AD-7 | Evidence tooling / Low | Parsing the combined line/JSON reporter output as one JSON document failed because a console summary followed the completed JSON envelope. Retry paths also had no distinct-attempt parameter. | Preserve the original protected log; parse its bounded complete envelope for the original outcome. Future runs use a dedicated JSON report, explicit attempt names, no-overwrite guards and encryption roundtrip hashes. Successful browser receipts require nonzero exercised tests and zero skips/failures/flakes/errors, and distinguish loopback preview from deployed-target scope. | Post-run outcome extraction caught it. A dedicated report file and immutable-attempt guard were the earlier opportunity; both are now applied. |
+| AD-8 | Validation configuration / Low | A per-session strict browser TypeScript config could not find node/vite type definitions because resolution started outside the owned web package. | Explicit owned @types root and Vite client declaration path give a strict browser-spec exit0. No package install, dependency borrowing or source-config weakening. Existing full-project errors remain explicitly non-green and are compared to HEAD through the compiler. | First focused compiler run caught the resolution issue; explicit config-relative type ownership could have prevented it. |
+| AD-9 | Tool selection / Low | A targeted npx ESLint invocation fetched a cached tool but failed because the web package has no eslint.config file. The chained size command therefore did not run. | Classify the existing unconfigured web lint gate as N/A, not PASS. Do not add unrelated lint tooling. Run the existing size command separately: all budgets pass. No source dependency manifests or locks changed. | Targeted lint invocation caught it; checking for the repository-required config before invoking a tool was the earliest opportunity. |
+
+These entries use retained command results and actual provider observations.
+They do not claim that the complete historical session transcript has been
+reconciled. Self-improvement is applied through the actual transformation
+regression and full provider readback, not a presence-only configuration test.
+Design disposition accepted: keep this recovery as scoped operational tooling;
+no runtime abstraction or general admission bypass is justified.
+
### Authorized performance-admission exception (2026-11-03)
**Decision ID:** `T10-0.55.67-PERF-EXCEPTION-2026-11-03`.
diff --git a/docs/UI_GUIDE.md b/docs/UI_GUIDE.md
index 6b5bba76..0415b877 100644
--- a/docs/UI_GUIDE.md
+++ b/docs/UI_GUIDE.md
@@ -273,6 +273,14 @@ These changes are being verified locally in Docker, not yet deployed.
The cap is **1000 operations and a 1 MB payload**. Before submitting you get a preview of the first ten operations and a **Copy full envelope as JSON** button, so you can inspect exactly what will be sent. Afterwards, **failure rows are downloadable as CSV** carrying the per-operation `scimType` and `detail` - fix that file and re-submit it rather than re-deriving which rows failed.
+**Tested but unreleased tenant 10 layout repair:** result identifiers, resource
+locations and error details remain bounded at wide and narrow viewports.
+Long values use ellipsis, reveal their full text on hover and copy the entire
+value rather than the visible abbreviation. The CSV-input screenshot above
+does not illustrate this updated result layout. The repair has passed built
+loopback frontend tests with the migrated-dev backend; it is not yet a
+validated Azure deployment. See [bulk layout repair evidence](PHASE_M2_BULK_OPERATIONS.md#current-layout-repair-tested-not-yet-deployed).
+
**Resource types** lists what this endpoint serves, creates custom ones beyond User and Group, and exposes the related discovery/enforcement settings in a collapsed pane above the inventory. An **Effective combined schemas** section flattens each type's core and extension attribute paths with their type, required, cardinality, and mutability characteristics.

diff --git a/docs/strategy/ENGINEERING_LESSONS_AND_PATTERNS.md b/docs/strategy/ENGINEERING_LESSONS_AND_PATTERNS.md
index f8168c44..fcc9d183 100644
--- a/docs/strategy/ENGINEERING_LESSONS_AND_PATTERNS.md
+++ b/docs/strategy/ENGINEERING_LESSONS_AND_PATTERNS.md
@@ -14,6 +14,30 @@
---
+## Long-value fixtures must precede deployment qualification (2026-11-03)
+
+The tenant 10 dev run measured a Bulk layout defect even though an earlier
+loopback browser run had passed the same bounds assertion. The assertion
+was meaningful; the short local URL did not exercise the dangerous input.
+Long Azure resource locations let CSS Grid's `1fr` tracks retain their
+intrinsic minimum width and expand beyond the result card at 900 px.
+
+Use `minmax(0, 1fr)` for bounded flexible text tracks and compose the existing
+copy/truncate primitive. Do not rely on a particular deployment hostname to
+exercise overflow. A deterministic browser fixture should supply long
+absolute URLs and long unbroken error values, assert every cell's real bounds
+at both 1440 px and 900 px, measure actual truncation, and verify that copying
+still returns the full value. Keep real API execution/duplicate/retry checks
+separate from the fixture-driven layout check.
+
+This pattern is applied in
+[bulk-and-manual-execution.spec.ts](../../web/e2e/bulk-and-manual-execution.spec.ts);
+the issue and RED/GREEN provenance are in the
+[migration execution record](../TENANT_10_MIGRATION_EXECUTION_2026_10_30.md).
+The failure was unintended; failure and GREEN screenshots were inspected,
+and no baseline was rewritten. Loopback GREEN still does not prove the
+corrected Azure image or replace operator visual approval.
+
## Execute the control inventory before claiming coverage (2026-11-02)
**Applied in:** [tenant 10 browser acceptance](../TENANT_10_MIGRATION_EXECUTION_2026_10_30.md).
diff --git a/web/e2e/bulk-and-manual-execution.spec.ts b/web/e2e/bulk-and-manual-execution.spec.ts
new file mode 100644
index 00000000..0c001c06
--- /dev/null
+++ b/web/e2e/bulk-and-manual-execution.spec.ts
@@ -0,0 +1,268 @@
+import { randomUUID } from 'node:crypto';
+import { expect, test, type Page } from '@playwright/test';
+import {
+ createFixtureEndpoint,
+ deleteFixtureEndpoint,
+ E2E_TOKEN,
+ seedAuthToken,
+} from './endpoint-fixture';
+
+const USER_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:User';
+const GROUP_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:Group';
+const headers = { Authorization: `Bearer ${E2E_TOKEN}`, Accept: 'application/scim+json' };
+let endpointId: string | null = null;
+
+test.beforeEach(async ({ page }) => {
+ await seedAuthToken(page);
+ endpointId = await createFixtureEndpoint(page, { namePrefix: 'e2e-execution' });
+});
+
+test.afterEach(async ({ page }) => {
+ if (!endpointId) return;
+ const ownedId = endpointId;
+ endpointId = await deleteFixtureEndpoint(page, endpointId);
+ const response = await page.request.get('/scim/admin/endpoints', { headers });
+ expect(response.ok(), 'cleanup inventory must be readable').toBe(true);
+ const body: { endpoints: Array<{ id: string }> } = await response.json();
+ expect(Array.isArray(body.endpoints)).toBe(true);
+ expect(body.endpoints.map((endpoint) => endpoint.id)).not.toContain(ownedId);
+});
+
+async function uploadCsv(page: Page, text: string) {
+ await page.getByTestId('bulk-csv-input').setInputFiles({
+ name: 'owned-execution.csv',
+ mimeType: 'text/csv',
+ buffer: Buffer.from(text),
+ });
+}
+
+async function persistedUser(page: Page, id: string, userName: string) {
+ const response = await page.request.get(`/scim/endpoints/${id}/Users`, { headers });
+ expect(response.status()).toBe(200);
+ const body: { Resources: Array<{ id: string; userName: string; displayName?: string }> } =
+ await response.json();
+ expect(Array.isArray(body.Resources)).toBe(true);
+ const matches = body.Resources.filter((user) => user.userName === userName);
+ expect(matches).toHaveLength(1);
+ expect(matches[0].id).toMatch(/^[0-9a-f-]{36}$/i);
+ return matches[0];
+}
+
+test('Bulk persists successful rows, reports a duplicate, and retries only the corrected row', async ({ page }) => {
+ const id = endpointId!;
+ const capability = await page.request.get(`/scim/endpoints/${id}/ServiceProviderConfig`, { headers });
+ expect(capability.status()).toBe(200);
+ expect((await capability.json()).bulk.supported).toBe(true);
+ const first = `bulk-first-${randomUUID()}`;
+ const second = `bulk-second-${randomUUID()}`;
+ const corrected = `bulk-corrected-${randomUUID()}`;
+ await page.goto(`/endpoints/${id}/bulk`);
+ await uploadCsv(page, `userName,displayName\n${first},First\n${first},Duplicate\n${second},Second`);
+ await expect(page.getByTestId('bulk-preview')).toContainText('3 total operations queued.');
+ const reply = page.waitForResponse((response) =>
+ response.url().endsWith(`/scim/endpoints/${id}/Bulk`) && response.request().method() === 'POST');
+ await page.getByTestId('bulk-submit').click();
+ const response = await reply;
+ expect(response.status()).toBe(200);
+ const body: { Operations: Array<{ bulkId: string; status: string; location?: string }> } =
+ await response.json();
+ expect(body.Operations.map((operation) => operation.bulkId)).toEqual(['row-1', 'row-2', 'row-3']);
+ expect(body.Operations.map((operation) => operation.status)).toEqual(['201', '409', '201']);
+ await expect(page.getByTestId('bulk-result-success-count')).toHaveText('2');
+ await expect(page.getByTestId('bulk-result-failure-count')).toHaveText('1');
+ await expect(page.getByTestId('bulk-result-row-row-2')).toContainText('409');
+ await expect(page.getByTestId('bulk-result-row-row-1')).toContainText(body.Operations[0].location!);
+ await persistedUser(page, id, first);
+ await persistedUser(page, id, second);
+ const [download] = await Promise.all([
+ page.waitForEvent('download'),
+ page.getByTestId('bulk-download-failures').click(),
+ ]);
+ expect(await download.failure()).toBeNull();
+ const stream = await download.createReadStream();
+ if (!stream) throw new Error('Failure CSV download has no readable stream.');
+ const chunks: Buffer[] = [];
+ for await (const chunk of stream) chunks.push(Buffer.from(chunk));
+ const failuresCsv = Buffer.concat(chunks).toString('utf8');
+ expect(failuresCsv).toContain('bulkId,method,status,scimType,detail');
+ expect(failuresCsv).toContain('row-2,POST,409,uniqueness,');
+ expect(failuresCsv).toContain(first);
+ expect(failuresCsv).not.toContain('row-1');
+ expect(failuresCsv).not.toContain('row-3');
+
+ for (const width of [1440, 900]) {
+ await page.setViewportSize({ width, height: 900 });
+ await page.getByTestId('bulk-result').scrollIntoViewIfNeeded();
+ const bounds = await page.getByTestId('bulk-result').evaluate((element) => {
+ const box = element.getBoundingClientRect();
+ const rows = Array.from(element.querySelectorAll('[data-testid^="bulk-result-row-"]'));
+ const cells = rows.flatMap((row) => Array.from(row.children));
+ return {
+ right: box.right,
+ rows: rows.length,
+ count: cells.length,
+ overflowingCells: cells.filter((cell) => cell.getBoundingClientRect().right > box.right + 2).length,
+ };
+ });
+ await test.info().attach(`bulk-result-layout-${width}`, {
+ body: JSON.stringify(bounds, null, 2),
+ contentType: 'application/json',
+ });
+ expect(bounds.right).toBeLessThanOrEqual(width + 2);
+ expect(bounds.rows).toBe(3);
+ expect(bounds.count).toBeGreaterThanOrEqual(9);
+ expect(bounds.overflowingCells).toBe(0);
+ }
+ await uploadCsv(page, `userName,displayName\n${corrected},Corrected`);
+ const retry = page.waitForResponse((r) =>
+ r.url().endsWith(`/scim/endpoints/${id}/Bulk`) && r.request().method() === 'POST');
+ await page.getByTestId('bulk-submit').click();
+ expect((await retry).status()).toBe(200);
+ await expect(page.getByTestId('bulk-result-success-count')).toHaveText('1');
+ await expect(page.getByTestId('bulk-result-failure-count')).toHaveText('0');
+ await expect(page.getByTestId('bulk-download-failures')).toHaveCount(0);
+ await persistedUser(page, id, corrected);
+ await persistedUser(page, id, first);
+});
+
+test('Bulk bounds long result values at wide and narrow viewports without losing their text', async ({ page }) => {
+ const id = endpointId!;
+ const location = `https://${'tenant'.repeat(20)}.example.invalid/Users/${randomUUID()}`;
+ const detail = `A resource with userName '${'resource'.repeat(40)}' already exists.`;
+ await page.route(`**/scim/endpoints/${id}/Bulk`, async (route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: 'application/scim+json',
+ body: JSON.stringify({
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:BulkResponse'],
+ Operations: [
+ { bulkId: 'row-1', method: 'POST', status: '201', location },
+ { bulkId: 'row-2', method: 'POST', status: '409', response: { scimType: 'uniqueness', detail } },
+ ],
+ }),
+ });
+ });
+ await page.goto(`/endpoints/${id}/bulk`);
+ await uploadCsv(page, 'userName,displayName\nowned-one,One\nowned-two,Two');
+ await page.getByTestId('bulk-submit').click();
+ await expect(page.getByTestId('bulk-result-success-count')).toHaveText('1');
+ await expect(page.getByTestId('bulk-result-failure-count')).toHaveText('1');
+ await expect(page.getByTestId('bulk-result-row-row-1')).toContainText(location);
+ await expect(page.getByTestId('bulk-result-row-row-2')).toContainText(detail);
+
+ for (const width of [1440, 900]) {
+ await page.setViewportSize({ width, height: 900 });
+ const bounds = await page.getByTestId('bulk-result').evaluate((element) => {
+ const right = element.getBoundingClientRect().right;
+ const rows = Array.from(element.querySelectorAll('[data-testid^="bulk-result-row-"]'));
+ return {
+ right,
+ rows: rows.length,
+ overflowingCells: rows.flatMap((row) => Array.from(row.children))
+ .filter((cell) => cell.getBoundingClientRect().right > right + 2).length,
+ };
+ });
+ await test.info().attach(`long-bulk-result-layout-${width}`, {
+ body: JSON.stringify(bounds, null, 2),
+ contentType: 'application/json',
+ });
+ expect(bounds.rows).toBe(2);
+ expect(bounds.right).toBeLessThanOrEqual(width + 2);
+ expect(bounds.overflowingCells).toBe(0);
+ for (const field of ['bulk-result-location-row-1', 'bulk-result-detail-row-2']) {
+ const truncation = await page.getByTestId(field).evaluate((element) =>
+ Array.from(element.querySelectorAll('span'))
+ .filter((span) => getComputedStyle(span).textOverflow === 'ellipsis')
+ .map((span) => ({ clientWidth: span.clientWidth, scrollWidth: span.scrollWidth })),
+ );
+ expect(truncation).toHaveLength(1);
+ expect(truncation[0].clientWidth).toBeGreaterThan(0);
+ expect(truncation[0].scrollWidth).toBeGreaterThan(truncation[0].clientWidth);
+ }
+ }
+ await page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
+ await page.getByTestId('bulk-result-location-row-1-copy-button').click();
+ await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe(location);
+ await page.getByTestId('bulk-result-detail-row-2-copy-button').click();
+ await expect.poll(() => page.evaluate(() => navigator.clipboard.readText())).toBe(detail);
+});
+
+test('Bulk rejects malformed CSV without submitting a mutation', async ({ page }) => {
+ const id = endpointId!;
+ let submissions = 0;
+ page.on('request', (request) => {
+ if (request.url().endsWith(`/scim/endpoints/${id}/Bulk`) && request.method() === 'POST') submissions++;
+ });
+ await page.goto(`/endpoints/${id}/bulk`);
+ await uploadCsv(page, 'userName,displayName\n"unterminated');
+ await expect(page.getByTestId('bulk-csv-error')).toBeVisible();
+ await expect(page.getByTestId('bulk-submit')).toBeDisabled();
+ expect(submissions).toBe(0);
+});
+
+test('Manual Provision creates persisted resources, reports conflict, retries, and clears results on tab change', async ({ page }) => {
+ const id = endpointId!;
+ const userName = `manual-${randomUUID()}`;
+ await page.goto(`/manual-provision?endpointId=${id}&resourceTypeId=User`);
+ const editor = page.getByTestId('manual-resource-body-input');
+ const payload = { schemas: [USER_SCHEMA], userName, displayName: 'Owned manual user', active: true };
+ await editor.fill(JSON.stringify(payload, null, 2));
+ const submit = page.getByRole('button', { name: 'Create User', exact: true });
+ const created = page.waitForResponse((response) =>
+ response.url().endsWith(`/scim/endpoints/${id}/Users`) && response.request().method() === 'POST');
+ await submit.click();
+ const createResponse = await created;
+ expect(createResponse.status()).toBe(201);
+ const resource: { id: string; userName: string } = await createResponse.json();
+ expect(resource.userName).toBe(userName);
+ await expect(page.getByTestId('provision-result-id')).toContainText(resource.id);
+ await expect(page.getByTestId('provision-result-json')).toContainText(userName);
+ expect((await persistedUser(page, id, userName)).id).toBe(resource.id);
+ const duplicate = page.waitForResponse((response) =>
+ response.url().endsWith(`/scim/endpoints/${id}/Users`) && response.request().method() === 'POST');
+ await submit.click();
+ const conflict = await duplicate;
+ expect(conflict.status()).toBe(409);
+ const failure: { detail: string } = await conflict.json();
+ await expect(page.getByTestId('provision-result-error')).toContainText(failure.detail);
+ await expect(page.getByTestId('provision-result-success')).toHaveCount(0);
+ const retryName = `manual-retry-${randomUUID()}`;
+ await editor.fill(JSON.stringify({ ...payload, userName: retryName }, null, 2));
+ await submit.click();
+ await expect(page.getByTestId('provision-result-success')).toBeVisible();
+ await expect(page.getByTestId('provision-result-error')).toHaveCount(0);
+ await persistedUser(page, id, retryName);
+ await page.getByRole('tab', { name: 'Group', exact: true }).click();
+ await expect(page.getByTestId('provision-result-empty')).toBeVisible();
+ await expect(page.getByTestId('provision-result-success')).toHaveCount(0);
+ const groupName = `manual-group-${randomUUID()}`;
+ await editor.fill(JSON.stringify({ schemas: [GROUP_SCHEMA], displayName: groupName }, null, 2));
+ const groupReply = page.waitForResponse((response) =>
+ response.url().endsWith(`/scim/endpoints/${id}/Groups`) && response.request().method() === 'POST');
+ await page.getByRole('button', { name: 'Create Group', exact: true }).click();
+ const groupResponse = await groupReply;
+ expect(groupResponse.status()).toBe(201);
+ const group: { id: string; displayName: string } = await groupResponse.json();
+ const stored = await page.request.get(`/scim/endpoints/${id}/Groups/${group.id}`, { headers });
+ expect(stored.status()).toBe(200);
+ expect((await stored.json()).displayName).toBe(groupName);
+ await page.getByTestId('provision-open-resource').click();
+ await expect(page).toHaveURL(new RegExp(`/endpoints/${id}/groups.*detail=${group.id}`));
+});
+
+test('Manual Provision cannot submit to an inactive owned endpoint', async ({ page }) => {
+ const id = endpointId!;
+ const deactivate = await page.request.patch(`/scim/admin/endpoints/${id}`, {
+ headers,
+ data: { active: false },
+ });
+ expect(deactivate.ok()).toBe(true);
+ let submissions = 0;
+ page.on('request', (request) => {
+ if (request.method() === 'POST' && request.url().includes(`/scim/endpoints/${id}/`)) submissions++;
+ });
+ await page.goto(`/manual-provision?endpointId=${id}`);
+ await expect(page.getByTestId('manual-provision-inactive-endpoint')).toBeVisible();
+ await expect(page.getByTestId('manual-resource-create-form')).toHaveCount(0);
+ expect(submissions).toBe(0);
+});
diff --git a/web/src/pages/BulkTab.test.tsx b/web/src/pages/BulkTab.test.tsx
index 6bb3d3ac..785fe5fe 100644
--- a/web/src/pages/BulkTab.test.tsx
+++ b/web/src/pages/BulkTab.test.tsx
@@ -170,6 +170,35 @@ describe('BulkTab (Phase M2)', () => {
// ─── 7. Failure-rows-as-CSV download ───────────────────────────────
+ it('preserves complete long result values in copyable fields', async () => {
+ const location = `https://${'tenant'.repeat(20)}.example.invalid/Users/u1`;
+ const detail = `A resource with userName '${'resource'.repeat(40)}' already exists.`;
+ mockMutateAsync.mockResolvedValueOnce({
+ status: 200,
+ durationMs: 100,
+ body: {
+ schemas: ['urn:ietf:params:scim:api:messages:2.0:BulkResponse'],
+ Operations: [
+ { bulkId: 'row-1', status: '201', location },
+ { bulkId: 'row-2', status: '409', response: { scimType: 'uniqueness', detail } },
+ ],
+ },
+ });
+ renderWithProviders();
+ const input = screen.getByTestId('bulk-csv-input');
+ Object.defineProperty(input, 'files', { value: [csvFile(sampleCsv)] });
+ fireEvent.change(input);
+ await waitFor(() => expect(screen.getByTestId('bulk-preview')).toBeInTheDocument());
+ fireEvent.click(screen.getByTestId('bulk-submit'));
+ await waitFor(() => expect(screen.getByTestId('bulk-result')).toBeInTheDocument());
+
+ expect(screen.getByTestId('bulk-result-location-row-1')).toHaveTextContent(location);
+ expect(screen.getByTestId('bulk-result-detail-row-2')).toHaveTextContent(detail);
+ expect(screen.getByTestId('bulk-result-id-row-1')).toHaveTextContent('row-1');
+ expect(screen.getByTestId('bulk-result-location-row-1-copy-button')).toBeInTheDocument();
+ expect(screen.getByTestId('bulk-result-detail-row-2-copy-button')).toBeInTheDocument();
+ });
+
it('Download failure rows triggers CSV with detail + scimType columns', async () => {
mockMutateAsync.mockResolvedValueOnce({
status: 200,
diff --git a/web/src/pages/BulkTab.tsx b/web/src/pages/BulkTab.tsx
index 0b538f06..c328e15e 100644
--- a/web/src/pages/BulkTab.tsx
+++ b/web/src/pages/BulkTab.tsx
@@ -51,7 +51,7 @@ import {
} from '../utils/bulk-builder';
import { toCsv, triggerCsvDownload } from '../utils/csv-export';
import { ScimErrorMessage } from '../components/primitives/ScimErrorMessage';
-import { CopyableJsonBlock, CopyJsonButton } from '../components/primitives';
+import { CopyableField, CopyableJsonBlock, CopyJsonButton } from '../components/primitives';
type BulkResource = 'Users' | 'Groups';
@@ -89,7 +89,7 @@ const useStyles = makeStyles({
},
resultRow: {
display: 'grid',
- gridTemplateColumns: '80px 80px 1fr 1fr',
+ gridTemplateColumns: '80px 80px minmax(0, 1fr) minmax(0, 1fr)',
columnGap: '12px',
padding: '6px 8px',
borderBottom: `1px solid ${tokens.colorNeutralStroke3}`,
@@ -390,14 +390,26 @@ export const BulkTab: React.FC = ({ endpointId }) => {
className={classes.resultRow}
data-testid={`bulk-result-row-${op.bulkId ?? 'unknown'}`}
>
- {op.bulkId ?? '-'}
+
{op.status ?? '-'}
-
- {op.location ?? '-'}
-
- {op.response?.detail ?? ''}
+ {op.location ? : -}
+ {op.response?.detail ? : }
))}