KB-76C4

LỆNH 3D.2 — Deploy Nuxt canary, rollback do thiếu Nginx route

11 min read Revision 1
codexlenh-3d2balonuxtnginxrollbackproduction2026-07-17

LỆNH 3D.2 — Deploy Nuxt canary, cutover nút Balo và verify production

Ngày thực hiện: 2026-07-17
Verdict: ROLLED_BACK — NGINX_ROUTE_REQUIRED
Trạng thái an toàn: production cũ đã phục hồi, healthy, restart_count=0; Balo không đổi; bridge vẫn active

0. Kết luận điều hành

Candidate Nuxt đã typecheck/build PASS, stage riêng và PASS toàn bộ canary. Cutover kỹ thuật cũng thành công: container mới healthy, đúng socket/GID, restart_count=0 và Matrix/Reports đọc được.

Tuy nhiên đường public bắt buộc của nút không tới Nuxt:

trong container mới:
POST /api/balo/reconcile -> HTTP 401 UNAUTHENTICATED

qua production Nginx:
POST /api/balo/reconcile -> HTTP 404 {"detail":"Not Found"}

Nguyên nhân đã chứng minh bằng nginx -T: Nginx chỉ chuyển một số prefix Nuxt đã liệt kê (/api/reports/, /api/registries-pivot/, ...); generic location /api/ đang chuyển sang Agent Data. Chưa có route /api/balo/ tới Nuxt.

Lệnh 3D.2 cấm sửa Nginx config và yêu cầu rollback khi endpoint/auth lỗi. Vì vậy candidate đã được rollback ngay; không bấm nút, không chạy singleton production, không cập nhật canonical.

1. Ba câu tuyên ngôn

  1. Vĩnh viễn: vẫn giữ một nút chung theo resolvedCollection === 'balo_thuc_the', một endpoint, một bridge và một DOT canonical; không tạo đường WRITE thay thế để né Nginx.
  2. Nhầm được không: candidate chỉ được cutover sau canary PASS; khi public endpoint không tới Nuxt, rollback trả đúng container/output cũ trước khi có thao tác Balo.
  3. Tự động: health/content/public-route gates quyết định giữ hay rollback; lần này public-route gate tự chặn trước Administrator click.

2. Luật và tài liệu đã đọc

Đã đọc trực tiếp:

  • .claude/skills/incomex-rules.md — 36 mục, 8 bước, quy trình hai mũ;
  • Operating Rules SSOT v7.58 rev51;
  • VPS Operating Rules v1.0;
  • Hiến pháp v4.6.3 rev44;
  • Balo canonical v1.9 rev15;
  • báo cáo Lệnh 3D.1 rev1.

Không cập nhật Operating Rules: đây là thiếu route cụ thể của deployment hiện hành, chưa phải nguyên tắc mới.

3. Gate 0 và backup

Commit đúng Owner gate:

DOT_HEAD=de4730013ca3cee5f55cb54c00fe51f1c2f1068b
NUXT_HEAD=2ea41583422427b6b5e6d886fa7a754198e121b7
GATE0=PASS
group=incomex-balo-sync
live_GID=987
socket=root:incomex-balo-sync:0660
bridge=active
Nuxt cũ=healthy, restart_count=0
Balo dry-run=delta 0, write 0
Balo verify=PASS_RECONCILED

Ba biến đọc từ source/runtime:

NUXT_BALO_OPERATOR_ROLE_IDS
NUXT_BALO_RECONCILE_SOCKET_PATH=/run/incomex/balo-reconcile.sock
NUXT_PUBLIC_SITE_URL=https://vps.incomexsaigoncorp.vn

Administrator role ID được cấu hình server-side:

a40a1070-0b62-4a7e-b2c0-bd4bce9d41ac

Backup và evidence:

/opt/incomex/backups/lenh3d2-balo-button-production-20260716T234206Z
/opt/incomex/evidence/lenh3d2-balo-button-production-20260716T234206Z

Backup gồm output production cũ, nginx conf, inspect Nuxt/nginx, nginx -T, bridge status và Balo pre-state.

4. Build và staged candidate

