# Gist 2026-06 — investigator-codebase (2026-06.md distilled)

> `distill-gen: 2` (already-distilled — do NOT re-compress).
> 4-field per record: **VIỆC** · **KẾT-LUẬN** (+file:line) · **BÀI-HỌC** · **BẤT-NGỜ**. Same-topic merged. Confidence cao/vừa/thấp.
> Each line ends with a back-resolve `substring:"…"` (unique Ctrl-F) into `2026-06.md`. Covers all 18 records (3 S57bis-era + 12 moved by Harness-9 curate 2026-06-17 + 3 S65-series moved by S71 curate 2026-06-18).
> 22-marker coverage gate satisfied across this + 2026-05.gist.md.

---

## gotcha #57 family — soft-delete + bare unique index (reachable 500)

- **[cao] gotcha #57 EXTENSION reachability audit** · VIỆC: 6 candidates for soft-delete + bare `.IsUnique()` on Code → recreate-after-delete throws DbUpdateException **500**. KẾT-LUẬN: **FIX 3 Master** = Department/Supplier/Project (`Department/Supplier/ProjectConfiguration.cs:18/24/19` bare unique), all CONFIRMED-reachable (`DepartmentFeatures.cs:76+125`, `ProjectFeatures.cs:87+147`, `CreateSupplierCommand.cs:45`+`DeleteSupplierCommand.cs:20`). **SKIP 3** = ContractClause (no Create/Update/Delete handler anywhere — not CRUD-reachable), MeetingRoom (Delete sets `IsActive=false` NOT IsDeleted, `MeetingFeatures.cs:178`; Create also `&& !IsDeleted`), EmployeeProfile (Create BLOCKS reuse by design — UserId check sees soft-deleted → throws "Cần khôi phục" :160-163; EmployeeCode auto-gen atomic). Mig 46 = exactly 3 indexes. BÀI-HỌC: every OTHER bare-unique is safe (composite junction, nullable-code already filtered, or no-soft-delete). BẤT-NGỜ: **Master's GLOBAL `HasQueryFilter(!IsDeleted)` MAKES the bug — auto-hides soft-deleted from the Create dup-check so it passes, then the unfiltered index throws 500 — opposite of HRM where the bug needs a manual `!IsDeleted`**; either way the unfiltered index is the fault. · 2026-06.md · substring:"S51 gotcha #57 EXTENSION reachability audit"
- **[cao] add-kind 11-spot stack + bare-unique confirm** · VIỆC: add a HrmConfigs kind (Vehicle/Driver). KẾT-LUẬN: **HrmConfigs has NO kind-enum/registry backend — 4 separate entities (LeaveType/Holiday/ShiftPattern/OtPolicy), "kind" is FE-only union+route param**; adding 1 kind = mirror full entity stack across **11 spots** (Domain/Configuration/DbContext×2/Features-region/Controller-4-routes/DbInitializer/MenuKeys+All/Page-KIND_CONFIG/App-route/menuKeys+staticMap). **gotcha #57 CONFIRMED still bare: `LeaveTypeConfiguration.cs:19` + `ShiftPatternConfiguration.cs:19` + `OtPolicyConfiguration.cs:22` `.IsUnique()` lack `.HasFilter("[IsDeleted]=0")` — only `HolidayConfiguration.cs:18` fixed (Mig43)** → Vehicle/Driver Code UNIQUE must add the filter from day one. BÀI-HỌC: each kind = its own table (NOT discriminated) → Mig 44 must CREATE TABLE. · 2026-06.md · substring:"S50 P11-C Vehicle+Driver — HrmConfigs add-kind pattern VERIFIED"
- **[vừa] P11-C Vehicle/Driver catalog pre-flight** · VIỆC: where to home the catalog. KẾT-LUẬN: extend HrmConfigs (NOT new module) — add Region 5/6 (kind vehicles/drivers); FE = +2 KIND_CONFIG entries; VehicleBooking stays free-text (`Office/VehicleBooking.cs:13-19`, no VehicleId/DriverId FK). BÀI-HỌC: **HRM entities have NO global HasQueryFilter → manual `.Where(!IsDeleted)` + UNIQUE soft-delete needs `.HasFilter("[IsDeleted]=0")` (Holiday Mig43 lesson)**. · 2026-06.md · substring:"P11-C Vehicle+Driver catalog pre-flight"

