Popup Dual-Run and Rollback Runbook¶
Purpose¶
It keeps the legacy KJPopup as the default path while letting a user compare the new KJModal of the same work directly through a legacy header icon. Only a TYPE:ACTION the user has confirmed as consistent is switched to the new default, one unit at a time, and the legacy runtime is not deleted until every operational check has passed for seven days after the last switch.
- Minimum recommended model:
GPT-5.6 Luna - Minimum reasoning effort:
high - Basis: the write mutation and callback preservation, the per-tenant contract, the staged deployment and the immediate rollback have to be verified together.
- If a lower model is used, add: a person re-checks the full field, event and CRUD matrix of that popup, the real browser legacy and new captures, and the API payload and callback and reload evidence.
Fixed Contract¶
- An unverified popup uses the legacy popup as its default path.
- Every legacy popup with a registered new owner shows a small icon-only
Open new popupcontrol next to the header close button. - The icon opens the new popup immediately with no intermediate confirmation dialog, and the current legacy form's values are not copied into the new form.
- Opening the new popup is not an automatic fallback, a retry or an incident detector.
- It passes the legacy
POPUP.createDialogtype,action,size,username,data,paramandreturnFunccontract through unchanged. - Whether the save happens on the new or the legacy path, the existing callback, table reload, CRUD and API payload and side effect have to be the same.
- Only
KJModal.createStandard()uses the new standard style. The default UI and keyboard behaviour of plainKJModal.create()and of the legacy runtime must not change through the shared foundation deployment. - Only an operation the user has confirmed is added to
ACCEPTED_MODERN_DEFAULTSinpublic/assets/js/modules/shared/kj-popup-modern-router.js, as aTYPE:ACTIONentry. An empty list is the fully legacy-first state. - A new default is named only in
ACCEPTED_MODERN_DEFAULTS, and removing it from that list returns to the legacy path.
Shared Foundation Deployment Gate¶
The shared foundation includes only the following.
- the opt-in style of
KJModal.createStandard()and nested Escape handling - the legacy header icon and the audit record in
kj-modal-legacy-bridge.js - the legacy-first owner routing and the per-operation switch list in
kj-popup-modern-router.js POST /api/platform/popup-migration/events- keeping the legacy JS/CSS loader
- the verification scripts and this runbook
When only the shared foundation is deployed, the existing legacy callers stay as they are, and only a migrated caller that already invoked a direct owner is routed to POPUP.createDialog() with the same contract. The business CRUD and callback behaviour must not change. The following has to pass before deployment.
After the shared foundation is deployed, every registered new owner can be compared immediately through the legacy header icon. The icon is not controlled per popup by environment configuration or a service restart.
node scripts/probes/test_kj_modal_standard.js
node scripts/probes/test_kj_modal_legacy_bridge.js
node scripts/probes/test_kj_popup_modern_router.js
node scripts/probes/test_popup_legacy_return_contract.js
node scripts/browser/popup/verify_legacy_popup_retirement.js --json
playwright-cli -s=ctc-popup-review run-code --filename scripts/browser/verify/playwright_verify_legacy_scale_ticket_crud.js
playwright-cli -s=ctc-popup-client2 run-code --filename scripts/browser/verify/playwright_verify_booking_client2_dual_run.js
php -l app/platform/api_routes.php
php -l app/shared/ui/runtime.php
php -l frontend/partials/_javascript.php
In the browser, with a CLIENT_ID=1 account, open one registered popup from the app menus and check the following.
- A default click opens the existing popup's header, body, controls, ESC and X, overlay and CRUD path exactly as before.
- The legacy header shows only a small
Open new popupicon with no text, and the new popup footer has no legacy switch button. - Clicking the icon closes the legacy popup immediately and opens the new popup with the same record, action and callback contract. No intermediate
window.confirmis called. - Clicking the icon alone causes no business API write.
/api/platform/popup-migration/eventsreturns parseable JSON with200, and the event body is left in the request log.
Per-Popup Deployment Units¶
One deployment contains only one popup ID, or the smallest bundle sharing the same business and write contract. Each unit has to be a commit separate from the shared foundation, and the following is recorded.
| Item | Required record |
|---|---|
| Popup ID / screen | CTC-10-POPUP-xx, menu path, URL |
| Release | commit SHA, deployment time, deployer |
| New owner | the caller and owner module files |
| Legacy contract | type:action, param, data ID, callback |
| Data contract | the full fields, validation, CRUD endpoints and payload, side effects |
| Browser evidence | a new screenshot, a previous-version screenshot, console and API results |
| Rollback criteria | P1/P2 defects, save mismatches, work interruption, callback or reload errors |
| Result | PASS, ROLLED BACK or OBSERVING |
Per-popup verification uses the matching row of docs/reference/modernization-v1/baseline/05-legacy-popup-retirement-inventory.md and the field, event and CRUD lists of 07-popup-deep-behavior-parity.md exactly as they are.
New Version Usage Audit¶
The bridge records the following events with POST /api/platform/popup-migration/events.
new-version-requestednew-version-openednew-version-failedlegacy-default-unavailable
A record contains popupType, the resolved action, at most 25 record IDs, clientId, the source path, the release SHA and the failure text. The release SHA prefers the deployment environment value in the order CTC_RELEASE_SHA, GITHUB_SHA, SOURCE_VERSION, and when none is present it is resolved from the Git HEAD of the current deployment checkout and injected into the app shell. The endpoint does not modify the business DB, and the existing API JSONL request log preserves the user, session and request body. For an operational check, query /api/platform/admin/logs/codex in a super-admin session, narrowed to path=/api/platform/popup-migration/events, method=POST and the required period and username.
Per-Popup Approval, Switch And Rollback¶
A problem where the new popup does not open at all is blocked by the pre-deployment browser gate. The operational rollback targets are a difference in save, lookup or business flow, or an inability for field users to work.
- The user compares the legacy default path and the new path from the header icon on the same record and action.
- Only an operation the user approves as fully matching in fields, validation, CRUD payload, callback and reload, and nested flow is added to
ACCEPTED_MODERN_DEFAULTS. - If a problem appears after switching to the new default, remove only that one
TYPE:ACTIONline from the list to return immediately to the legacy default. The shared foundation and the other popups that already passed are not rolled back. - Obtain the popup ID, action, record ID, release SHA and error from the audit log, and confirm the legacy open, read, write and callback reload again.
- Add the switch or rollback SHA, time, cause, affected record, and the recovery verification screenshot and API to that popup's record.
If it fails after part of the data was saved, check the impact separately from the UI rollback, following the existing business API and DB contract. Modifying or recovering operational data follows separate approval and the DB safety rules.
Seven-Day Observation After The Full Switch¶
From the business day after the last popup deployment, leave a daily check on seven distinct dates. Copy popup-dual-run-observation.example.json as the starting point.
The PASS conditions for each date:
- the paths of new popup open, read, create, edit and delete that apply to that screen work correctly
- the callback, table reload and nested popups work correctly
- no browser console error and no malformed JSON
- no
new-version-failedorlegacy-default-unavailable - the reason for each new-version icon use is classified (comparison, approval verification, or a real defect)
- if it is a real defect, the popup is re-verified after the fix
- no unrecovered data mismatch and no field blocking report
If a popup fix is deployed during the observation, its observation period is recalculated from the business day after the fix. The full deletion gate has to be satisfied for seven days measured from the most recently fixed popup.
Final Legacy Deletion Gate¶
Only when all the conditions below are true is popup.core.js, kj.popup.js, kj.popup.css, the legacy adapter and editor, the loader, and the bridge, button and event endpoints deleted in a separate removal diff.
- Every popup in the inventory has passed deployment and user verification.
- The seven-day observation after the most recent popup deployment or fix has passed.
- The observation JSON has seven unique PASS dates and
approvedForRetirement: true. - Every active caller uses a direct owner.
- The final-retirement scanner passes.
node scripts/browser/popup/verify_legacy_popup_retirement.js \
--final-retirement \
--observation-evidence docs/runbooks/popup-dual-run-observation.json \
--json
The removal diff is deployed independently as well, and the tag or SHA immediately before deployment and the rollback command are left in the release record.
Local verification record — 2026-07-16¶
- Environment: local
127.0.0.1:8081, local DB loopback, CLIENT_ID 1, admin session. - Entry: app menu
Shipping → List → Booking→Create. - Legacy-first:
BOOKING_LIST:CREATEopens in the legacyKJPopupand one icon-onlyOpen new popupis shown in the header.ACCEPTED_MODERN_DEFAULTSand the runtime defaults are an empty array. - Direct switch: one click on the icon, with no
window.confirm, gives 0 legacy and 1 modernCreate Booking. The new footer'sOpen Previous Versionbutton count is 0, and the audit request is HTTP 200. - Safety: the Submit and Create save buttons were not pressed and there was no business data write. Console errors in the final new browser tab: 0.
- Evidence:
output/playwright/legacy-first-booking-create-2026-07-16.png,output/playwright/legacy-header-direct-modern-booking-create-2026-07-16.png. - Additional real owner captures: the canonical screenshots for Container Check In, Delivery Edit and Driver Log were regenerated after their source changed.
- Legacy write contract: on the default Scale Ticket path, real local DB create, update and delete each returned HTTP 200, with 3 caller callbacks and 2 work confirmations. An empty numeric field is normalised to
0exactly as on the new path, and the verification row was deleted. Evidence:output/playwright/runtime-legacy-popup-15-create-crud-2026-07-16.png,output/playwright/runtime-legacy-popup-15-update-delete-crud-2026-07-16.png. - CLIENT_ID 2: the existing
MKBookingEditoris kept as the legacy default contract, but the visibleMKsubtitle is removed, and a tenant-awareKJBookingEditoris opened through the header icon. Both Create and Edit rendered 40 fields each on the legacy and the new path, includingSO #,DOC Cutoff,P.O.L,P.O.DandCommodity Rate, and excludingPO #. Switch confirmations: 0. Business writes: 0. Evidence:output/playwright/booking-create-client2-legacy-default-2026-07-16.png,output/playwright/booking-create-client2-new-option-2026-07-16.png,output/playwright/booking-edit-client2-legacy-default-2026-07-16.png,output/playwright/booking-edit-client2-new-option-2026-07-16.png. - Full gate: 24 popups / 123 source contracts, 24 behavior PASS, 24 canonical screenshots at 1600x1000, legacy dual-run scanner PASS, and router, bridge and parity regression PASS.
verify-change --scope=changedwas 65 PASS / 0 FAIL / 3 auth prerequisite SKIP, and it passed after the rollback-safe local DB and the 8-step field UI smoke were both cleaned up. - Deployment kill switch: with an empty environment allowlist there were 0 new icons in the Booking legacy header and 0 new modals, and adding only
BOOKING_LIST:CREATEgave 1 icon and opened the new Create Booking. Emptying the allowlist again while a new-default approval value was left in place returned to legacy immediately, and both audit POSTs were HTTP 200, with 0 confirmations and 0 business writes. This surfaced a defect where the rollback KJPopup mounted inside the hidden overlay of a closed KJModal and was not visible, and it was fixed by makingkj.popup.jsselect only the visible overlay as its parent. Evidence:output/playwright/popup-rollout-disabled-legacy-only-2026-07-16.png,output/playwright/popup-rollout-enabled-legacy-option-2026-07-16.png.
Source:
docs/runbooks/POPUP_DUAL_RUN_ROLLBACK_RUNBOOK.md— ctc repository · This page is an English translation of the source document. Identifiers, file paths and code symbols are unchanged. A user instruction quoted inside the text is kept in its original language.