The Work Order Breakdown object from the backend serializes its keys
without JsonPropertyName, so they arrive lowercase (pm/emergency/reactive/
overdue/other). The mapper copied them straight into `status`, which the
card uses as both label and the `types` drill-down id, and
`workOrderTypeDrilldownSearch` only accepts canonical WOType values — so
every breakdown row rendered lowercase and was inert (SH-354). Route both
mapping branches through a normalizer that maps type keys back to their
canonical labels case-insensitively; `other` is not a WOType, so it stays a
labelled, non-drillable "Other" row, and unknown values (legacy array-shape
lifecycle labels) pass through unchanged.
Dispatcher Performance gated its drill-down on `completionRate > 0`, so a
dispatcher with assigned work but no completions yet was inert here while
drillable from Workload. The drill-down filters by dispatcher + range and
never uses the rate, so gate on identity instead.
Also mock useDashboardTrend in the dashboard page test so it no longer
issues a real request that failed quietly in jsdom.
* feat(locations): derive site areas from state
* chore(locations): keep ticket keys out of source comments
* feat(locations): manage ordered site contacts
---------
Co-authored-by: Codex Review Integration <codex-review@local.invalid>
* feat(locations): derive site areas from state
* chore(locations): keep ticket keys out of source comments
---------
Co-authored-by: Codex Review Integration <codex-review@local.invalid>
* feat(vendors): structure the address into Street/City/State with autocomplete
The Vendor form carried one free-text "Address (optional)" line, with City,
State and Zip already present but hidden behind display:none, and a separate
"Google Maps URL" input someone had to paste into by hand.
Street Address, City and State are now three required fields. Typing three
characters in Street Address offers up to four candidates; picking one fills
all three at once, and typing without picking stays plain free text. The
Google Maps URL input is gone — the location is derived from the address, the
Street Address itself is the link in view mode, and a keyless map preview
renders once all three parts are present. Zip stays in the payload but out of
the form; the ticket scopes the visible set to three.
The suggestion algorithm, candidate cities and copy are ported from the
approved prototype rather than invented, so dev and design agree on what a
dispatcher sees. Suggestions are deterministic for a given input on purpose:
a reshuffling list moves a row out from under the pointer mid-click.
Test fixtures that predate the requirement now carry an address, so each test
still fails for the reason it is about. The two drawer tests asserting the
stored-URL "Open in Google Maps" row are rewritten to the behaviour that
replaced it.
Delivers SH-271.
* feat(work-orders): show the vendor location map in the Vendor dialog
The last of SH-271's five acceptance bullets. The Work Order Vendor dialog
gets one composed address line from the vendor dropdown payload, not the
structured parts the form and detail drawer hold, so the preview takes the
line directly — it is saved data either way, and the completeness rule exists
to stop a map of half-typed input, not to reject a stored address.
The dialog's hand-built maps URL now goes through the shared helper, so the
link and the preview cannot drift apart.
* test(vendors): update the browser specs and pixel baselines for the new address
`npm run verify` does not run Playwright, so the first push went out with the
browser suite still asserting the UI this ticket removes. Three assertions
were stale: the combined "Address (optional)" input, the "Google Maps URL
(optional)" input, and the detail drawer's separate "Open in Google Maps"
row — now replaced by the Street Address itself being the link, checked
against the derived href. A fourth test created a vendor without an address,
which the new requirement blocks; it fills one, so the test still fails only
for the reason it is about.
Baselines regenerated in mcr.microsoft.com/playwright:v1.61.1-noble, the image
CI uses — macOS font rendering produces different pixels. Exactly three of the
sixteen were rewritten (vendor add, edit, detail); the rest, including every
Work Orders shot, are byte-identical, which is the evidence that this change
stays inside the surfaces it claims.
* test(vendors): keep the map preview out of the pixel baselines
The visual suite mocks `**/api/**` and nothing else, so the address map
preview's iframe reached maps.google.com for real. Whether that frame paints,
and what it paints, depends on the network and on what Google serves that
minute — which is why `vendor-edit` failed in CI at 18178 differing pixels
while passing in a container that could not reach Google. Regenerating the
baseline would not have fixed it; it would have moved the flake.
Aborting the request pins the frame to a blank box, so the shot measures our
layout and nothing else. The committed baselines are unchanged by this — they
were already correct — and a second container run with no --update passes
16/16, which is the evidence the shot is now stable rather than merely green
once.
* fix(vendors): complete structured address map flows
* test(vendors): align add visual baseline with CI
---------
Co-authored-by: Codex Review Integration <codex-review@local.invalid>
Capturing the curl body in "$(...)" strips the trailing newline, so the
served sha256 never matched dist/index.html and every release and rollback
verify polled to the budget and failed. Hash the response stream directly
and give the test fixture a trailing newline so the suite covers it.
* fix(terraform): ignore origin response_completion_timeout in the release plan guard (SH-300)
AWS returns 0 when the timeout is unset. The provider writes null on
origin_path updates, so the first real CD plan failed closed.
* fix(ci): drop duplicate verify from the content CD workflow (SH-300)
Frontend checks already runs verify on PRs and pushes. Removing the
validate job also requires dropping needs: validate so dispatch can run.
* fix(terraform): equate origin timeout 0 and null only (SH-300)
Numeric timeout changes still fail closed. Rename the filter so it is
not read as an after_unknown allowlist.
* feat(terraform): ship dev content CD through Terraform (SH-300)
GitHub uploads immutable release prefixes; Terraform owns live publish.
Push-to-dev stays off until TERRAFORM_CONTENT_CD_ENABLED is set.
* fix(terraform): align release-plan guard flags and CloudFront verify IAM (SH-300)
* ci(terraform-isolation): re-evaluate the gate on label changes
* test(terraform-isolation): lock the ci.yaml label-event contract
* fix(terraform-isolation): do not treat terraform markdown as a mixed change
* fix(ci): do not skip Frontend checks on isolation label events
* ci(terraform-isolation): run label retriggers in a dedicated workflow
* fix: apply eslint formatting
* fix: apply additional missed eslint formatting
Two-phase runbook, ownership boundary, workspace invariants, rollback
per phase, and operational rules in terraform/README.md; CDK adoption
mode and the retired cd-cdk path in infra/cdk/README.md; gate matrix and
deployment section updates.
Governance now runs the import-plan checker tests, Terraform fmt and
validate for terraform/live/dev, the isolation gate tests, and the CDK
build, tests, and synth in both modes. A new terraform-isolation
workflow fails PRs that change terraform/** together with application
code; the terraform-isolation-override label is the reviewed exception.
Renovate gains the terraform manager.
Remove the push-to-dev trigger and the org cd-cdk.yaml caller so CI no
longer runs cdk deploy during the adoption. The workflow assumes the
pinned dev role and runs the simple scripts/deploy-web.sh against a
pinned bucket and distribution, which keeps content deploys working
after CloudFormation relinquishes the stack outputs. Staging is
untouched.
retainForTerraformAdoption=true adds the required ManageSiteInfrastructure
parameter, conditions the 13 transferred resources and the S3 auto-delete
custom resource on it, applies Retain policies, pins the live dev origin
ID, attaches the deploy boundary and HcpTerraformWorkspace tag, and
narrows the OIDC subject to StringEquals. Normal synthesis is unchanged;
template tests cover both modes.
Port the reviewed dev root and environment-owned/inventory modules from
111eb556 with the 13 pinned dev identifiers. adoption_complete is pinned
to false in code; the root has no variables so a workspace variable
cannot change what applies. The tf-poc root, staging root, and tf-poc
map entries are dropped; staging constants stay only for the checker's
cross-environment negative tests.
Three conflicts, all where dev refactored code this branch had instrumented:
- api.ts — dev extracted the session-expiry helper into
lib/auth/expire-session. Took dev's import, dropped the now-duplicate local
copy, kept the tracing import.
- work-order-board-documents-api.ts — dev replaced the ky upload with
uploadFormWithProgress, an XHR path that exists because ky's
onUploadProgress streams the body and browsers refuse that over HTTP/1.1.
Kept dev's helper and wrapped it in traceHttpOperation so the upload stays
instrumented; neither change is lost.
- work-orders-api.test.ts — kept both mock surfaces, since the merged
work-orders-api calls apiRequestRaw while other code uses the ky instance.
getMediaContent arrived from dev calling `api.get` directly, which this file
no longer imports; routed it through apiRequestRaw like its siblings, which
also brings it under tracing.