## Permission / authz / seed model

- **[cao] BE authz split — Master write-open** · VIỆC: assess making modules visible to all roles (blocks A/B/E/F). KẾT-LUẬN: config controllers gate WRITE behind `[Authorize(Roles="Admin")]`, READ open (HrmConfigs/Catalogs/MeetingRooms) → FE-grant is pure UI-visibility there. **BUT Master 3 controllers = class `[Authorize]` ONLY, no per-action role: `SuppliersController.cs:17`, `ProjectsController.cs:11`, `DepartmentsController.cs:11` — ANY authed user can POST/PUT/DELETE via API** (FE menu is the only gate). **S55 prod data lives in `SeedRealMasterDataAsync :2267-2460` → Projects(62 :2270), WorkItems(71 :2430-2438), Suppliers(3 :2440-2456), all ungated idempotent.** BÀI-HỌC: making Suppliers/Projects/Departments visible-to-all needs `[Authorize(Roles="Admin,CatalogManager")]` or the staff can overwrite S55 master data. 10 departments now (9 + IT); 31 demo users, none zero-role. · 2026-06.md · substring:"S57 perm-broaden blocks A/B/E/F"
- **[cao] seed model — no per-employee default Read** · VIỆC: blocks C/D recon. KẾT-LUẬN: `SeedAdminPermissionsAsync DbInitializer.cs:1939-1977` loops MenuKeys.All CRUD=true (skip-existing); 2 sub-seeders give 7 roles Read+Update on PE keys and CatalogManager full-CRUD 9 master keys. **NO generic per-employee Read seed → a plain Drafter sees ONLY PE keys; most non-admin staff see ~nothing but PE.** 4 inherit-roots (Contracts/Workflows/PurchaseEvaluations/PeWorkflows) cascade root→child; Hrm/Off/Master NOT inherit (each leaf needs own row). BÀI-HỌC: grant-all pattern = mirror CatalogManager seeder but loop ALL 13 roles × key-set, CanRead-only, inserted AFTER the catalog seeder. · 2026-06.md · substring:"S57 perm-broaden RECON blocks C/D"
- **[cao] menu seed = UPSERT that re-sets Order** · VIỆC: menu reorder cross-repo SE↔NAMGROUP. KẾT-LUẬN: SE menu = `SeedMenusAsync DbInitializer.cs` tuple-list; **seed UPSERT re-sets Order (`:1845-1871`) → reordering in code propagates to Dev/prod next deploy, NO migration; but Label/ParentKey/Icon are NOT touched on existing rows → rename needs a separate labelBackfill dict**. Order = BE-only (`GetMyMenuTreeQuery.cs:35 OrderBy`); both FE render menu as-is → no FE edit for pure reorder. BÀI-HỌC: **HR is SCATTERED across 2 roots** — `Hrm` (Hồ sơ+Config+Dashboard) and transactional HR under `Off` (DonTu/DatXe/ChamCong/AttendanceReport). BẤT-NGỜ: NAMGROUP "Puro" = hardcoded FE array (NOT DB seed), index=order, flat single links vs SE deep-nested. · 2026-06.md · substring:"menu-order cross-repo recon SE↔NAMGROUP"
- **[cao] public-HRM for all-role — seed-only** · VIỆC: open HRM module (Hồ sơ/Dashboard) to every role. KẾT-LUẬN: **`EmployeesController.cs:23-25` is `[Authorize(Policy="Hrm_HoSo.Read")]` (NOT `Roles="Admin"`) — policy resolves THROUGH the permission matrix (`MenuPermissionHandler.cs:40-52`), so seeding a CanRead row also unlocks the API, NO 403** → seed BE permission is enough, no controller edit; FE auto-renders (menu-tree API-driven, `Hrm` NOT in `USER_HIDDEN_KEYS`, root auto-shows if a child has access). **`Hrm_HoSo`+`Hrm_Dashboard` ARE in `MenuKeys.All` (unlike Pe_* leaves); 13 roles in `AppRoles.All`.** BÀI-HỌC: Pe-style grant-all = `SeedAllRolesReviewReadPermissionsAsync :2055` loops all roles × keys, upsert CanRead, idempotent. BẤT-NGỜ: **`RevokeTemporarilyHiddenModulesAsync` is called LAST `:2040` in SeedAsync (after the grant `:2033`) and wipes CRUD on every `StartsWith("Hrm")||"Off"||==Personal` non-Admin row → it BEATS any earlier grant; opening Hrm needs either excluding Hrm_HoSo/Dashboard from the revoke OR granting AFTER :2040.** · 2026-06.md · substring:"S65 recon — public HRM module for all-role"

