diff --git a/.claude/agent-memory/cicd-monitor/MEMORY.md b/.claude/agent-memory/cicd-monitor/MEMORY.md index efd875b..5fc9713 100644 --- a/.claude/agent-memory/cicd-monitor/MEMORY.md +++ b/.claude/agent-memory/cicd-monitor/MEMORY.md @@ -68,10 +68,11 @@ BE (test+build) ~90s · FE × 2 ~60s/app · deploy ~30s · **total ~3min code / ## 📅 Recent runs (FIFO — older → archive/git) +- **2026-06-17 S69 Run #305 (run_number 305, id419) sha=`a8bbdae` PASS ~4-5m (FE BOTH-APP foundation "Văn phòng số" + index.css sync + BE menu-seed NO-mig: 3 shared component mới PageHeader/KpiCard/WidgetCard + OfficeDashboardPage landing route `/office/dashboard` 4-place-wire BOTH apps + `fe-admin/src/index.css` SYNCED (Hồ sơ NS accent tokens, rotate admin mạnh) + BE menu key `Off_Dashboard` (MenuKeys.cs L100 + DbInitializer.cs L1825 seed parent=`Off` Order0 LayoutDashboard); deploy 9/9 session after #297–#304 all PASS):** Push `764fe70..a8bbdae` (1 commit, 20 files). Diff: FE 14 files (2× {PageHeader,KpiCard,WidgetCard,OfficeDashboardPage,App.tsx,Layout.tsx,menuKeys.ts} + fe-admin index.css) + BE 2 (MenuKeys.cs + DbInitializer.cs) + 4 agent-memory `.md`. `.tsx`/`.cs`/`.css` non-ignored → full pipeline RAN (the 4 `**/*.md` agent-memory match paths-ignore but co-mixed w/ code → Discovery#3 range any-non-ignored ⟹ whole build). GITEA_TOKEN+PROD_DB_PW empty → anon Gitea API + DB pw từ prod `appsettings.Production.json`→`ConnectionStrings.Default` (path `C:\inetpub\solution-erp\api`, uid `vrapp`). Run IN-PROGRESS first 4 polls (running 09:26→09:29) — correctly did NOT verify-bundle-mid-flight (anti#3); pre-deploy baseline captured BEFORE poll-loop: admin `CNUv1jxY` (S78 #304 live) + user `CpOskeS1` (S78 #304 live) — both == spec S68 baseline (still live, deploy not yet shipped). Polled iter5 status=success (started ~09:25 → success 09:29:50 ≈4-5m). CI gate (both proj pre-deploy ⟹ status=success ⟹ test gate **286** baseline (45D+241I) passed; `conclusion` empty — `tasks` endpoint terminal=`status:success` doesn't populate `conclusion`, trust success; 286 INFERRED from gate-passes-pre-build invariant NOT log-confirmed numerically). **★ BUNDLE BOTH ROTATE (FE-both-app + index.css sync ⟹ both MUST rotate; verified AFTER status=success +re-confirm STABLE 2nd-fetch identical no-transient — anti#3): admin ROTATE `CNUv1jxY→Bl2o_kUq`** ✓ (Văn phòng số + index.css shipped) **+ user ROTATE `CpOskeS1→BImrKQNn`** ✓ (foundation shipped). BOTH required → BOTH did. ⚠️ **Prod CI hashes (`Bl2o_kUq`/`BImrKQNn`) ≠ spec local-build (`TbkadgKd`/`DrxDysO7`) — EXPECTED divergence (CI rebuilds w/ different Node/npm/dep-resolution → different content-hash; only matters NEW≠baseline, BOTH rotated confirmed ship). Spec assumption "prod khớp local nếu source-identical" holds ONLY if byte-identical build-env — it is NOT (S77 same lesson).** Smoke **4×200**: api `/health/ready`+`/health/live` + admin root + eoffice root. **NO migration** — prod `__EFMigrationsHistory` top = `20260616035929_AddHoSoLinkToPurchaseEvaluation` (Mig 52) == repo HEAD GIỮ NGUYÊN ✓ (`git diff --name-only 764fe70 a8bbdae -- '*Migrations*'` = EMPTY; menu-seed is runtime-idempotent DbInitializer NOT EF-migration → top did NOT advance). **sys.tables=88 verified** (sqlcmd COUNT excl mighist — unchanged, menu-seed inserts rows not tables). **★ MENU-SEED VERIFIED (NEW check this run — DbInitializer seed ungated reaches prod by design): sqlcmd `SELECT [Key],ParentKey,Label,IsVisible FROM MenuItems WHERE [Key]='Off_Dashboard'` → 1 row Key=`Off_Dashboard` Parent=`Off` Label="Bảng điều khiển Văn phòng số" IsVisible=1 ✓.** **★ OFFICE-HIDDEN CONFIRMED (RevokeTemporarilyHiddenModulesAsync StartsWith("Off") scope — DbInitializer.cs L2172): sqlcmd `SELECT r.Name FROM Permissions p JOIN Roles r ON r.Id=p.RoleId WHERE p.MenuKey='Off_Dashboard'` → ONLY `Admin` (CanRead=1), 1 row / 13 roles total ✓ → non-admin NO Off_Dashboard perm → Office stays hidden (revoke executed, admin auto via All[]).** 0 regression. NO prod-data mutation (read-only curls + sqlcmd SELECT-only). Visual "Dashboard landing render / Hồ sơ NS CSS" NOT verified (anh xem mắt) — only ship+rotate+health+mig-unchanged+tables88+menu-seed+office-hidden. **LESSON: FE-both-app + index.css-sync + BE-menu-seed (NO-EF-mig) verify = both bundles MUST ROTATE + Mig-top + sys.tables MUST stay prev (menu-seed = DbInitializer runtime row-insert, NOT a migration → does NOT advance __EFMigrationsHistory NOR sys.tables; verify seed via direct MenuItems SELECT not via mig-count). Office-hidden = query Permissions-by-role: temporarily-hidden module has perm row ONLY for Admin (All[] auto-grant) after RevokeTemporarilyHidden runs. ⚠️ Prod CI bundle-hash ≠ local-build-hash is NORMAL — never FAIL on hash-mismatch-to-local, only FAIL if NOT-rotated-from-baseline. TOOLING (re-confirmed S78): Bash=POSIX (`$var`/`$env:`/`''` literal-quote mangle through bash→ssh→PS→sqlcmd layers) → write PS to `.ps1` + `powershell -File "ABS/FWD/SLASH.ps1"`; Gitea `tasks` via `Invoke-RestMethod` (match `head_sha -eq sha`, `?limit=N` honored URI-direct); SSH→PS base64 `-EncodedCommand` (UTF-16LE iconv) for BOTH appsettings-read AND sqlcmd; ⚠️ **sqlcmd string-LITERAL in query: doubled `''x''` BREAKS (PS single-quote-string closes early) → build literal via `[char]39 + "x" + [char]39` concat (NOT `''`)**; this sqlcmd build supports `-U/-P` direct (no -ConnectionString flag); table name `Roles` NOT `AspNetRoles`, `Permissions(RoleId,MenuKey,CanRead)`; CLIXML/Progress stdout-noise grep-filter out. NEVER fixed code (READ-only).** Tag `[s69, run305, pass, fe-both-app-vanphongso-foundation, shared-PageHeader-KpiCard-WidgetCard, index-css-sync-admin, bundle-BOTH-rotate-Bl2o_kUq-BImrKQNn, prod-ci-hash-NE-local-EXPECTED, be-menu-seed-Off_Dashboard-NO-mig, no-mig-top-stays-mig52, tables88-verified, menu-seed-verified-sqlcmd, office-hidden-confirmed-admin-only-1of13, sqlcmd-char39-literal-not-doubled-quote, deploy9of9, no-regression, test286-inferred]`. - **2026-06-16 S78 Run #304 (run_number 304, id418) sha=`37752eb` PASS ~4m (FE BOTH-APP 1-line CSS-precedence fix: "Hồ sơ NS" employee-detail banner NAME đen→trắng — `

