STATUS/HANDOFF S88 prepend (State THAT: Mig 59 · 88 bang · 419 test (45D+374I) · gotcha 73 · bundle admin BQq2eTP6/user dHa64I59) + session log + agent-memory harvest (6 sub: cicd/impl-backend/impl-frontend/inv-codebase/reviewer/test-spec). D1/D2 production already shipped:73cce1f(Run #347 PASS, +11 test, the 6 fixes) +1aa3bdb(Run success, +6 test, create-contract 1->N + list winner names). Both prod-verified (bundle rotation + Gitea run=success; cicd-monitor verdict truncated #53 -> curl-self-verify). Curate-debt: test-specialist L1 34.8KB OVER-SOFT (root-caused #53 x3 this session) DEFERRED next session as #1 NEXT-em (proper byte-exact L1->L2 + _INDEX, not rushed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
21 KiB
Implementer-Frontend Agent — Persistent Memory
Persistent diary cross-session. Auto-injected first ~200 lines at spawn (L1 HOT). Update BEFORE every stop. Tiered Memory v1: L1 HOT soft-cap ~30KB · L2
archive/on-demand · L3 RAGsearch_memoryjust-in-time. Keep entry ≤ 1.5K chars (gotcha #53). NEW agent S39 (2026-05-29) — split từ implementer (FE 2 app half). Backend scaffold history ởimplementer-backend.
🆕 S88 D2 (2026-06-25) — PE multi-winner OUTWARD FE ×2 app (BE đã đổi: create→LIST, list DTO winner names)
3 edit OUTWARD, em-main chốt FE behavior, coord BE (impl-backend đã viết). 2 SHA-pair + 1 type-DIVERGE.
- E1 CreateContractDialog (PeDetailTabs ~:2144): mut type
{contractId:string}→{contractIds:string[]}(BE tạo 1 HĐ/mỗi IsWinner). onSuccess: toast`Đã tạo ${res.data.contractIds.length} hợp đồng từ phiếu.`+navigate(/contracts/${res.data.contractIds[0]})(giữ destination/contracts/${...}của bản single). Dialog GIỮ joined winner-names (:2174) + 1 ContractType-cho-mọi-winner. Bản gốc KHÔNG invalidate → nothing to preserve. - E2 PeListPanel label (~:168):
{p.selectedSupplierName && …✓…}→ preferwinnerSupplierNames.join(', ')khiwinnerSupplierNames?.length>0, elseselectedSupplierName. Giữ ✓ +text-emerald-600. Ternary lặp 2 lần (guard + render) — optional-chain?.lengthan-toàn undefined. - E3 type (DIVERGE 2 file):
PeListItem+winnerSupplierNames:string[]sau urgent-flags (anchorisUrgentByPro/Ccm+}byte-identical 2 app → Edit same string ×2, grep-confirm KHÔNG vào inter-app diff, stat giữ 30ins/26del). Create-contract response = inline generic trong PeDetailTabs (KHÔNG named type ở types file) → E3b no-op;contractId:string|null:121/:443 = link-HĐ-đã-gắn field KHÁC, GIỮ. - SHA256 ×2 pair: PeDetailTabs
9d8120b6…, PeListPanelc97e133d…(cp admin→user, git-diff --no-index IDENTICAL). Build PASS ×2 (adminindex-C3j9HaFh.js2.18s / userindex-Ct2cpC7h.js2.23s, 0 TS err). Pre-existing warn: >500KB + realtime.ts dynamic. NO BE/test touch. LESSON: OUTWARD multi-winner = winnerSupplierNames[] (BE null selectedSupplierName khi ≥2) → FE prefer-list-fallback-single; type DIVERGED ≠ cp → edit-same + verify-not-in-diff (S87+ confirm). Tag[s88, pe-multi-winner-outward, contractIds-list, winner-names-fallback, sha256-2pair, type-diverge-edit-same].
🆕 S87+ (2026-06-25) — FROZEN PE behavior-preserving cleanup ×2 app (em-main chốt, coord BE null-safe)
4 edit PeDetailTabs.tsx (SHA-pair) + 2 wording types/purchaseEvaluation.ts (DIVERGED — KHÔNG SHA-pair, edit identical 2 file).
- E1 HoSoLink clear:
HoSoLinkRow.save(~:2020)hoSoLink: hoSoLink || null→hoSoLink.trim()(state đã string nên??""=no-op; trim ra""khi xóa). Coord BEUpdatePeDraftHandlernull-safe:""=CLEAR, null/omit=PRESERVE, val=set (fix data-loss xóa link khi edit header). GUARD: InfoTab.save (:817) +PeHeaderFormPUT body GIỮ OMIT hoSoLink (omit=preserve = đúng fix) — verify KHÔNG thêm. - E2 #70 stale-echo guard: 2 ô PRO
adjustMut(row3 NS-kỳ-này :1548 + row8 GT-còn-lại :1649) chỉsaving={adjustMut.isPending}→|| peFetching(replace_allđúng 2 occ;peFetchingcomputed :1268). Sibling ccmPeriod/proPrice/ccmPrice/proMut/ccmMut đã có. Chống fast-double-save clobber staleev.*(2 ô share 1 absolute-set mut cross-echo). - E3 stale comment :2507: "AddItemDialog giữ (dead)…" → đã xóa (grep chỉ comment match; live =
DetailDialog). Sửa thành "nút đã bỏ (dialog = DetailDialog)". - E4 type wording (2 file):
previousSelectedTotal/currentProposalTotalinline cmt "của SelectedSupplier" → "NCC trúng (IsWinner)" (BE giờ multi-winner). Block 7-dòng byte-identical 2 app → edit same → KHÔNG new divergence (grep-confirm my-line ∉ diff). - SHA256 PeDetailTabs ×2 =
36292924…(cp admin→user, git-diff empty). PeHeaderForm untouched987291c1…identical. Build PASS ×2 (admin 1946modindex-CqiQWR8e.js/ user 1935modindex-Cp0DuKFQ.js, 0 TS err). Pre-existing warn: @import CSS + >500KB + realtime.ts. NO BE/test touch. LESSON: type files DIVERGED (S76/S79 color+prose) ≠ SHA-pair → mirror = edit-identical-block + verify-not-in-diff, KHÔNG cp. Tag[s87plus, pe-frozen-cleanup, hosolink-clear-empty, gotcha70-guard, type-diverge-edit-identical].
🆕 S79 (2026-06-19) — PE detail: live thousand-sep + ghi chú giá PRO/CCM + typo (×2 app)
3 task presentation+wiring PeDetailTabs.tsx (+PeWorkspaceCreateView.tsx typo). BE thêm note body song song; FE chốt UX rồi.
- A live phân cách: chỉ 2 comp thiếu sep —
VndInlineEdit(~990) +BudgetCell(~1100): đổionChangevalue.replace(/[^\d.]/g,'')→formatVndInput(parseVnd(e.target.value))(format-on-keystroke, reuse helper module-level :51/52). Dialog money inputs (2111/2555/2645) ĐÃ dùngformatVndInput/parseVndsẵn → no-op. KHÔNG đụng phone(tel)/%/qty. - B ghi chú giá:
SuggestedPriceRows(~1509) —proPriceMutbody{minPrice,maxPrice,note}+ccmPriceMut{ccmPrice,note}(absolute-set 3-field S74). 3 call-site giá echonote:ev.*SuggestedPriceNote. StateproNoteText/ccmNoteText(useEffect sync ev.*). Textarea+nút "Lưu ghi chú" dưới PRO Min/Max & dưới CCM (gate canEdit; read-only<p>khi có note).hasAnyValue+!!note(early-return không nuốt note-only readonly). - Type DIVERGE 2 app (xác nhận S76):
PeDetailBundle+proSuggestedPriceNote+ccmSuggestedPriceNotesửa CẢ 2 file riêng (sauccmSuggestedPrice). PeDetailTabs/PeWorkspaceCreateView byte-identical → cp. - C typo:
d. Bản so sánh→d. Bảng so sánh giáPeDetailTabs:1661(<span>) + PeWorkspaceCreateView:278(label). Comment :1712 +PeAttachmentPurposeLabel[4]GIỮ (ngoài spec, không UI). - SHA256 IDENTICAL ×2 pair: PeDetailTabs
285ebcf9…, PeWorkspaceCreateView99caed4b…. Build PASS ×2 (user 1935modindex-_axiw6cM.js/ admin 1946modindex-BqPYp7Pt.js, 0 TS err). Pre-existing warn: @import CSS + >500KB + realtime.ts. NO ambiguity, full precedent. Tag[s79, pe-live-vnd-sep, suggested-price-note, sha256-2pair, type-diverge-2app].
🆕 S78 (2026-06-19) — PE "focus mode" redesign mirror fe-user→fe-admin (CONVERGE, em-main chốt UX)
Mechanical mirror-step: redesign built+reviewed-PASS ở fe-user, em-main đã chốt UX (focus overlay). fe-admin ONLY, NO BE/schema. 2 file:
- PeWorkflowPanel.tsx PURE-ADDITIVE: +
onApproved?:()=>voidprop +wasRejectconst (byte-mirrorisRejectformula mutationFn) +if(!wasReject) onApproved?.()cuốionSuccess. Trả-lại/Từ-chối giữ overlay; chỉ forward-approve auto-đóng. - PurchaseEvaluationsListPage.tsx STRUCTURAL port: bỏ 3-panel grid → list-state
mx-auto max-w-5xlpanel (4-level tree giữ verbatim) + focus-state?id=fixed inset-0 z-50overlay slide-from-right (translate-x-full→0, 2× rAF ~200ms) che menu+TopBar, top-strip "← Thu gọn"+title+"Chế độ duyệt" pill, phiếu trái scroll-riêng + panel duyệt phảilg:w-[24rem] xl:w-[26rem]stack <lg. a11y: role=dialog aria-modal, Esc, focus-trap, scroll-lock, backdrop-click, prefers-reduced-motion.selectRowmatchMedia-split →setParam('id',id)UNCONDITIONAL; MAIN comp dropnavigate(→useCallback setParam); bottomPurchaseEvaluationDetailPageGIỮuseNavigate(deep-link compat). - PRESERVE fe-admin props (rule):
<PeDetailTabs readOnly={true}>+<PeWorkflowPanel readOnly={!pendingMe} onApproved={closeFocus}>. Tình cờ trùng fe-user → redesign CONVERGE 2 app. - SHA256 IDENTICAL ×2 pair (khớp tới trailing-newline
}\n\n): page011968bb…, panel1dc24641…. LESSON: focus-overlay redesign khiến 2 app converge (props trùng) → mirror-step ra file byte-identical, đo bằngdiff+sha256sumKHÔNG mắt. Build PASS (1946 mod,index-D6IyFKgP.js, 0 TS err — tsc-b clean trước vite). Pre-existing warning: @import-order CSS + >500KB chunk + realtime.ts dynamic-import. NO ambiguity, full precedent. Tag[s78, pe-focus-mode, mirror-step, sha256-2pair, converge-2app].
🆕 S76 (2026-06-19) — PE budget MA TRẬN 3 cột + bảng lưới <table> + badge quyền NS (anh Kiệt FDC)
- Part 1 mirror (fe-user→fe-admin byte-identical, Python difflib slice-region):
PeBudgetSummaryTableBlock A → ma trận 3 cột (DỰ ÁN hiển-thị-only—/ PROcanEditPro/ CCMcanEditCcm). TypePeBudgetSummary+proInitialAmount+proAdjustmentAmountcạnhproEstimateAmount(LEGACY).proMutbody{proInitialAmount,proAdjustmentAmount,proNote}. proFull=proInit+proAdj. - Part 3 mirror:
PeWorkflowPanelapprover.join(' / ')→ map từng approver + badge✎ NS PRO(amber)/✎ NS CCM(sky) gatea.canEditProBudget/canEditCcmBudget. TypePeCurrentApprovalLevelApprover+2 cờ CẢ 2 app (purchaseEvaluation.ts types DIFFER giữa 2 app → sửa cả 2; PeDetailTabs/PeWorkflowPanel byte-identical). - Bảng lưới (em-main, anh phản hồi "chưa chia cột giống Excel"): Block A flex→
<table border-collapse>viền ô.BudgetCellxếp dọc (input full ô + Lưu dưới).BudgetNoteRow→BudgetNoteCell(td colSpan=3). LESSON: flex+gap KHÔNG ra "bảng" — phải<table>viền ô mới giống spreadsheet. - cwd-misland: Part-1
cd fe-adminnpm build → MEMORY rơife-user/.claudestray → em-main harvest về +.gitignoreguardfe-*/.claude/(S76).
🎯 Role baseline
WRITE specialist FE 2 app (fe-admin + fe-user). Cookie-cutter mirror SHA256 IDENTICAL + Pattern 16-bis 4-place + declarative KIND_CONFIG + npm build × 2. Case 1+2 only. Tools: Read, Edit, Write, Bash, Skill, Grep, Glob + 5 RAG. Skill: permission-matrix.
🚫 Split boundary
- ✅ MINE:
fe-admin/src/**+fe-user/src/** - ❌ NOT:
src/Backend/**→implementer-backend(chỉ Read DTO shape) · tests/ →test-specialist - ❌ NOT: UX flow decision (drawer/tab/modal) → em main solo
📋 Patterns proven (apply confidently)
Pattern 16-bis 4-place mirror (9× cumulative — BLESSED FOUNDATION)
Add/move page cross-app MUST mirror 4 places:
- Page/types file · 2.
App.tsxRoute · 3.lib/menuKeys.tsconst · 4. ⚠️components/Layout.tsxresolvePathstaticMap (DỄ MISS → silent sidebar drop gotcha #50) Verified clean S33/S34/S35/S36/S37/S38 = 9× cumulative. Spec MUST list 4 places explicit.
SHA256 IDENTICAL × 2 app
Viết fe-admin → cp fe-user → sha256sum verify. Khác UX (admin full sidebar vs user filter) → mirror tay + diff. Proven S36 (3 pair) + S37 (4 pair) + S38 (5 pair).
Declarative KIND_CONFIG Record (S35, 3× proven)
Single-page multi-kind CRUD URL :kind param + Record<Kind, {fields, columns, icon, label}> + renderField switch FieldType. Reuse: HrmConfigs (S35) + WorkflowApps (S38) + HrmConfigs +2 kind (S51). ADD-KIND playbook: union type + KIND_CONFIG entry + KINDS array + renderCells branch (before fallback) + import icon — :kind-driven page = NO new App.tsx route.
Pattern 14 Tailwind JIT palette
Dynamic class purged. PALETTE array full literal as const cycle index % length.
TS6 / convention
erasableSyntaxOnlycấm enum →const X = {...} as const+type X = typeof X[keyof typeof X]- Named export only (trừ App). UI 100% tiếng Việt.
- PageHeader signature (S37):
title/description/actionsonly — KHÔNGicon/childrenprop (build fail TS2322) - fe-user thiếu Card/Badge shadcn → fallback inline
<div className="rounded-lg border bg-card">
✅ Verify protocol
cd fe-admin && npm run build + cd fe-user && npm run build BOTH 0 TS error + sha256sum mirror proof. Bundle >500KB warning OK pre-existing.
📅 Recent activity (last 10 FIFO)
- 2026-06-17 (Văn phòng số "Bảng điều khiển" — mirror fe-user→fe-admin + 4-place wiring ×2 app): Mechanical mirror+wire step (frontend-designer built in fe-user, em main chốt UX). (A) index.css SYNC prereq: fe-admin STALE pre-S66 →
cp fe-user/src/index.css fe-admin/(no admin-only class to merge — utility sets identical, chỉ S66 gotcha-66 diffs: h1-h4#0b1220/wt700 + .label-eyebrow brand-600 + accent palette comments) → SHA256e8631471…identical. (B) Mirror 4 filecpuser→admin SHA256 IDENTICAL: ui/PageHeader6ff5303f…(Văn-phòng-số richer header — eyebrow/icon/accent/breadcrumb, NOT @/components/PageHeader constrained one), ui/KpiCardf8042ade…(clickable filter chip a11y role=button), ui/WidgetCard9221cbed…(gotcha66 gradient headertext-white!), pages/office/OfficeDashboardPagec6d9dc08…(composes 3 ui over EXISTING hooks, NO new API). All@/imports + Button/api/cn + types/{proposal,workflowApps,meeting} đã có ở fe-admin → 0 import fix. (C) 4-place ×2 app: page(mirror) + App.tsx import+<Route path="/office/dashboard">(NEW/office/prefix convention — page file ở pages/office/, không xung đột; flat office routes giữ nguyên) + menuKeysOffDashboard:'Off_Dashboard'(mirror BE MenuKeys.OffDashboard, đặt sauOfftrướcOffDanhBađúng thứ tự BE) + Layout staticMapOff_Dashboard:'/office/dashboard'(4th place gotcha#50, trước Off_DanhBa). Leaf admin auto via BE All[]. Build PASS ×2 (user 1934modindex-BYj_ew5Q.js, admin 1945modindex-Cn1flmn6.js, 0 TS err). CSS @import-order warning + >500KB chunk = pre-existing. NO ambiguity, full precedent (Pattern 16-bis 4-place + SHA256 mirror). Tag[office-dashboard, mirror-step, 4-place, sha256-5file, office-route-prefix]. - 2026-06-16 (S65 PE mục E "Link hồ sơ" FE ×2 app — em-main PROXY, PE-Workflow FE-stage died-empty #53): Thêm mục "e. Link hồ sơ" hyperlink NAS dưới mục "d. Bản so sánh" + rename "Dự trù PRO"→"Ngân sách PRO". 4-file ×{user,admin} SHA256-mirror: PeDetailTabs.tsx (
HoSoLinkRow:1353/1386 — useState(ev.hoSoLink), PUT echo required+hoSoLink, readOnly→<a target=_blank rel=noopener noreferrer>null-safe) + PeWorkspaceCreateView.tsx + types/purchaseEvaluation.ts (+hoSoLink). Ship Run #293 PASS, bundle both-rotate, GET phiếu thật"hoSoLink":nullbackward-compat ✓. SURPRISE: render landed on disk despite empty-return — work COMPLETE, chỉ MEMORY-update bị cắt (#53, lần này trong Workflow fan-out); em main self-gate bắt badge "DỰ TRÙ PRO" sót rename (agent chỉ đổi row label 1120/1126, sót badge 1078) → vá nốt ×2 app. Tag[s65, pe-section-e-link-FE, hosolink-row, em-main-proxy-truncated-53, workflow-fanout]. - 2026-06-16 (Department parentId — cây tổ chức, fe-admin ONLY): Case 1 master-data enrich (NO 4-place, NO menu/route/Layout — chỉ Place-1 page+type). BE đã sẵn (local
0f44d97):DepartmentDto.parentId:Guid?+ POST/PUT nhậnparentId?+ cycle-guard 409 ConflictException. fe-admin ONLY (intentional — KHÔNG fe-user, KHÔNG SHA256). 2 file: (1) types/master.tsDepartment+parentId:string|null(sau name, trước managerUserId) — DepartmentInput auto-inherit via Omit · (2) DepartmentsPage.tsx: import Select + FormState/emptyForm +parentId:string('' khi rỗng) + load-all query['departments-all']pageSize:200 (reuse pattern proven UsersPage/Workflows/AttendanceReport) +deptNameByIdMap từ allDepts cho cột "Thuộc" + mutate payloadparentId:d.parentId||null+ openEditparentId:d.parentId??''+ Dialog<Select>"Phòng cha (Thuộc khối/phòng)" sau Tên trước Ghi chú: option đầuvalue="""— Không có (cấp gốc) —" +.filter(d=>d.id!==form.id)exclude-self khi Edit (chống tự-làm-cha; cycle sâu BE guard 409) + table column "Thuộc" giữa name↔note (d.parentId?deptNameById.get()??'—':'—'). Select = native<select>passthrough,value=''↔ null sentinel (proven UsersPage departmentId). Build PASS (1941 mod, 0 TS err — tsc -b clean trước vite). NO ambiguity, full precedent (S55 enrich + UsersPage Select). - 2026-06-11 (S57bis PE WorkItem FE ×2 app — PARTIAL, on-behalf em main ghi hộ, H2-proposed): Task = PeWorkspaceCreateView select "c. Hạng mục công việc *" sau Dự án + PeHeaderForm (select + load existing + PUT/POST workItemId) + PeDetailTabs (subtitle "Dự án – Hạng mục" + FormRow + inline-edit khóa) + types +3 field. Return-truncated #53 GIỮA mirror fe-admin → em main solo mirror 7 edits PeHeaderForm + 3 edits PeDetailTabs ×2 app; PeHeaderForm SHA256 IDENTICAL. LEARNED: mirror đo bằng SHA256 (không diff mắt); option label
[Category] Code — Name+ canSubmit require; route reuse/catalogs/work-items(KHÔNG endpoint mới). SURPRISE: điểm gãy lặp tại mirror-2-app-trong-1-spawn → cân nhắc per-app stage khi slice lớn. Tag[s57bis, truncated-53, sha256-mirror, on-behalf]. - 2026-06-09 (S55 HMW P2 — Project +4 optional master fields): Case 1 master-data enrich (NO 4-place, NO menu/route/Layout — chỉ Place-1 page+type). BE adds 4 nullable Project fields parallel (implementer-backend). 2 file × 2 app: (1) types/master.ts
Project+year:number|null+investor/location/package:string|null(saunote) +ProjectInput auto-inherit via Omit · (2) ProjectsPage.tsx (single-Dialog CRUD, NO separate pages): FormState +4string(form dùng string, convert on submit) + emptyForm +4 '' + mutate payloadyear:d.year?Number():null, investor/location/package:d.x||null+ openEdityear:p.year?.toString()??'', x:p.x??''+ Dialog 4 Input sau "Ngày kết thúc" trước "Ghi chú" (Năm type=number, Chủ đầu tư, Địa điểm col-span-2, Gói thầu col-span-2) + table column "Chủ đầu tư" (p.investor??'—') giữa name↔startDate.package= valid TS object KEY (reserved chỉ khi binding-identifier) →form.package/{...form,package:x}build sạch, KHÔNG cần rename. cp admin→user SHA256 IDENTICAL: master.ts93ac1b0f…, ProjectsPageb002061…. Build PASS ×2 (admin 1945mod, user 1934mod, 0 TS err — tsc -b clean trước vite). Reuse S42 enrich-pattern (string-form + convert-on-submit). NO ambiguity, full precedent. - 2026-06-08 (S54 ItTicket reassign → CONVERGE 2 app, REVERSE S53 divergence) [harvested by em main — agent MEMORY write mis-landed, B2/B3]: S53 đã tách fe-admin-only (admin reassign). S54 cho tổ IT tự reassign → cả 2 app cần nút. Pattern mới: BE capability-flag gate thay vì FE đoán role. Dropdown đổi
GET /users→GET /it-tickets/assignable-stafftrả{canReassign:bool, staff:[{id,fullName}]}([Authorize]any-auth, KHÔNG 403 → chống gotcha #44).canReassign = staffQ.data?.canReassign ?? false(fetch on mount, KHÔNGenabled) → nút Pencil bọc{canReassign && …}. types/workflowApps.ts +AssignableStaff+AssignableStaffResult(mirror cả 2). fe-admin rewrite (bỏ UserOption/Paged) + fe-user full-add → SHA256 IDENTICAL4bcaf2f…(viết admin canonical →cp→ verify; cùng@/...import + shadcn Dialog/Select/Button identical 2 app). Build PASS ×2 (0 TS err). Gotcha (pre-existing, NOT từ change này): types/workflowApps.ts 2 app NOT SHA-identical — fe-admin cóAttendanceReportDto(P11-E) mà fe-user thiếu; 2 type S54 mirror đúng cả 2. Lesson: BE-computed capability flag = single-source → 2 app converge lại sau intentional divergence. - 2026-06-08 (S52 Task C+D-FE — ItTicket admin reassign + AttendanceReport menu, fe-admin ONLY): Both intentional mirror-break (admin-only, NO fe-user touch, NO SHA256). Task D-FE menu wiring: Page+App.tsx route
/attendance/reportALREADY exist (S52 prior). Only 2 of 4-place needed: (1) menuKeys.ts +OffAttendanceReport='Off_AttendanceReport' (mirror BE string exact, after OffChamCong) · (2) Layout.tsx staticMap +Off_AttendanceReport:'/attendance/report' (4th place gotcha #50).types/menu.ts= MenuNode tree type, key:string NOT typed-union → NO mirror there (resolvePath(key:string)). Leaf perm-gated via BE All[]→admin auto. Task C reassign: ItTicketsPage.tsx top-comment updated DIVERGES fe-user. Per-card Pencil button (cạnh 👤 assignee) → Dialog (size sm) + Select user. Users source = GET /users {params:{page:1,pageSize:200}}→{items:UserOption{id,fullName,email}} (reuse, proven PeWorkflows/Workflows/MeetingCalendar —enabled:target!==nulllazy fetch). useMutation api.put(/it-tickets/${id}/assign,{assignedToUserId})→204 (NO json read)→invalidate['it-tickets']+toast.success+close. preselect t.assignedToUserId. UI deps: Dialog(open/onClose/title/children/footer?/size) + Select(native passthrough) + Button(variant=outline) + toast(sonner) + getErrorMessage(@/lib/apiError). Build PASS (0 err, 1945 mod). git: only 3 fe-admin file, fe-user untouched.
⚠️ Anti-patterns (DO NOT)
- ❌ Touch BE files · 2. ❌ Miss 4th Layout staticMap · 3. ❌ Skip npm build × 2 · 4. ❌
git add -A· 5. ❌ Push remote · 6. ❌ UX decision autonomous → REFUSE
🔄 Curate trigger
Size > ~30KB → archive to L2 (tiered v1). Commit scope (em main commits): FE-Admin · FE-User.