## Prod facts / census / wipe

- **[cao] prod test-data wipe + FK + PE tree** · VIỆC: wipe prod test PE + retree by Hạng mục. KẾT-LUẬN: prod PE=10 active (MaPhieu A/031-040, all WorkItemId NULL) + Contracts=7 all `[DEMO]` V1; **FK: PE children CASCADE except `Quotes→PE NO_ACTION` (multi-path) — Plan R proved a single `DELETE FROM PurchaseEvaluations` works (NO_ACTION checked end-of-statement after Details→Quotes cascade)**; PE.ApprovalWorkflowId Restrict → wipe PE before deleting AW. Demo gate OK (SeedDemoContracts/PE inside `DemoSeed:Disabled`). FE tree = `pe/PurchaseEvaluationsListPage.tsx:138-179` Project>Year>Supplier, PeListItem already has workItemId/Name → **FE-only change**. BÀI-HỌC: ~10 orphan upload folders from S23 (files not deleted on wipe). BẤT-NGỜ: 20 REAL users batched 2026-06-11 06:01 (S58 seed-fix landed; `thanh.lethanh` now exists, correcting stale S57bis memory). · 2026-06.md · substring:"S59 recon — prod test-data wipe + PE tree"
- **[cao] prod user census + pwd-policy env divergence** · VIỆC: why is the demo-user lock a no-op. KẾT-LUẬN: **`LockDemoSampleUsersAsync DbInitializer.cs:1552` hardcodes 14 named-person emails = a population that exists ONLY on Dev**; prod has 34 all-active users (20 UAT-matrix placeholders hand-created 2026-05-13, 9 real staff, `binh.lethanh`, a typo-domain `chuong.phan@solution.com.vn` dup, admin/catalog.manager/nv.test). **ROOT CAUSE seed-users-never-on-prod = prod `Identity:Password:RequiredLength=12` vs `DemoUserPassword="User@123456"`=11 chars → CreateAsync silent-fails every prod startup since 04-21; Dev fallback length 8 → Dev gets the 33 named users.** BÀI-HỌC: fix needs 20 real prod emails; keep nv.test (breaking it breaks CI smoke). BẤT-NGỜ: `bod.1@` never appears in git pickaxe → created by hand via admin UI, not seed. · 2026-06.md · substring:"S57bis lock no-op — prod user census"
- **[cao] PE entity recon — 4 đầu việc** · VIỆC: PE Year/WorkItem/create-UI/perm. KẾT-LUẬN: PE has NO Year, NO WorkItem link (free-text detail); MaPhieu gen-AT-CREATE `PurchaseEvaluationCodeGenerator.cs:23` format `PE/{YYYY}/{A|B}/{Seq:D3}`; PE controller class-`[Authorize]` only (no policy → opening the menu is enough, no silent-403); Pe_* leaves NOT in `MenuKeys.All`. BÀI-HỌC: **extending InReviewScope must match `key == MenuKeys.PurchaseEvaluations` EXACT — the prefix "Pe" would also catch PeWorkflows (admin)**; root inherit cascades. BẤT-NGỜ: WorkItem write is Admin-only (`CatalogsController:113-130`) so CatalogManager has the menu but the API write is blocked. · 2026-06.md · substring:"S57bis PE recon — 4 đầu việc sếp"