` rendered BLACK do unlayered `h1,h2,h3,h4{color:#0b1220}` index.css thắng `text-white` (Tailwind v4 @layer precedence); fix = `text-white`→`text-white!` important-modifier + `text-xl font-extrabold`→`text-lg font-bold`. 2 file `EmployeesListPage.tsx` fe-user+fe-admin SHA256-IDENTICAL `8BBAEC34…`, 1-line each, NO BE/mig/index.css; deploy 8/8 session after #297–#303 all PASS):** Push `6983609..37752eb` (1 commit). Diff 2 files: both `.tsx` only → not in paths-ignore → full pipeline RAN. GITEA_TOKEN+PROD_DB_PW empty → anon Gitea API + DB pw từ prod `appsettings.Production.json`→`ConnectionStrings.Default` (path `C:\inetpub\solution-erp\api`, uid `vrapp` len24). Run IN-PROGRESS first 7 polls (running 19:55→19:58) — correctly did NOT verify-bundle-mid-flight (anti#3); pre-deploy baseline captured BEFORE poll-loop: admin `D532XZKG` (S77 #303 live) + user `CuFaBoWt` (S77 #303 live) — both == spec baseline (still live, deploy not yet shipped). Polled iter8 status=success (started ~19:55 → success 19:59:26 ≈4m). CI gate (both proj pre-deploy ⟹ status=success ⟹ test gate **286** baseline (45D+241I; FE-only ⟹ 0 BE call-site risk) passed; `conclusion` empty — `tasks` endpoint terminal=`status:success` doesn't populate `conclusion`, trust success; exact CI count NOT log-confirmed numerically — Gitea logs web-UI-only anon, 286 INFERRED from gate-passes-pre-build invariant). **★ BUNDLE BOTH ROTATE (the change-point — FE-BOTH-app, both EmployeesListPage changed ⟹ both bundles MUST rotate; verified AFTER status=success +re-confirm STABLE 2nd-fetch identical no transient — anti#3): admin ROTATE `D532XZKG→CNUv1jxY`** ✓ (`text-white!` name-color fix shipped) **+ user ROTATE `CuFaBoWt→CpOskeS1`** ✓ (same page shipped). BOTH required per spec → BOTH did (mirror S74/S75/S76/S77 pure-FE-both-app pattern — both same-SHA256 file ⟹ both rotate; frozen sibling here = ship-fail flag). Smoke **4×200**: api `/health/ready`+`/health/live` + admin root + eoffice root. **NO migration** — prod `__EFMigrationsHistory` top = `20260616035929_AddHoSoLinkToPurchaseEvaluation` (Mig 52) == repo HEAD GIỮ NGUYÊN ✓ (`git diff --name-only 6983609 37752eb -- '*Migrations*' '*Persistence*'` = EMPTY; FE cannot alter schema → top did NOT advance; prev-2 = `AddDepartmentParentId` Mig51 + `ReplaceBudgetModuleWithPeWorkItemBudgets` Mig50 chain intact). **sys.tables=88 verified** (sqlcmd COUNT excl mighist — unchanged, no schema touch). 0 regression. NO prod-data mutation (read-only curls + sqlcmd SELECT-only). Visual "tên NV trắng render đúng" NOT verified (anh xem mắt) — only ship+rotate+health+mig-unchanged+tables88; SHA256-identical-between-2-apps is a SOURCE-claim (git), not runtime DOM-equality; CSS-precedence-actually-fixed is render-time, NOT provable by curling bundle (the `!important` text is in dist css per spec-claim, but visual-black-vs-white is browser-render). **LESSON: pure-FE-BOTH-app 1-line CSS-fix verify (8th consecutive deploy this session, both apps same component SHA256-identical) = both bundles MUST ROTATE (≠ asymmetric one-app where sibling frozen; ≠ BE-only where both frozen). Even a 1-line `text-white`→`text-white!` change → new content-hash both apps. Migration top + sys.tables MUST stay = prev (FE-only). No BE call-site/DTO/endpoint smoke (no API surface — render-only). TOOLING (re-confirmed S77): Bash tool is POSIX bash (`Select-String`/`$var`/`$env:` unavailable inline) → write PowerShell to `.ps1` + invoke `powershell -File "ABSOLUTE/FORWARD/SLASH.ps1"` (Bash EATS backslash in `tmp\x.ps1`→`tmpx.ps1`); parse Gitea `tasks` via `Invoke-RestMethod`+native object (match `head_sha -like sha*`, `limit=N` ignored); SSH→PS nested-quote mangles → base64 `-EncodedCommand` (UTF-16LE) BOTH the appsettings-read AND the sqlcmd-query (no `$` in B64 passes bash+ssh clean); CLIXML progress-stream on stdout harmless (data rows clean below); read DB pw from prod appsettings when PROD_DB_PW empty. NEVER fixed code (READ-only).** Tag `[s78, run304, pass, fe-both-app-hoso-name-color-fix-text-white-important, fe-both-2files-sha256-identical-8BBAEC34, bundle-BOTH-rotate-CNUv1jxY-CpOskeS1, css-precedence-tailwind-v4-layer, no-mig-top-stays-mig52, tables88-verified, deploy8of8, ssh-encodedcommand-base64, no-regression, test286-inferred]`. - **2026-06-16 S77 Run #303 (run_number 303, id417) sha=`6983609` PASS ~5m (FE BOTH-APP additive: "Hồ sơ NS" employee-detail BANNER text-polish — name `text-xl font-extrabold`+drop-shadow, meta `text-[13px] font-medium text-white`, status badge → solid status-colored pill emerald/amber/slate. 2 file `EmployeesListPage.tsx` fe-user+fe-admin SHA256-IDENTICAL `F013B748…`, NO BE/mig/index.css; +docs 2 files `{dep-audit SKILL.md gotcha 64→65, root CLAUDE.md test 263→286}` no-build-impact; deploy 7/7 session after #297–#302 all PASS):** Push range `231a7b0..6983609` (2 commits). Diff 4 files: 2 `.tsx` + `CLAUDE.md` + `dependency-audit-erp/SKILL.md`. `.tsx` → not in paths-ignore → full pipeline RAN (docs alone would SKIP, but tsx present ⟹ build per Discovery#3 push-range any-non-ignored ⟹ whole build). GITEA_TOKEN+PROD_DB_PW empty → anon Gitea API + DB pw từ prod `appsettings.Production.json`→`ConnectionStrings.Default` (path `C:\inetpub\solution-erp\api`). Run IN-PROGRESS first 6 polls (running 19:36→19:39) — correctly did NOT verify-bundle-mid-flight (anti#3); pre-deploy baseline captured BEFORE poll-loop: admin `CcrZqfht` (S76 #302 live) + user `DniDFUB_` (S76 #302 live) — both == spec baseline (still live, deploy not yet shipped). Polled iter6 status=success (started ~19:35 → success 19:40 ≈5m). CI gate (both proj pre-deploy ⟹ status=success ⟹ test gate **286** baseline (45D+241I; FE-only ⟹ 0 BE call-site risk) passed; `conclusion` empty — `tasks` endpoint terminal=`status:success` doesn't populate `conclusion`, trust success; could NOT extract exact CI count — Gitea logs web-UI-only anon, 286 INFERRED from gate-passes-pre-build invariant NOT log-confirmed numerically). **★ BUNDLE BOTH ROTATE (the change-point — FE-BOTH-app, both EmployeesListPage changed ⟹ both bundles MUST rotate; verified AFTER status=success +re-confirm STABLE 2nd-fetch identical no transient — anti#3): admin ROTATE `CcrZqfht→D532XZKG`** ✓ (banner polish shipped) **+ user ROTATE `DniDFUB_→CuFaBoWt`** ✓ (same page shipped). BOTH required per spec → BOTH did (mirror S74/S75/S76 pure-FE-both-app pattern — both same-SHA256 file ⟹ both rotate; frozen sibling here = ship-fail flag). ⚠️ Local-build hashes (fe-user `SuT9mDAQ`, fe-admin `7ICczYiQ` per spec) ≠ deployed CI hashes — EXPECTED (CI rebuilds; only matters NEW≠baseline, confirmed). Smoke **4×200**: api `/health/ready`+`/health/live` + admin root + eoffice root. **NO migration** — prod `__EFMigrationsHistory` top = `20260616035929_AddHoSoLinkToPurchaseEvaluation` (Mig 52) == repo HEAD GIỮ NGUYÊN ✓ (`git diff --name-only 231a7b0 6983609 -- '*Migrations*' '*Persistence*'` = EMPTY; FE+docs cannot alter schema → top did NOT advance; prev-2 = `AddDepartmentParentId` Mig51 + `ReplaceBudgetModuleWithPeWorkItemBudgets` Mig50 chain intact). **sys.tables=88 verified** (sqlcmd COUNT excl mighist — unchanged, no schema touch). 0 regression. NO prod-data mutation (read-only curls + sqlcmd SELECT-only). Visual "banner text-xl/drop-shadow/status-pill render đúng" NOT verified (anh xem mắt) — only ship+rotate+health+mig-unchanged+tables88; SHA256-identical-between-2-apps is a SOURCE-claim (git), not runtime DOM-equality. **LESSON: pure-FE-BOTH-app additive verify (7th consecutive deploy this session, both apps same component SHA256-identical) = both bundles MUST ROTATE (≠ asymmetric one-app where sibling frozen; ≠ BE-only where both frozen). Docs-files in same range as tsx do NOT suppress build (range any-non-ignored ⟹ build). Migration top + sys.tables MUST stay = prev (FE-only). No BE call-site/DTO/endpoint smoke (no API surface — render-only). TOOLING (re-confirmed S76): Bash tool EATS inline `$var`/`$env:`/`@{}` in PS strings → write PowerShell to `.ps1` + run `-File`; parse Gitea `tasks` via `Invoke-RestMethod`+native object (match `head_sha -like sha*`, `limit=N` ignored); SSH→sqlcmd base64 `EncodedCommand` (UTF-16LE via iconv, no `$` in B64 passes bash clean), CLIXML progress-stream stdout harmless grep-filter out; ⚠️ this `sqlcmd` build does NOT support `-ConnectionString` flag → parse `User Id`/`Password` from conn-string via regex → `-U/-P`; read DB pw from prod appsettings when PROD_DB_PW empty. NEVER fixed code (READ-only).** Tag `[s77, run303, pass, fe-both-app-hoso-banner-textpolish, fe-both-2files-sha256-identical-F013B748, bundle-BOTH-rotate-D532XZKG-CuFaBoWt, docs-in-range-no-suppress-build, no-mig-top-stays-mig52, tables88-verified, deploy7of7, sqlcmd-no-connstring-flag-use-U-P, no-regression, test286-inferred]`. - **2026-06-16 S76 Run #302 (run_number 302, id416) sha=`536dd6b` PASS ~4m (FE BOTH-APP additive: PE "Link hồ sơ ổ mạng" render upgrade — đường-dẫn-ổ-mạng từ chữ+Copy → `` bấm-thử mở Explorer + GIỮ nút Copy dự phòng. 2 file `PeDetailTabs.tsx` fe-user+fe-admin SHA256-IDENTICAL `b415023b…`, +46/−14 mỗi file, NO BE/mig/index.css/Employee-page; deploy 6/6 session after #297/#298/#299/#300/#301 all PASS):** Push 2 files `{fe-admin,fe-user}/src/components/pe/PeDetailTabs.tsx`. `.tsx` → not in paths-ignore → full pipeline RAN. GITEA_TOKEN+PROD_DB_PW empty → anon Gitea API + DB pw từ prod `appsettings.Production.json`→`ConnectionStrings.Default`. Run IN-PROGRESS first 6 polls (running 14:54:20→14:56:57) — correctly did NOT verify-bundle-mid-flight (anti#3); pre-deploy baseline captured BEFORE poll-loop: admin `I1fpLeYw` (S75 #301 live) + user `DrQYkzh0` (S75 #301 live) — both == spec baseline. Polled iter7 status=success (started ~14:53 → success 14:57:13 ≈4m). CI gate (both proj pre-deploy ⟹ status=success ⟹ test gate **286** baseline (45D+241I; FE-only ⟹ 0 BE call-site risk) passed; `conclusion` empty — `tasks` endpoint terminal=`status:success` doesn't populate `conclusion`, trust success). **★ BUNDLE BOTH ROTATE (the change-point — FE-BOTH-app, both PeDetailTabs changed ⟹ both bundles MUST rotate; verified AFTER status=success +re-confirm STABLE 2nd-fetch identical no transient — anti#3): admin ROTATE `I1fpLeYw→CcrZqfht`** ✓ (file:// link shipped) **+ user ROTATE `DrQYkzh0→DniDFUB_`** ✓ (same component shipped). BOTH required per spec → BOTH did (mirror S74/S75 pure-FE-both-app pattern — both same-SHA256 file ⟹ both rotate; frozen sibling here = ship-fail flag). Smoke **4×200**: api `/health/ready`+`/health/live` + admin root + eoffice root. **NO migration** — prod `__EFMigrationsHistory` top = `20260616035929_AddHoSoLinkToPurchaseEvaluation` (Mig 52) == repo HEAD GIỮ NGUYÊN ✓ (`git diff --name-only 536dd6b~1 536dd6b -- '*Migrations*'` = EMPTY; FE cannot alter schema → top did NOT advance; prev-2 = `AddDepartmentParentId` Mig51 + `ReplaceBudgetModuleWithPeWorkItemBudgets` Mig50 chain intact). **sys.tables=88 verified** (sqlcmd COUNT excl mighist — unchanged, no schema touch). 0 regression. NO prod-data mutation (read-only curls + sqlcmd SELECT-only). Visual "link file:// bấm mở Explorer / nút Copy" NOT verified (anh xem mắt) — only ship+rotate+health+mig-unchanged+tables88; SHA256-identical-between-2-apps is a SOURCE-claim (git), not runtime DOM-equality (file:// scheme browsers thường block từ https-origin → click có thể no-op tùy browser, đó là lý do GIỮ Copy dự phòng — không kiểm chứng được qua curl). **LESSON: pure-FE-BOTH-app additive verify (6th consecutive deploy this session, both apps same component SHA256-identical) = both bundles MUST ROTATE (≠ asymmetric one-app where sibling frozen; ≠ BE-only where both frozen). Migration top + sys.tables MUST stay = prev (FE-only). No BE call-site/DTO/endpoint smoke (no API surface — render-only). TOOLING (re-confirmed S75): Bash tool EATS inline `$var`/`$env:` in `powershell -Command` → write PowerShell to `.ps1` + run `-File`; `certutil -hashfile … SHA256` for SHA256 (NOT findstr-pipe — quoting breaks); parse Gitea `tasks` via `Invoke-RestMethod`+native object (match `head_sha -eq sha`, `limit=N` ignored); SSH→sqlcmd base64 `EncodedCommand` (UTF-16LE, no `$` in B64 passes bash clean), CLIXML progress-stream stdout harmless; read DB pw from prod appsettings when PROD_DB_PW empty (path `C:\inetpub\solution-erp\api`, key `ConnectionStrings.Default`). NEVER fixed code (READ-only).** Tag `[s76, run302, pass, fe-both-app-pe-link-hoso-file-scheme, fe-both-2files-sha256-identical-b415023b, bundle-BOTH-rotate-CcrZqfht-DniDFUB_, no-mig-top-stays-mig52, tables88-verified, deploy6of6, no-regression, test286]`. -- **2026-06-16 S75 Run #301 (run_number 301, id415) sha=`6df1b2d` PASS ~2.5m (FE BOTH-APP additive: PE "Link hồ sơ" auto-detect render — `http(s)`→hyperlink bấm-mở (giữ nguyên) / đường dẫn ổ mạng `O:\...`→chữ+nút Copy. 2 file `PeDetailTabs.tsx` fe-user+fe-admin SHA256-IDENTICAL `da0884a5…`, NO BE/mig/index.css/Employee-page; deploy 5/5 session after #297/#298/#299/#300 all PASS):** Push 2 files `{fe-admin,fe-user}/src/components/pe/PeDetailTabs.tsx`. `.tsx` → not in paths-ignore → full pipeline RAN. GITEA_TOKEN+PROD_DB_PW empty → anon Gitea API + DB pw từ prod `appsettings.Production.json`→`ConnectionStrings.Default`. Run IN-PROGRESS first poll (running 14:31) — correctly did NOT verify-bundle-mid-flight (anti#3); pre-deploy baseline captured BEFORE poll-loop: admin `PxiZQkaw` (S74 #300 live) + user `B36hGoKd` (S74 #300 live) — both == spec baseline. Polled iter4 status=success (started ~14:31 → success 14:33:21 ≈2.5m — fastest streak, FE-only no big BE build). CI gate (both proj pre-deploy ⟹ status=success ⟹ test gate **286** baseline (45D+241I; FE-only ⟹ 0 BE call-site risk) passed; `conclusion` empty — `tasks` endpoint terminal=`status:success` doesn't populate `conclusion`, trust success). **★ BUNDLE BOTH ROTATE (the change-point — FE-BOTH-app, both PeDetailTabs changed ⟹ both bundles MUST rotate; verified AFTER status=success +re-confirm STABLE 2nd-fetch identical no transient — anti#3): admin ROTATE `PxiZQkaw→I1fpLeYw`** ✓ (Link-hồ-sơ auto-detect shipped) **+ user ROTATE `B36hGoKd→DrQYkzh0`** ✓ (same component shipped). BOTH required per spec → BOTH did (mirror S74 pure-FE-both-app pattern — both same-SHA256 file ⟹ both rotate; frozen sibling here = ship-fail flag). Smoke **3×200**: api `/health/ready` + admin root + eoffice root. **NO migration** — prod `__EFMigrationsHistory` top = `20260616035929_AddHoSoLinkToPurchaseEvaluation` (Mig 52) == repo HEAD GIỮ NGUYÊN ✓ (`git diff --name-only 6df1b2d~1 6df1b2d -- '*Migrations*'` = EMPTY; FE cannot alter schema → top did NOT advance; prev-2 = `AddDepartmentParentId` Mig51 + `ReplaceBudgetModuleWithPeWorkItemBudgets` Mig50 chain intact). **sys.tables=88 verified** (sqlcmd COUNT excl mighist — unchanged, no schema touch). 0 regression. NO prod-data mutation (read-only curls + sqlcmd SELECT-only). Visual "hyperlink bấm-mở / nút Copy hiện đúng" NOT verified (anh xem mắt) — only ship+rotate+health+mig-unchanged+tables88; SHA256-identical-between-2-apps is a SOURCE-claim (git), not runtime DOM-equality (auto-detect http-vs-O:\ branching is component logic, not provable by curling bundle). **LESSON: pure-FE-BOTH-app additive verify (5th consecutive deploy this session, both apps same component SHA256-identical) = both bundles MUST ROTATE (≠ asymmetric one-app where sibling frozen; ≠ BE-only where both frozen). Migration top + sys.tables MUST stay = prev (FE-only). No BE call-site/DTO/endpoint smoke (no API surface — render-only change). TOOLING (re-confirmed S74): Bash tool EATS inline `$var`/`$env:` in `powershell -Command` → write PowerShell to `.ps1` + run `-File`; parse Gitea `tasks` via `Invoke-RestMethod`+native object (match `head_sha -eq sha`, `limit=N` ignored); SSH→sqlcmd base64 `EncodedCommand` (UTF-16LE, no `$` in B64 passes bash clean), CLIXML progress-stream stdout harmless; read DB pw from prod appsettings when PROD_DB_PW empty (path `C:\inetpub\solution-erp\api`). NEVER fixed code (READ-only).** Tag `[s75, run301, pass, fe-both-app-pe-link-hoso-autodetect, fe-both-2files-sha256-identical, bundle-BOTH-rotate-I1fpLeYw-DrQYkzh0, no-mig-top-stays-mig52, tables88-verified, deploy5of5, fastest-2.5m, ps1-file-not-inline-dollar, no-regression, test286]`. +- _(S75 Run #301 sha=`6df1b2d` PASS ~2.5m [FE-both-app PE Link-hồ-sơ auto-detect render, bundle-BOTH-rotate I1fpLeYw/DrQYkzh0, no-mig-mig52, tables88, test286, fastest-streak] → FIFO-trimmed, full verbatim git + `archive/2026-06.md`)_ - _(S74 Run #300 sha=`91aaf05` PASS [FE-both-app list-redesign flex-row, bundle-BOTH-rotate PxiZQkaw/B36hGoKd, user-unfroze-from-s71-streak, no-mig-mig52, tables88, test286, TOOLING: ps1-file-not-inline-dollar + ssh-encodedcommand-base64] → FIFO-trimmed, full verbatim git + `archive/2026-06.md`)_ - **2026-06-16 S73 Run #299 (id413) sha=`bcd619d` PASS ~3m (TESTS-ONLY BE: +3 files `tests/SolutionErp.Infrastructure.Tests/Application/{DepartmentTreeTests,PeHoSoLinkTests,HrmProfilePermissionSeedTests}.cs` = +23→286 total (45D+241I). NO prod code, NO FE, NO migration; deploy 3/3 session after #297/#298 both success):** `.cs` not in paths-ignore → full pipeline RAN. Run IN-PROGRESS first poll (running 14:06) → polled iter4 status=`success` (~14:09 ≈3m); `conclusion` empty (terminal=success, trust). **CI test gate = the focus: gate runs BOTH test projects BEFORE build ⟹ status=success ⟹ 23 new tests passed on CI (not just local 286).** Could NOT extract exact CI count — Gitea `runs/413/logs`+`jobs` endpoints all **404 anon** (logs web-UI-only without auth token; GITEA_TOKEN empty). Inferred 286 from success (gate-passes-pre-build invariant) — NOT log-confirmed numerically. **★ BUNDLE BOTH-FROZEN (BE-only ⟹ neither app may rotate), verified AFTER status=success +2nd-fetch STABLE no transient (anti#3): admin `xkSz9BfE` FROZEN ✓ + user `BumgrwCJ` FROZEN ✓** (==pre-deploy baseline both; rotate-on-BE-only = anomaly→none). Smoke **4×200**: api `/health/ready`+`/health/live`+admin root+eoffice root. **NO migration** — prod `__EFMigrationsHistory` top=`AddHoSoLinkToPurchaseEvaluation`(Mig52)==repo HEAD GIỮ NGUYÊN ✓ (prev-2 Mig51 `AddDepartmentParentId`+Mig50 `ReplaceBudgetModule…` chain intact; commit 0 mig files). **sys.tables=88** ✓ (sqlcmd excl mighist, unchanged). 0 regression, NO prod-data mutation (read curls+SELECT-only). **TOOLING LESSON (supersedes S72 #298): `python3`/`py -3` on this box = BROKEN ZKBioTime embed (`AssertionError: SRE module mismatch` on `import re`/`json`) — version banner works but stdlib import dies. USE PowerShell `Invoke-RestMethod`+native object access (NOT ConvertFrom-Json on cached temp file — temp got stale snapshot once) to parse Gitea tasks; `limit=N` param IGNORED (returns full 299, match by id anyway). SSH→sqlcmd: nested bash→ssh→PS ate `$var` (S42) → use `iconv UTF-16LE|base64`→`powershell -EncodedCommand`; CLIXML progress-stream noise on stdout is harmless, data lines clean. Read DB pw from prod `appsettings.Production.json`→`ConnectionStrings.Default` when PROD_DB_PW empty (path `C:\inetpub\solution-erp\api`). NEVER fixed code (READ-only).** Tag `[s73, run299, pass, tests-only-be, +23-test-286-CI-gate-inferred-not-logcount, bundle-both-frozen-xkSz9BfE-BumgrwCJ, no-mig-top-mig52, tables88, deploy3of3, python3-broken-use-powershell-IRM, no-regression]`. - **2026-06-16 S72 Run #298 (run_number 298, id412) sha=`292d64d` PASS ~4m20s (FE-Admin MIRROR "Hồ sơ Nhân sự" master-detail từ fe-user: overwrite EmployeesListPage.tsx cũ 1200→1602 dòng SHA256-IDENTICAL với fe-user + index.css +4 accent palette teal/amberx/violet/greenx + utility `.icon-chip`/`.app-gradient-brand`/`.card-accent`/`.stat-value` — FE-ADMIN-ONLY 2 files, NO BE, NO migration, NO fe-user; ASYMMETRIC NGƯỢC deploy-1 S71 #297 (đó fe-user-rotate/admin-frozen, đây admin-rotate/user-frozen)):** Deploy 2/2 trong session (deploy-1 = `ab4e681` #297 PASS fe-user `BumgrwCJ`; #297=success TRƯỚC push này nên KHÔNG bị cancel — confirmed in tasks list). Push 2 files `fe-admin/src/index.css`(+81) + `fe-admin/src/pages/hrm/EmployeesListPage.tsx`(1020+/537−, 1476-line overwrite). `.tsx`+`.css` → not in paths-ignore → full pipeline RAN. GITEA_TOKEN+PROD_DB_PW empty → anon Gitea API + DB pw từ prod `appsettings.Production.json`→`ConnectionStrings.Default` (`buKL3...buKL3TGBkD0wDDbYVw65QeX9`). Run IN-PROGRESS first poll (running 13:52) — correctly did NOT verify-bundle-mid-flight (anti#3); pre-deploy baseline snapshot captured BEFORE poll: admin `BDwV5d0X` (S68 frozen-since, must rotate) + user `BumgrwCJ` (S71 deploy-1 live, must stay frozen). ⚠️ POLL-PARSER BUG: my `tr ','|grep -A4 '"id":412'` returned empty status ×10 iter (false negative — grep -A on tr-newlined JSON misanchors) → re-queried with `python3 json.load` → status=`success` (started 13:51:27 → updated 13:55:47 ≈4m20s). LESSON-tooling: parse Gitea tasks JSON via python3 NOT tr/grep -A (multi-field-per-object splits wrong); always cross-check with a clean parser before trusting empty-status. CI gate (both proj pre-deploy ⟹ status=success ⟹ test gate **263** baseline (45D+218I; FE-only ⟹ 0 BE call-site risk) passed; `conclusion` empty — trust success). **★ BUNDLE ASYMMETRIC NGƯỢC-deploy-1 (the change-point — FE-one-app verify) ALL PASS, verified AFTER status=success +re-confirm STABLE 2nd-fetch identical no transient (anti#3): admin ROTATE `BDwV5d0X→xkSz9BfE`** ✓ (EmployeesListPage mirror + accent index.css shipped — admin un-froze from S68's BDwV5d0X frozen-streak S68→S71, now rotated correctly cho fe-admin change) **+ user FROZEN `BumgrwCJ`==deploy-1-baseline** ✓ (fe-user 0 files touched deploy-2 → MUST NOT rotate = correct; rotate-when-untouched = anomaly→flag). Health api live+ready **200/200** (real endpoints `/health/ready`+`/health/live`, NOT `/health`) + admin/eoffice root 200. **NO migration** — prod `__EFMigrationsHistory` top = `20260616035929_AddHoSoLinkToPurchaseEvaluation` (Mig 52) == repo HEAD GIỮ NGUYÊN ✓ (prev-2 = `AddDepartmentParentId` Mig51 + `ReplaceBudgetModuleWithPeWorkItemBudgets` Mig50 chain intact; commit 0 mig files — FE cannot alter schema → top did NOT advance). **sys.tables=88 verified** (sqlcmd COUNT excl mighist — unchanged, no schema touch). 0 regression. NO prod-data mutation (read-only curls + sqlcmd SELECT-only). Visual "mirror identical / chữ xanh đậm brand-800 / accent đẹp" NOT verified (anh xem mắt) — only ship+rotate+health+mig-unchanged+tables88; SHA256-identical-with-fe-user is a SOURCE-claim (git diff), not runtime-verified by me (can't compare rendered DOM cross-app). **LESSON: deploy-2 of same-session asymmetric-NGƯỢC = (a) verify prior deploy (#297) NOT cancelled by this push (it was success first — confirmed in tasks); (b) status=success ⟹ deploy ran; (c) THIS-app bundle ROTATE + OTHER-app FROZEN — direction FLIPS vs deploy-1 (deploy-1 fe-user-rotate/admin-frozen → deploy-2 admin-rotate/user-frozen); admin un-froze from multi-session frozen-streak = EXPECTED when fe-admin finally changes (frozen-streak break is normal, not anomaly); (d) migration top + sys.tables MUST stay = prev (FE-only). Re-query Gitea JSON with python3 when tr/grep parse yields empty — parser bug ≠ run-stuck. No BE call-site/DTO/endpoint smoke (no API surface). SHA256-source-identical = git-level claim, runtime-mirror-equality not provable by curl. NEVER fixed code (READ-only).** Tag `[s72, run298, pass, fe-admin-mirror-hoso-from-feuser, fe-admin-only-2files, bundle-asymmetric-NGUOC-admin-rotate-user-frozen, admin-unfroze-from-s68-streak, no-mig-top-stays-mig52, tables88-verified, deploy2of2-prior297-not-cancelled, poll-parser-bug-use-python3, no-regression, test263]`. diff --git a/.claude/agent-memory/frontend-designer/MEMORY.md b/.claude/agent-memory/frontend-designer/MEMORY.md index 82588ca..84b6d2e 100644 --- a/.claude/agent-memory/frontend-designer/MEMORY.md +++ b/.claude/agent-memory/frontend-designer/MEMORY.md @@ -44,6 +44,7 @@ ## Activity log - **S69 (2026-06-17) OfficeDashboardPage.tsx fe-user — E-Office landing, PURO HomePage, COMPOSES 3 shared widgets:** built `pages/office/OfficeDashboardPage.tsx` (~400 LOC) over EXISTING data hooks of 4 modules. Read-first: 3 ui widgets (exact prop sigs) + 4 source pages (ProposalsListPage/WorkflowAppsListPage/ItTicketsPage/MeetingCalendarPage) to harvest queryKey+endpoint + types + App.tsx routes. **Reused hooks verbatim** → shared TanStack cache, ZERO new API/BE: `GET /proposals` (+inboxOnly query for needs-my-action) · `/leave|ot|travel-requests` (merged, countByStatus client-side) · `/it-tickets` · `/meeting-bookings` (today window). Layout = PageHeader(brand) + `lg:grid-cols-3` [LEFT col-span-2 = 4 WidgetCards w/ KpiCard filter-chip bodies | RIGHT col-span-1 = "Công việc của tôi" hero+MetricRows + "Thao tác nhanh" 3 buttons], 1-col ` + 2 `useMemo` (counts + visibleItems) DERIVED over already-fetched `items`; **KHÔNG touch query/endpoint/queryKey/navigation** (page chỉ fetch `page:1` như cũ, không có filter-state sẵn nên thêm view-layer = thuần presentation; empty-state phân biệt "chưa có data" vs "không có đơn ở trạng thái này"). **DETAIL:** ui/PageHeader teal + 4 section dùng local `Card`(accent-rail pseudo `before:bg-{x}-500` + icon-chip) + `Field`(label uppercase `text-{x}-700`, value `text-brand-800`) — copy idiom HRM EmployeesListPage (KHÔNG import HRM, helper local riêng). Accent gán: Thông tin=teal · Số dư phép=greenx · Quy trình=violet · Ý kiến=brand. Status badge giữ `WORKFLOW_APP_STATUS_BADGE`. Drop raw "⚠️" emoji trong over-budget banner→text thuần (anti-slop FD3). **ALL data logic VERBATIM** (grep-verified): 2 query `[endpoint,id]`+`['approval-workflows-v2',applicableType]` (key/endpoint/`enabled` y nguyên) · 3 mutation pinWorkflow(PUT /workflow)/submit(POST /submit)/action(POST /{k}) body+onSuccess+invalidate identical · 3 state + flags isDraft/isInWorkflow/hasWorkflow + mọi onClick/nav target bất biến. **gotcha Tailwind-v4 stop:** dùng CHỈ -50/-500/-700 cho teal/violet/amberx/greenx (no -800) — `border-l-greenx-500`/`bg-amberx-50`/`text-amberx-700` đều stop tồn tại (index.css verified). **Self-caught:** `SendHorizonal` (KpiCard "Đã gửi duyệt" icon) export-aggregation-line trong lucide d.ts → đổi `Send` (proven-safe export) tránh alias-risk. Self-review: mọi import resolve, 0 unused local (noUnusedLocals strict) — `Info`/`Wallet`/`GitBranch`/`MessageSquareText` đều dùng. **KHÔNG run npm build** (em main builds central, 7-agent parallel interference) + KHÔNG modify ui/index.css (other agents edit). FD2 authed-screenshot SKIP (ProtectedRoute + rig gotcha #3 — verify via deploy). fe-admin NOT mirrored. Tag [s69, eoffice-donutu-reskin, puro-hrm-visual, consume-ui-pageheader-kpicard, client-side-filter-view, logic-verbatim, no-build-parallel-fanout, lucide-alias-dodge]. - **S66 (2026-06-16) HRM Hồ sơ Nhân sự fe-user REFINE từ eoffice LIVE (3 việc) — layout 3-cột→2-cột + tô màu detail:** anh góp ý sau khi xem prod. **Việc 1 (layout):** 3-cột-ngang `[tree 244 | list 352 | detail 1fr]` → **2-cột** `lg:grid-cols-[22rem_1fr] xl:[24rem_1fr]`: CỘT TRÁI = `
` ôm tree (TRÊN, `lg:max-h-[44%] lg:shrink-0`, overflow-auto) + list+filter (DƯỚI, `flex-1`, overflow-auto) — mỗi panel cuộn độc lập; CỘT PHẢI = detail (flex-1, rộng hơn nhiều, đỡ chật). ` → [Org tree | List | Detail 5-tab]. **Strategy chống truncation #53 = ONE atomic `Write` (cả file)** thay piecemeal Edit (atomic Write either fully-lands or errors, KHÔNG half-break) → emit change-list TRƯỚC build → DID BOTH Part A (avatar header+5 tab+section→tab redistribution) + Part B (org tree panel) trong 1 pass, không phải defer B. Org tree consume `/departments/tree` verified BE-side (DepartmentFeatures.cs DepartmentTreeNodeDto, controller `[HttpGet("tree")]`, class-Authorize only). Foundation màu mới DÙNG: `.app-gradient-brand` header / `.icon-chip` / accent palette teal/violet/amberx/greenx (avatar tones) — brand #1F7DC1 + Be Vietnam Pro KEPT. **5 satellite CRUD + 16 api endpoint + query keys preserved VERBATIM** (grep-verified: 16 api.post/put/delete identical payload shape, 5 form fns intact). `npm run build` (tsc -b strict + vite) **PASS 0 TS err, 6.13s**. 1 self-caught bug: typo garbage token `网络Placeholder` trong lucide import (mojibake autocomplete) → removed, all 21 icons valid (node-checked). FD2 authed-screenshot SKIPPED per explicit task instruction + gotcha #3 (rig blocks authed; anh xem qua deploy) — did static structural verify instead (grep endpoint/key preservation). fe-admin NOT touched (mirror = separate pass), no commit. Tag [s65, hrm-3panel, namgroup-ref, atomic-write-antitrunc, crud-preserved, build-pass]. - **S58 (2026-06-11) fe-user redesign theo UI/UX guide AI_INFRA canonical — KEEP brand [em main proxy — truncated #53 giữa FD2 screenshot, 2nd consecutive]:** Mirror design-system fe-admin S55 → 14 file fe-user (index.css heading-ladder+.label-eyebrow / 6 ui primitives — Button gần SHA-identical fe-admin chỉ khác comment / 6 shell DataTable+RowActions-additive·Layout-brand-left-rail·TopBar·PageHeader·PhaseBadge-ring·EmptyState / LoginPage polish). Rubric mới = guide 13 mục `D:\Dropbox\CONG_VIEC\AI_INFRA\docs\reference\ui-ux-design-guide.md` (density 14px/h32-34/radius-8/thead-sticky/action-luôn-hiện/no-font-bold). BRAND KEPT: #1F7DC1 + Be Vietnam Pro + slate (guide cho plug hue riêng). Chết NGAY TRƯỚC with_server.py screenshot /login → em main recover: build ×2 PASS 0 TS + diff-review key-stability từng file + ship `e959f72`; authed visual qua deploy prod (rig-gotcha #3 standing). LESSON: 2 lần liên tiếp truncate ở CÙNG điểm (sau khi sửa xong, lúc bắt đầu FD2 rig) → lần sau EMIT file-list verdict TRƯỚC khi vào screenshot loop. Tag [s58, fe-user-redesign, guide-aiinfra, keep-brand, truncated-53-proxy]. diff --git a/.claude/agent-memory/reviewer/MEMORY.md b/.claude/agent-memory/reviewer/MEMORY.md index 0feeb79..06e877b 100644 --- a/.claude/agent-memory/reviewer/MEMORY.md +++ b/.claude/agent-memory/reviewer/MEMORY.md @@ -61,6 +61,7 @@ Adversarial pre-commit reviewer SOLUTION_ERP. Read-only verify + live curl prod ## 📅 Recent activity (FIFO — older → archive/git) +- **2026-06-17 (S69 Văn phòng số RE-SKIN static logic-preservation — PASS, 0 blocker):** 10 pages presentation-only re-skin → PURO PageHeader/KpiCard + Hồ sơ-NS idiom (9 fe-user office + 1 fe-admin AttendanceReport). NOT built yet, fe-admin not mirrored (em main next). **Strongest proof = exact API/queryKey diff OLD-vs-NEW byte-identical ALL 8 fe-user pages** (grep `api\.(get|post|put|delete)` + `queryKey:[...]` sorted -u, zero delta): proposals POST /submit + /{kind} · workflow-apps POST /{k}+/submit+PUT /workflow · meeting-bookings POST/DELETE+invalidate · it-tickets PUT /{id}/assign · directory/departments/attendance-report/excel-blob all UNCHANGED. Mutation side-effects (onSuccess/onError/invalidateQueries/setActionDialog/setComment/navigate) 1:1 (line-shift only). ProposalCreate validation `!title.trim()` throw + required + submit-disabled intact. AttendanceReport exportExcel blob (createObjectURL→a.download→click→revoke) intact. **Cat2 orphans CLEAN:** 0 unused import — flagged Users(=UsersIcon alias) + FormEvent/ReactNode (React.* namespace not named-import) + Accent(comment word) all FALSE-alarm verified. **Cat3 shared-comp contract:** PageHeader{eyebrow,title,subtitle,icon,accent,actions} + KpiCard{label,value,icon,accent,active,onClick} props all match real sig; KpiCard onClick wired to REAL filter state (ItTickets `setFilter`/WorkflowAppsList `setStatusFilter`/ProposalsList — driving actual client `.filter()`), InternalDirectory 2 KpiCards INTENTIONALLY inert (no onClick=presentational counts, matches comp design — NOT dummy). **Shared comps + index.css NOT modified** (git status -- ui/ + *.css EMPTY; sha256 identical fe-user==fe-admin per ls). **Cat4 color-trap CLEAN:** grep added lines for `(teal|violet|amberx|greenx)-(200|300|400|800|900)` = ZERO; index.css confirms accents ship only 50/100/500/600/700 (brand has full 50-900 so brand-800 valid); gotcha #66 — 0 gradient/dark-bg headings added (all headers on light surface use accent-ink text-brand-800/{accent}-700 via PageHeader). **Cat1 mock-markers:** 0 //Mock/alert/TODO-wire. **Client-side filter additions** (ItTickets filter/breached, WorkflowAppsList statusFilter useMemo) = presentation views over fetched items, NO new query/endpoint. **2 MINOR (non-block):** (a) ProposalDetail status badge now renders TWICE — PageHeader actions slot + existing status-row (cosmetic dup, both presentation); (b) it-tickets/workflow-apps client-filter is view-only over a `pageSize:100/50` first-page fetch (pre-existing pagination limit, re-skin doesn't worsen). **Learned:** for pure re-skin, the decisive logic-preservation proof is `grep api-call + queryKey sorted -u` OLD-vs-NEW byte-equality across every page — faster + more rigorous than reading each hunk; orphan-import heuristic (body-occ<=1) flags `X as Y` aliases + `React.X` namespace + comment-words as false-positives, always grep the actual usage line before flagging build-break. **surprise:** custom accent palettes (amberx/greenx/teal/violet) deliberately ship NO -800 stop so headings MUST use -700 (brand is the only -800-bearing accent) — a -800 on a non-brand accent = silent no-class Tailwind v4, the re-skin respected this everywhere. Verdict PASS — safe for em main to build+mirror. Tag [s69, office-reskin, presentation-only, api-querykey-byte-equal, color-trap-clean, kpicard-inert-vs-filter, gotcha66-clean]. - **2026-06-16 (S65 PE mục E HoSoLink review — em-main PROXY, PE-Workflow reviewer-stage died-empty):** Review mục-E hyperlink render + HoSoLink BE wiring (`5a0aaa4`). Reviewer-stage trong Workflow `pe-hoso-link-rename-pro` return RỖNG → em main self-gate evidence: Detail DTO `hoSoLink` present + `null` backward-compat phiếu thật (Run #293 GET 200); Create/Update +trailing-optional `HoSoLink=null` KHÔNG vỡ call-site (grep 0 manual ctor — KHÁC CreateDepartmentCommand #291 CS7036 vì positional-required vs trailing-optional); mirror fe-user==fe-admin SHA256 IDENTICAL (PeDetailTabs+PeWorkspaceCreateView); hyperlink `` no reverse-tabnabbing; rename "Dự trù PRO"→"Ngân sách PRO" CHỈ display (giữ "Ghi chú từ PRO" + field-code). LEARNED: hyperlink free-text = no server-side XSS (render-as-href client-only); absolute-set Update (null=clear) chủ đích. SURPRISE: reviewer-stage chết-rỗng trong fan-out = lý do verify-heavy task vẫn cần em-main self-gate dù có Workflow (verdict `feedback_workflow_fanout_reliability`). Tag `[s65, pe-section-e-review, em-main-proxy-self-gate, hosolink-backward-compat, workflow-fanout]`. - **2026-06-16 (S65 public Hồ sơ NS read for all roles — static pre-commit, PASS, 0 blocker, gotcha #44 family CLEAN):** 1-file change DbInitializer.cs (+66, call-site :2046 SAU revoke :2040 + new `SeedAllRolesHrmProfileReadPermissionsAsync` :2203). Prod NOT deployed (static review, build PASS đã claim). **7 verify ALL PASS:** (1) **Ordering** — grant gọi SAU `RevokeTemporarilyHiddenModulesAsync` trong SeedAsync → grant thắng (git diff confirms call sits immediately after revoke). (2) **Upgrade path prod-critical** — method MUTATES existing row `if(!row.CanRead){row.CanRead=true;upgraded++}` (EF change-tracked → SaveChanges persists); NOT skip-existing-noop. Correctly fixes S58-class bug (revoke set CanRead=false on prod rows → upgrade flips true). (3) **Scope precise** — `hrmKeys = new[]{MenuKeys.Hrm, MenuKeys.HrmHoSo}` EXACTLY 2; NO Hrm_Dashboard/Hrm_Config*/Off*/Personal. `Hrm` is NOT one of 4 inherit-roots (Contracts/Workflows/PE/PeWorkflows in GetMyMenuTree:56-59) so granting Hrm root does NOT cascade to Dashboard/Config children → they keep own false flags → filtered out by `HasAccess(n)=n.CanRead||Children.Any(HasAccess)`. Menu shows Hrm root → Hồ sơ NS leaf ONLY (HrmHoSo ParentKey=Hrm:1806, Dashboard sibling ParentKey=Hrm:1850 stays hidden). (4) **Read-only** — add-path CanCreate/Update/Delete=false; upgrade-path touches ONLY CanRead. (5) **No regression** — Admin bypass at MenuPermissionHandler:27 untouched; revoke unchanged; Off/Personal/Dashboard/Config stay hidden after full seed. (6) **Idempotent** — 2nd run: row.CanRead already true → `if(!row.CanRead)` false → 0 change. (7) **No non-Admin write path** — `MenuPermissionHandler` Read→AnyAsync(CanRead) is what GET checks; all 19 EmployeesController write actions (main+5 satellite) require Hrm_HoSo.Create/Update/Delete which grant leaves false → 403. **surprise/monitor-note (NOT a defect, NOT introduced by this change):** HrDashboardController/HrmConfigsController/Attendances/LeaveBalances carry ONLY class-level `[Authorize]` (any-auth, NO per-action Hrm_*.Read policy) — so their data was already reachable by direct URL pre+post S65 (menu-hide ≠ API-lock; S58 revoke comment DbInit:2153-2155 explicitly acknowledged this). S65 does NOT widen it (only touches perm matrix rows Hrm+Hrm_HoSo + menu filter). cicd-monitor must NOT assume "Dashboard hidden in menu"=="dashboard data unreachable". Spec comment said "6 catalog Hrm_Config*" but there are 6 config leaves + Hrm_Config subgroup = 7 keys — cosmetic count, all stay hidden, not a code bug. **Learned:** for menu-key read-grant, verify the granted root is NOT an inherit-root (else cascade leaks siblings) + trace HasAccess filter + confirm leaf ParentKey chains to the visible root; upgrade-path correctness = grep that method MUTATES row (not skip-existing) when a prior revoke pre-set the flag false on prod. Verdict PASS — safe commit. Tag [s65, public-hrm-hoso, upgrade-path-correct, inherit-root-no-cascade, gotcha44-family-clean, menu-only-not-api-lock-monitor-note]. - **2026-06-12 (S60 đợt1 PE submit-guard + drafter-bypass gate — KHÔNG DELIVER, die mid-run, on-behalf em main ghi hộ, H2-proposed):** Task: review `37122f0` cross-stack (BE TransitionAsync submit-guard đủ-4-thông-tin mục 3 + bypass người-soạn-trong-chuỗi V2 BƯỚC-ĐẦU-only + FE PeDetailTabs ×2 + 14 PeSubmitGuardAndBypassTests 240→254). Die mid-run #53-class (commit body tự khai "Reviewer die mid-run → em main self-gate evidence-checklist PASS 0 blocker") → ship Run #283 PASS prod-verified, bundle rotate both. LEARNED: self-gate em main đứng vững lần 2 (sau S57bis) — checklist deterministic (test gate + diff scope + prod smoke 401/404-control) đủ cho PE refinement cross-stack. SURPRISE: die lần 3 trong 2 ngày (S57bis die-0-byte ×2 + S60 mid-run) DÙ promote-tier inherit Fable 5 → model-tier KHÔNG phải nguyên nhân die (nghi resume-kill/harness class) — trend data cho Harness-4. Tag `[s60, die-mid-run-3rd, self-gate, on-behalf]`. diff --git a/fe-admin/src/pages/office/AttendanceReportPage.tsx b/fe-admin/src/pages/office/AttendanceReportPage.tsx index 1749be0..3ee582f 100644 --- a/fe-admin/src/pages/office/AttendanceReportPage.tsx +++ b/fe-admin/src/pages/office/AttendanceReportPage.tsx @@ -2,11 +2,18 @@ // fe-admin ONLY: endpoint /attendances/report* là [Authorize(Roles=Admin)] → fe-user KHÔNG có page này. // Filter Năm/Tháng/Phòng ban → TanStack Query → Table + footer Tổng. Xuất Excel qua api.get responseType:'blob' // (api instance đã inject JWT qua interceptor + hỗ trợ refresh-token retry — chuẩn hơn raw fetch). +// +// Re-skin S69 (2026-06-17): áp PURO layout + visual language Hồ sơ Nhân sự — CHỈ trình bày, +// KHÔNG đổi logic. ui/PageHeader (icon-chip + actions) · KpiCard hàng tổng hợp · .card-accent +// cho bộ lọc + bảng · .label-eyebrow nhãn lọc · text-brand-800 cho số quan trọng. Mọi query / +// mutation / queryKey / endpoint / handler giữ NGUYÊN (attendance-report, /attendances/report, +// /attendances/report/excel, departments-all-attendance-report). import { useState } from 'react' import { useMutation, useQuery } from '@tanstack/react-query' -import { Download, ClipboardList } from 'lucide-react' +import { Download, ClipboardList, CalendarCheck, Users, Clock, Hourglass } from 'lucide-react' import { toast } from 'sonner' -import { PageHeader } from '@/components/PageHeader' +import { PageHeader } from '@/components/ui/PageHeader' +import { KpiCard } from '@/components/ui/KpiCard' import { Button } from '@/components/ui/Button' import { Input } from '@/components/ui/Input' import { Select } from '@/components/ui/Select' @@ -68,12 +75,17 @@ export function AttendanceReportPage() { }) const rows = report.data?.rows ?? [] + // Tổng hợp tô màu KpiCard — thuần dẫn xuất từ dữ liệu đã fetch (KHÔNG gọi API mới). + const totalDaysPresent = rows.reduce((s, r) => s + r.daysPresent, 0) return (
} + accent="brand" actions={ - )} - - {t.slaDueAt && ( - - {t.slaBreached ? 'Quá hạn SLA' : `SLA ${formatSlaDue(t.slaDueAt)}`} - - )} -
-
- ))} - - + // Cột nào hiển thị theo chip lọc (presentation). 'all' = mọi cột; statusKey = 1 cột; + // 'breached' = mọi cột nhưng chỉ giữ ticket quá hạn (cardMatch lọc bên trong). + const ORDER = [1, 2, 3, 5, 4] + const visibleColumns = + filter === 'all' || filter === 'breached' + ? ORDER + : ORDER.filter(s => s === filter) + const cardMatch = (t: ItTicketDto) => (filter === 'breached' ? t.slaBreached : true) + + return ( +
+ } + accent="violet" + /> + + {/* ── KpiCard filter-row (PURO) — thay status tabs. value = count, onClick = + set filter hiển thị. Bấm lại chip đang chọn → bỏ lọc (về 'all'). ── */} +
+ {STATUS_CHIPS.map(({ key, icon: Icon, accent }) => ( + } + accent={accent} + active={filter === key} + onClick={() => setFilter(prev => (prev === key ? 'all' : key))} + /> ))} + } + accent="amberx" + active={filter === 'breached'} + onClick={() => setFilter(prev => (prev === 'breached' ? 'all' : 'breached'))} + /> +
+ + {/* ── Kanban columns — Hồ sơ NS chrome: card-accent rail + header tinted ── */} +
+ {visibleColumns.map((statusKey) => { + const accent = COLUMN_ACCENT[statusKey] + const cards = grouped[statusKey].filter(cardMatch) + return ( +
+
+

+ {IT_TICKET_STATUS_LABELS[statusKey]} +

+ + {cards.length} + +
+
+ {list.isLoading &&
Đang tải...
} + {!list.isLoading && cards.length === 0 && ( +
Trống
+ )} + {cards.map((t) => ( +
+
+ {t.maTicket ?? '—'} + + {IT_TICKET_PRIORITY_LABELS[t.priority]} + +
+
{t.title}
+
+ {IT_TICKET_CATEGORY_LABELS[t.category]} · {t.requesterFullName} +
+
+ + + + {t.assignedToFullName ?? Chưa giao} + + {/* Reassign Admin OR tổ IT (BE capability canReassign). Nút nhỏ cạnh người xử lý → mở Dialog gán lại. */} + {canReassign && ( + + )} + + {t.slaDueAt && ( + + {t.slaBreached ? 'Quá hạn SLA' : `SLA ${formatSlaDue(t.slaDueAt)}`} + + )} +
+
+ ))} +
+
+ ) + })}
{!list.isLoading && items.length === 0 && ( -
- - Chưa có ticket nào. +
+ + + +

Chưa có ticket nào.

)} @@ -162,18 +266,18 @@ export function ItTicketsPage() { > {target && (
-
- Ticket {target.maTicket ?? '—'} · {target.title} +
+ Ticket {target.maTicket ?? '—'} · {target.title}
- + - {staffQ.isLoading &&
Đang tải danh sách…
} + {staffQ.isLoading &&
Đang tải danh sách…
}
)} diff --git a/fe-admin/src/pages/office/MeetingCalendarPage.tsx b/fe-admin/src/pages/office/MeetingCalendarPage.tsx index b6b690f..bf2a267 100644 --- a/fe-admin/src/pages/office/MeetingCalendarPage.tsx +++ b/fe-admin/src/pages/office/MeetingCalendarPage.tsx @@ -6,9 +6,9 @@ // Pattern 16-bis 4-place mirror foundation (7× cumulative). import { useMemo, useState, type FormEvent } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import { ChevronLeft, ChevronRight, Calendar as CalendarIcon, Clock, MapPin, Plus, Trash2, Users as UsersIcon, X } from 'lucide-react' +import { ChevronLeft, ChevronRight, CalendarDays, Clock, MapPin, Plus, Trash2, Users as UsersIcon, X } from 'lucide-react' import { toast } from 'sonner' -import { PageHeader } from '@/components/PageHeader' +import { PageHeader } from '@/components/ui/PageHeader' import { Button } from '@/components/ui/Button' import { Input } from '@/components/ui/Input' import { Label } from '@/components/ui/Label' @@ -259,13 +259,11 @@ export function MeetingCalendarPage() { return (
- - Đặt phòng họp - - } - description={bookings.isLoading ? 'Đang tải…' : `${totalThisWeek} booking tuần này`} + eyebrow="Văn phòng số" + title="Lịch phòng họp" + subtitle={bookings.isLoading ? 'Đang tải…' : `${totalThisWeek} booking tuần này`} + icon={} + accent="amberx" actions={
{/* Calendar grid */} -
+
{/* Header row: 7 days */}
@@ -526,52 +527,69 @@ export function MeetingCalendarPage() { > {detailBooking && (
-
-

{detailBooking.title}

-
- - {statusLabel(detailBooking.status)} - - - Người tạo: {detailBooking.bookedByFullName} - +
+ + + +
+

{detailBooking.title}

+
+ + {statusLabel(detailBooking.status)} + + + Người tạo: {detailBooking.bookedByFullName} + +
-
-
- - {detailBooking.roomCode} — {detailBooking.roomName} +
+
+
+ + Phòng họp +
+
+ {detailBooking.roomCode} — {detailBooking.roomName} +
-
- - +
+
+ + Thời gian +
+
{new Date(detailBooking.startAt).toLocaleString('vi-VN', { dateStyle: 'short', timeStyle: 'short' })} {' – '} {new Date(detailBooking.endAt).toLocaleString('vi-VN', { dateStyle: 'short', timeStyle: 'short' })} - +
{detailBooking.description && (
- +
Mô tả

{detailBooking.description}

)} {detailBooking.note && (
- +
Ghi chú

{detailBooking.note}

)}
-