typecheck=PASS
build=PASS
candidate=/opt/incomex/deploys/nuxt-output-balo-20260716T234206Z
files=1713
bytes=39187981
tree_sha256=a128fd390cccd3302425a68d19b3b424255383cf0a2e70d540bffe15a84a74d7

Inventory không lùi:

route .mjs: 44 -> 45
API .mjs:   40 -> 41
reconcile.post.mjs: 1
reconcile.get.mjs:  0

Source/candidate verifier:

BALO_ENDPOINT_FIXTURES|PASS|auth=5|request=6|bridge=8
BALO_UI_INHERITANCE|PASS|current=4|future=1|non_balo_hidden=1
Matrix markers=8
Reports handleRefresh marker=1
hardcoded report-balo URL=0

Fixture endpoint 10/10 PASS:

no_session=401
editor/agent/tac_admin=403
wrong_origin=403
missing_header=403
nonempty_body=400
administrator_updated=200
already_running=409
malformed_fail_closed=500

Fixture dùng session Administrator giả lập đúng contract, không dùng service token giả người dùng.

5. Canary runtime và kết quả

Canary:

name=incomex-nuxt-balo-canary
health=healthy
restart_count=0
network=docker_incomex
public_port=none
user=nuxtjs uid=1001 primary_gid=65533
supplementary_groups=987,65533

Chỉ có hai mount:

/opt/incomex/deploys/nuxt-output-balo-20260716T234206Z -> /app/.output
/run/incomex/balo-reconcile.sock -> /run/incomex/balo-reconcile.sock

Không mount Docker socket, DOT repo, SSH key, toàn bộ /run hoặc /opt/incomex. Socket connect bằng user nuxtjs PASS.

Canary content PASS HTTP 200:

/
/knowledge/pivot                         marker=Matrix
/reports
/reports/report-pg                       marker=Làm mới
/reports/report-balo-table
/reports/report-balo-view
/reports/report-balo-function
/reports/report-balo-trigger
/api/registries-pivot/summary
/api/reports/live?table_id=tbl_report_pg

Live request gates nội bộ:

no session=401
wrong Origin=403
missing X-Incomex-Action=403
nonempty body=400
GET=404

6. Cutover đã thử

Cutover không dùng compose:

candidate production ID=b4fcfe381f6435b89359383a73d80f9ce4a4b21b5d5f6a4a5ac2821ba4b40eb1
health=healthy
restart_count=0
group_add=[987]
candidate output mount=đúng
exact socket mount=đúng
old container giữ stopped=đúng
nginx reload lúc cutover=không

Matrix và các trang Reports qua public vẫn HTTP 200. Hai lần gọi Reports live trả timestamp mới:

2026-07-16T23:56:34.081Z
2026-07-16T23:56:36.242Z

Điều này chứng minh Reports live/no-store vẫn hoạt động trên candidate.

7. Public endpoint failure và nguyên nhân

Endpoint candidate tự thân tồn tại:

http://172.18.0.6:3000/api/balo/reconcile
HTTP/1.1 401 UNAUTHENTICATED

Cùng request qua origin production:

https://vps.incomexsaigoncorp.vn/api/balo/reconcile
HTTP/1.1 404 Not Found
Server: nginx/1.29.5
access-control-allow-origin: *
body={"detail":"Not Found"}

Header/body cho thấy response đến từ Agent Data, không phải Nuxt. nginx -T xác nhận:

location /api/reports/          -> nuxt_backend
location /api/registries-pivot/ -> nuxt_backend
...
location /api/                  -> agent_data_backend

Không có location /api/balo/ hoặc exact route tương đương.

8. Rollback đã thực hiện

Theo §9 của lệnh:

1. stop container candidate;
2. giữ nguyên candidate để điều tra;
3. rename candidate khỏi tên production;
4. rename container rollback về incomex-nuxt;
5. start container cũ;
6. verify health/content/public routes.

Kết quả:

incomex-nuxt
  ID=890fee21392ca5c7a3da5c637bbb8dc1b0b8c61a6cb23712900344c8556cd4a7
  state=running
  health=healthy
  restart_count=0
  output=/opt/incomex/deploys/nuxt-output

incomex-nuxt-failed-20260716T234206Z
  state=exited
  giữ để điều tra

incomex-nuxt-balo-canary
  state=exited