## Pre-golive verify / FE-redesign / master-import

- **[cao] pre-golive logic verify — 4 streams PASS** · VIỆC: audit P11-B/D/E/F + ApproveV2 + catalogs + S55 wiring. KẾT-LUẬN: LeaveBalance deduction exactly-once (terminal DaDuyet, guard `Status!=DaGuiDuyet :296`); AttendanceReport classifies day-type in-memory; MaTicket gen-on-Create Serializable; ApproveV2 4-module flatten correct; master-data idempotency PROVEN (re-run → identical counts). BÀI-HỌC: **Travel/Vehicle ApproveV2 = 0 tests (cookie-cutter of tested Leave/OT) — add 2 smoke post-golive.** BẤT-NGỜ: **dept "IT"/Phòng CNTT DOES exist on prod (Id 65CC6307…) but has 0 active users → ItTicket auto-assign no-ops, SLA job no target** (corrects stale S52 "no IT dept"); ops fix = assign ≥1 user to dept IT (1 UPDATE). · 2026-06.md · substring:"S56 pre-golive verify — 4 logic streams"
- **[vừa] FE-redesign Phase-2 recon** · VIỆC: 25-page redesign audit. KẾT-LUẬN: NOT a rewrite — S55 already redesigned ui-primitives+DataTable+shell so importers auto-inherit density; hover-hidden quick-win ~absent (only 1 real `opacity-0`×group-hover site, excluded); only 5/25 use `<DataTable>` (12 roll raw `<table>`); 3 Drawer candidates (Suppliers/Projects/Users-create); **no `Drawer.tsx` exists yet**; bậc-thang inline-edit reference already exists (`EmployeesListPage.tsx`). BÀI-HỌC: effort mostly S (auto-inherit) + a few M (Drawer/bậc-thang). · 2026-06.md · substring:"S56 Phase 2 FE-redesign RECON — 25 page audit"
- **[cao] master-data Excel-import recon** · VIỆC: import 3 masters from Excel. KẾT-LUẬN: **WorkItem/"Hạng mục" master EXISTS** (`Domain/Master/Catalogs/WorkItem.cs`, full CRUD) — no new table needed; PE detail is pure free-text (no FK→WorkItem); **Project MISSING Year/Investor/Location/Package (only Note free-text)**; Supplier MISSING Status/bank-acct/legal-rep + "Cả hai" unmappable (Type single-valued); seed = idempotent `existingCodes.Contains→skip`; **no bulk import (Master is single-CRUD, POST one-at-a-time)**. BÀI-HỌC: nạp via idempotent DbInitializer mirror (NOT API loop) → reaches prod by design. BẤT-NGỜ: SeedDemoMasterData + SeedCatalogs run REGARDLESS of `DemoSeed:Disabled` (outside the if-block) → real+demo data mix on prod unless gated. · 2026-06.md · substring:"S55 master-data Excel-import recon"

## FE mirror / UI-insert recon

- **[cao] mirror Hồ sơ-NS fe-user→fe-admin = patch CSS first** · VIỆC: replicate the redesigned employee page from fe-user into fe-admin. KẾT-LUẬN: **VERDICT B — a plain page-copy BREAKS COLORS; must patch `fe-admin/src/index.css` FIRST then cookie-cutter.** fe-admin index.css = 86 lines (pinned `7feb53e`, pre-S58 redesign) → **missing 4 accent palettes (teal/amberx/violet/greenx, each 50/100/500/600/700) + utilities `.icon-chip`/`.app-gradient-brand`/`.card-accent`/`.stat-value`**; brand-50..900 hex already present (incl brand-800 #175685). The fe-user page really depends on text-brand-800 ×9, the 4 accents ×4 each, icon-chip ×3, app-gradient-brand ×1. **Wiring already complete in fe-admin (0 changes): route, identical `EmployeeCreatePage.tsx`, menu `Hrm_HoSo`+staticMap; ui-primitives/types/api all parity.** Scope = 3 files (index.css insert ~40 lines + overwrite EmployeesListPage.tsx + optional heading-700). BÀI-HỌC: mirror exactly like S35 (both apps committed together); write stays Admin-gated at BE. · 2026-06.md · substring:"S66 recon — mirror Hồ sơ NS fe-user"
- **[cao] PE Section-E "Link hồ sơ" insert point** · VIỆC: add mục E "Link hồ sơ" right under mục D "Bản so sánh" in the PE form. KẾT-LUẬN: render in 4 files (SHA256-identical 2 apps): `components/pe/PeDetailTabs.tsx` + `PeWorkspaceCreateView.tsx` × {fe-user,fe-admin}; NOT tabs — 5 vertical `<Section>`. **Mục D lives in `ChonNccSection` (`PeDetailTabs.tsx:1302-1375`), d.Bản so sánh at :1337-1348 = `GeneralAttachmentsSection` upload filtered `supplierId===null` purpose=ComparisonTable. INSERT E at `PeDetailTabs.tsx:1348` (after mục D's `</div>`, before paymentTerms :1350); create-view at `PeWorkspaceCreateView.tsx:277`.** **BE `PurchaseEvaluation.cs` has NO URL field — 1 link = add `string? HoSoLink`(1000)+Mig+cmd+DTO+validator; many links = child entity (heavy).** BÀI-HỌC: attachments are IFormFile-only (`PurchaseEvaluationAttachmentFeatures.cs:18-55`) — cannot reuse for a URL. BẤT-NGỜ: comment :1314 claims "purpose=ComparisonTable OR supplier-row null" but the real filter :1315-17 is ONLY `supplierId===null`. · 2026-06.md · substring:"S65ter recon — Mục E"

## Governance / wave / harness

- **[vừa] Harness 1/2/3 adap-apply recon** · VIỆC: apply AI_INFRA broadcast. KẾT-LUẬN: roster 8→10 (+tooling-auditor H1, +harvest-curator H2); SE `hmw.js` is pre-wave (AI_INFRA = canonical); email id authoritative = `se`; `git check-ignore -v` = ground-truth B6 (wave patterns must sit AFTER `!.claude/**` last-match-wins). BÀI-HỌC: git-diff + chunk-count = defense-in-depth (caught 1 self-MEMORY write, 0 RAG-write). · 2026-06.md · substring:"Harness 1/2/3 adap-apply recon — 3 slice"
- **[cao] wave h2-verify — B6 isolation + Bash surprise** · VIỆC: audit wave write-isolation. KẾT-LUẬN: B6 = 2 rules — transient `wave-*/`+`agent-teams/` gitignored (audit-noise=0) AND canonical `agent-memory/**/MEMORY.md` TRACKED (rogue writes surface in git status); all 10 MEMORY.md tracked. BÀI-HỌC: ordering gotcha — wave/team patterns MUST sit after `!.claude/**` or it un-ignores everything. BẤT-NGỜ: **Bash tool = `/usr/bin/bash` NOT PowerShell despite env=PowerShell → `Get-ChildItem`/`Select-String`/`Test-Path` fail (exit 2/127); read-only Bash-only subs MUST use POSIX.** · 2026-06.md · substring:"S50 wave `h2-verify` — B6 guardrail audit"

## Monthly drift audit

- **[vừa] 2026-06 monthly drift audit** · VIỆC: ground-truth counts vs docs. KẾT-LUẬN: migrations=42 (last AddLeaveBalances) / gotchas highest=#56 (file header has no self-count) / tests=154 / tables≈91; biggest drift = ef-core-migration SKILL (says 31 mig / 59 bảng / 111 test). BÀI-HỌC: schema-diagram migration table stops at Mig 16 → missing §§ for Mig 27-42. BẤT-NGỜ: STATUS backlog "Curate 4 agent MEMORY 35.7/…" is STALE (already curated S40) → remove. · 2026-06.md · substring:"MONTHLY DRIFT AUDIT"