Nginx reload khi rollback: không cần. Không có hai container production chạy song song.

9. Production sau rollback

HTTP 200 và có nội dung:

/knowledge/pivot
/reports
/reports/report-pg
/reports/report-balo-table
/api/registries-pivot/summary
/api/reports/live?table_id=tbl_report_pg
Nuxt cũ=running, healthy, restart_count=0
bridge socket=active
socket=root:incomex-balo-sync:0660

Nút Balo không được coi là đã deploy vì production đã trở về output cũ.

10. Balo trước/sau

Trước và sau giống hệt:

total=2148
table=385
view=699
function=654
trigger=410
fingerprint=8ed1cd5d2f93f5f8a970497bd5e4ec87
KEEP_date_updated_hash=b2ed4caf31d04bdbfb16c575147fc47d
sequence last_value=2148
is_called=true
increment_by=1
predicted_next=2149
sequence_safe=true
DRY_RUN|PASS|delta=0|write=0
VERIFY|PASS_RECONCILED|delta=0|write=0
conflict=0

Hai dòng cuối nguyên vẹn:

2147|table:public.sua_re_khai_niem_nhap|2026-07-16 10:06:33.15177+00
2148|table:public.sua_re_ui_khai_niem_nhap|2026-07-16 10:06:33.15177+00

11. Các bước cố ý không chạy

Do rollback xảy ra trước gate Administrator production:

PRODUCTION_ADMIN_SESSION_TEST=NOT_RUN_AFTER_PUBLIC_ROUTE_FAILURE
PRODUCTION_BUTTON_CLICK=NOT_RUN
PRODUCTION_SINGLETON_HTTP_409=NOT_RUN
CANONICAL_BALO_UPDATE=NOT_RUN_PASS_CONDITION_NOT_MET

Không dùng kết quả fixture để mạo nhận production PASS.

12. Chứng minh phạm vi cấm

docker compose recreate=NONE
Nuxt/DOT/bridge code change=NONE
direct DB WRITE=NONE
PG test object=NONE
cron=NONE
GitHub push=NONE
old container deletion=NONE
nginx config change=NONE
nginx reload=NONE

Các truy vấn Balo trực tiếp đều đặt default_transaction_read_only=on; cơ chế DOT chỉ chạy --dry-run--verify.

13. Canonical KB

Không sửa Balo canonical. Điều kiện “chỉ cập nhật sau production PASS” chưa đạt, nên revision vẫn là:

Balo canonical v1.9 rev15

14. Gate Owner tiếp theo

Blocker duy nhất đã chứng minh:

NGINX_ROUTE_REQUIRED

Đề xuất một lệnh riêng, phạm vi tối thiểu:

  1. khóa lại nginx -T và backup config;
  2. thêm exact/prefix route /api/balo/reconcile hoặc /api/balo/ tới nuxt_backend, đặt trước generic /api/;
  3. nginx -t, reload có kiểm soát;
  4. chứng minh public endpoint trả 401/403/400/404 đúng trước khi cutover;
  5. khóa lại commit/runtime/Balo rồi chạy lại canary và cutover;
  6. chỉ sau public PASS mới dùng Administrator thật, bấm một lần và kiểm singleton 409.

Không nên bypass bằng URL nội bộ, không đổi endpoint sang generic Agent Data và không mở bridge public.

15. Evidence paths

evidence=/opt/incomex/evidence/lenh3d2-balo-button-production-20260716T234206Z
backup=/opt/incomex/backups/lenh3d2-balo-button-production-20260716T234206Z
candidate=/opt/incomex/deploys/nuxt-output-balo-20260716T234206Z
pre inspect=incomex-nuxt-inspect-pre.json
candidate inspect=failed-new-before-rollback-inspect.json
restored inspect=rollback-restored-inspect.json
nginx config=nginx-T-pre.txt
candidate inventory=candidate-inventory.txt
canary gates=canary-gates-live-final.txt
production pre-click=production-http-preclick.txt
Balo final=balo-final.txt
DOT final=balo-dryrun-final.log, balo-verify-final.log

CHỐT: ROLLED_BACK — NGINX_ROUTE_REQUIRED. Production cũ và Balo an toàn; dừng chờ Owner gate cho route Nginx riêng.