# TEK Access Control — Bộ thiết kế giao diện (AppUI)

Bản thiết kế HTML tĩnh cho phần mềm **TEK Access Control** (kiểm soát ra vào + chấm công trên controller **HID Aero X1100A / X100 / X200 / X300**), khách hàng **Jointech**.
Phong cách kế thừa design system TEK của Car Parking (`thamkhao/AppUI`), không có Client App.

- Brief đầy đủ: `output/_notes/07-ui-brief.md` · Ghi chú mẫu: `output/_notes/04-thamkhao-templates.md` mục 7
- Đích chuyển đổi: **ASP.NET Core 8 + Razor + Bootstrap 5**, MySQL 8.4, SignalR cho web realtime, gRPC Device Service ↔ Core, chạy trên Windows Server.

---

## 1. Cách mở

| Cách | Lệnh |
|---|---|
| Mở thẳng | bấm đúp `index.html` (file://) — mọi đường dẫn là tương đối, không cần Internet |
| Máy chủ tĩnh | `python -m http.server 8778` trong thư mục `AppUI` → http://localhost:8778 |
| Claude Code | `.claude/launch.json` cấu hình `acui` (port 8778) |

Không dùng CDN — toàn bộ thư viện nằm trong `shared/vendor/` (Bootstrap 5.2.3 CSS + 5.3 bundle, FontAwesome Free 7, DataTables 2.3.4, Chart.js, jQuery 3.7.1, Sortable, axios, SignalR client).

**Thử vai trò / license / phạm vi**: thanh trên cùng có dropdown *Mô phỏng vai trò* (R00–R08) và *Mô phỏng gói license* (BASIC / STANDARD / ENTERPRISE). Lựa chọn lưu `localStorage` (`tek.ac.role`, `tek.ac.lic`, `tek.ac.tenant`, `tek.ac.site`, `tek.ac.lang`, `tek.ac.theme`), bọc try/catch nên file:// hay chế độ ẩn danh vẫn chạy. Chọn **R00** để thấy bộ chọn Tenant; tenant **SGN-T3** có 3 site để thử "Tất cả site".

## 2. Cấu trúc

```
AppUI/
├─ index.html                 danh mục ~38 màn hình theo menu, phase, trạng thái, người dựng
├─ README.md
├─ .claude/launch.json        acui · python -m http.server 8778
├─ management/                phần mềm quản trị web (≈35 trang)
├─ ess/                       cổng nhân viên (login, my-attendance) — mobile-first, chỉ xem
└─ shared/
   ├─ vendor/                 thư viện local
   ├─ css/tek.css             design system TEK (giữ nguyên từ Car Parking)
   ├─ css/ac.css              component riêng Access Control (mục 4.4)
   ├─ js/tek-i18n.js          VI / EN + TEK.i18n.add() cho từng trang
   ├─ js/tek-data.js          dữ liệu giả lập AC, seed cố định (mục 4.2)
   ├─ js/tek-ui.js            helper chung + helper AC (mục 4.3)
   ├─ js/tek-shell.js         topbar + sidebar + mô phỏng vai trò / license / phạm vi (mục 4.1)
   └─ templates/TEK-Mau-NhapNhanSu.xlsx   mẫu nhập nhân sự (sinh bằng output/scripts/build_import_template.py)
```

## 3. Design system

- Màu TEK: navy `#1F3864` (primary), `#2A4B82`, `#5980A6`, nền `#f4f5f7`; ok `#1E7145`, warn `#B26B00`, danger `#9C1E1E`, info `#1F4E79`.
- Topbar 52px navy, brand `TEK<span>/ACCESS</span>`; sidebar trắng 232px; card viền mảnh, bóng nhẹ, bo `.5rem`.
- **Không**: gradient sặc sỡ, glassmorphism, emoji, icon minh hoạ kiểu AI, câu marketing. "Khác biệt" đến từ chức năng (topology, quét IP, I/O test, canvas workflow, bảng bit, ma trận quyền, luồng LIVE).
- Bảng màu trạng thái cửa (dùng thống nhất ở mọi trang):

| state | Nghĩa | Màu |
|---|---|---|
| `closed` | Đóng, bình thường | xanh lá |
| `open` | Đang mở (vừa quẹt) | xanh dương |
| `unlocked` | Mở tự do theo lịch / lệnh | navy nhạt |
| `locked` | Giữ khoá | navy đậm |
| `held` | Mở quá lâu | cam |
| `forced` | Bị phá | đỏ, nhấp nháy |
| `offline` | Mất kết nối | xám sọc chéo |
| `lockdown` | Lockdown | nền đỏ đậm chữ trắng |

- Tag phase: `<span class="ph ph-1">P1</span>`, `ph-2`. Tính năng P2 hiển thị kèm tag, thường `disabled`.
- Ô ghi chú vàng `TEK.ui.mockNote(html)` — giải thích "vì sao thiết kế thế này / khác iSecure ở đâu". Xoá khi lên production.

## 4. API dùng chung

### 4.1 `TEK.shell` (tek-shell.js)

| Hàm | Ý nghĩa |
|---|---|
| `can(perm)` | vai trò mô phỏng có quyền? (quyền xem `TEK.data.PERM_GROUPS`) |
| `licensed(flag)` | gói license mô phỏng có cờ? vd `'wf.designer'`, `'ta.core'` |
| `isTek()` | đang là R00 Superadmin Jointech |
| `tenantObj()`, `sites()`, `siteObj()` (null khi Tất cả site), `isAllSites()` | phạm vi |
| `siteIds()` | mảng site đang trong phạm vi — **dùng để lọc mọi dữ liệu** |
| `plan()` | `{code, sites, doors, ctl, flags}` của gói mô phỏng |
| `roleObj()` | vai trò hiện tại (`TEK.data.ROLES`) |
| `applyGuards(root)` | ẩn phần tử `data-perm`, `data-lic`, `data-tek-only` không thoả; `data-lic-mode="disable"` thì làm mờ + khoá thay vì ẩn. Shell tự gọi 1 lần lúc mount; nội dung vẽ sau thì gọi lại |
| `MENU`, `PAGE_PARENT` | cấu trúc menu; trang con (vd `controller-detail`) tô sáng mục cha |

Menu item: `{key, i18n, text, icon, href, perm, lic, tekOnly, badge}`. Mục không có quyền / không thuộc gói **không hiện**. Mở thẳng URL một trang bị ẩn → shell chèn băng xám giải thích (bản thật trả 403). Sự kiện `tek:shell` phát ra sau khi dựng xong.

Topbar còn có: tìm kiếm toàn cục (tên không dấu, mã NV, số thẻ, CCCD — kết quả nhân sự / cửa / controller; Enter → `people.html?q=`), chip `x/y controller trực tuyến`, VI/EN, toàn màn hình, chế độ tối (P2, `body.tek-dark`).

### 4.2 `TEK.data` (tek-data.js)

Hằng & danh mục:

| Tên | Nội dung |
|---|---|
| `TODAY` / `TODAY_VN` / `mockNow()` | "hôm nay" 22/10/2026; `mockNow()` = 11:42:00 + số giây từ lúc mở trang |
| `LIC_FLAGS`, `LIC_PLANS` | 26 cờ phân hệ (platform.org, platform.multisite, ac.hardware, ac.doors, ac.policy, ac.credential, ac.people, ac.advanced, ac.monitor, ac.alarm, wf.designer, ac.elevator, bio.terminal, ta.core, ta.timesheet, ta.ess, rpt.core, sec.core, int.api, vis.core, ext.*) và gói BASIC/STANDARD/ENTERPRISE/TRIAL (hạn mức site/cửa/controller) |
| `TENANTS` (5), `SITES` (8) | JTC-FACTORY (1 site), SGN-T3 (3 site), EDU-VAS (2), HCM-TOWER, DEMO-JT |
| `BUILDINGS`, `FLOORS`, `ZONES` | S01 chi tiết: 6 toà, 7 khu vực có `apb` (none/soft/hard/timed), `limit`, `occ`, `sec` 1–3 |
| `MODEL_SPEC` | X1100A / X100 / X200 / X300: số reader, input, relay |
| `CONTROLLERS` (22), `MODULES`, `DOORS` (84), `READERS`, `INPUTS`, `RELAYS` | controller có ip, mac `00:06:8E:…`, fw, serial, `online`, `state` (online/warn/offline), latency, syncPct, queue, buffered, power{ac,battery,batteryV,tamper}, cards/events dung lượng. Cửa có `state` (bảng màu ở mục 3), type (door/speedgate/turnstile/barrier/elevator/mantrap), mode, thời gian |
| `INTERLOCKS`, `ACCESS_LEVELS`, `SCHEDULES`, `HOLIDAYS`, `HOLIDAY_GROUPS` | lịch dạng `intervals:[{days:[1..7],from,to}]`; lễ VN 2026–2027 có âm lịch, nghỉ bù |
| `PEOPLE` (750) | tên Việt, `code` NV00123, cccd, bhxh, mst, dept, title, type, company, cards[≤2], `levels` (≤ 8), tempLevels, floors, shiftGroup, bioConsent, blacklist, inside, initials, color |
| `CARD_FORMATS`, `CARD_FORMAT_LIMIT` (253) | fields bit `{k:'P'|'FC'|'ID', from, len, label}` cho bảng bit |
| `EVENT_TYPES`, `PRIORITIES`, `ALARM_STATES`, `SOP`, `ALARMS` | loại sự kiện có `kind` (ok/warn/danger/muted), icon, `alarm` |
| `SHIFTS`, `TS_SYMBOLS`, `LEAVE_TYPES`, `SHIFT_GROUPS` | chấm công |
| `ROLES` (R00–R08, `perms` = mảng hoặc `'*'`), `PERM_GROUPS`, `ALL_PERMS` | phân quyền |
| `WF_NODE_TYPES`, `WF_TEMPLATES` | bảng node (trigger/cond/action/approval, `hw` = chạy được trên controller) và luồng mẫu có `nodes[{id,t,x,y,label,props}]`, `edges[[a,b]]` |
| `SERVICES`, `SERVER`, `CHART` | sức khoẻ hệ thống, số liệu biểu đồ |

Hàm sinh (deterministic):

| Hàm | Trả về |
|---|---|
| `inScope(list, sites?)` | lọc theo `site` của phạm vi đang chọn |
| `stats(sites?)` | `{ctl, ctlOn, doors, doorsOpen, doorsAlarm, doorsOffline, people, inside, alarmsOpen, queue}` |
| `topology(ctlId)` | `{ctl, buses:[{port:'Trên bo'|1|2, modules:[{…, doors, readers, inputs, relays}]}], modules}` |
| `scanNetwork(cidr, siteId)` | kết quả quét IP `[{ip, mac, model, fw, serial, state:'managed'|'new'|'other'}]` |
| `commandQueue(ctlId)` | hàng đợi lệnh `[{id, cmd, created, state, tries, by}]` |
| `events(n, {sites, date, from, to, seed, types})` | lịch sử sự kiện, mới nhất trước |
| `liveStream({sites})` → `.next(type?)` | 1 sự kiện mới tại `mockNow()` — dùng với `setInterval` |
| `cards(tenantId, n)` | kho thẻ (Issued/InStock/Lost/Damaged/Revoked, thẻ tạm, QR động P2) |
| `timesheet(year, month, people)` | `[{person, days:[{d, dow, sym, in, out, late, ot, edited}], tot}]` |
| `leaveBalances(people)`, `taRequests(n)`, `users(n)`, `audit(n)` | số dư phép, đơn từ, tài khoản, nhật ký kiểm toán có hash |
| `gen(seed)` → `{R, int, pick, chance, weighted}` | bộ ngẫu nhiên có seed cho trang tự sinh thêm dữ liệu |
| `fmtDate`, `maskId('079192001234')` → `0791******34`, `pad` | tiện ích |
| `door(id)`, `ctl(id)`, `site(id)`, `tenant(id)`, `zone(id)`, `person(id)`, `byId(list,id)` | tra cứu |

### 4.3 `TEK.ui` (tek-ui.js)

Kế thừa từ Car Parking: `pagehead(title, sub, actions, crumb)`, `mockNote(html)`, `toast(msg, 'ok'|'err'|'warn'|'info')`, `table(rows, cols, {dt, pageLength, order})` (tự bật DataTables tiếng Việt), `chip(text, kind, dot)`, `statusChip(s)`, `chart(id, cfg)`, `clock(id, mock)`, `fullscreen`, `initTooltips()`, `money()`, `pct()`.

Bổ sung cho AC:

| Hàm | Ý nghĩa |
|---|---|
| `doorChip(state)`, `doorTile(door, {compact, selectable, sub, last})`, `doorLegend()` | chip / ô cửa / chú giải theo bảng màu trạng thái |
| `DOOR_STATES`, `DOOR_TYPES`, `typeIcon(type)` | meta trạng thái & loại cửa |
| `avatar(person|name, {size:'sm'|'md'|'lg'|'xl'})` | "ảnh" chủ thẻ dạng chữ cái đầu |
| `evChip(event|type)`, `prioChip(1..4)`, `alarmStateChip(state)` | sự kiện & báo động |
| `ctlChip(ctl)`, `powerIcons(ctl)` | trạng thái controller, nguồn AC / ắc quy / tamper |
| `kpis([{k, v, d, t, tv, icon, href, cls}], colClass)` | dải KPI |
| `bar(pct, kind)` | thanh mảnh (ok/warn/danger/primary) |
| `modal({id, title, icon, body, footer, size, kind:'danger'})` | modal động, trả `bootstrap.Modal` |
| `confirm({title, body, reason:true, okText, kind, onOk(reason)})` | xác nhận có lý do (điều khiển cửa) |
| `stepUp({action, detail, twoPerson, onOk(reason)})` | **xác thực nâng cấp** mật khẩu + TOTP (Q07): reset/xoá controller, lockdown, phục hồi CSDL |
| `topoTree(ctlId, {selected})` + `topoBind(el, onSelect(type, id, node))` | cây topology; type = ctl/bus/mod/door/rdr/inputs/relays |
| `qs(name)` | đọc tham số URL |

### 4.4 `ac.css` — lớp CSS

| Nhóm | Lớp |
|---|---|
| Trạng thái cửa | `.ac-ds .ac-ds-<state>`, `.ac-door-grid(.compact)`, `.ac-door(.picked .selectable)`, `.ac-ds-sw` |
| Avatar | `.ac-avatar.sm/md/lg/xl` |
| Luồng sự kiện | `.ac-feed`, `.ac-ev.k-ok/k-warn/k-danger/k-muted(.new)` |
| Topology | `.ac-topo`, `.ac-topo-node.n-ctl/n-bus/n-mod/n-door/n-rdr/n-io(.sel)`, `.ac-sdot.ok/act/bad/unk` |
| Kiểm tra I/O | `.ac-io-grid`, `.ac-io(.is-fault .is-active .relay-on)`, `.ac-led.normal/active/fault-open/fault-short/unknown`, `.ac-relay(.on)`, `.ac-raw .p/.fc/.id` |
| Quét mạng | `.ac-scan-bar > i`, `.ac-scan-log`, `table.ac-scan tr.is-new/is-off` |
| Bảng bit | `.ac-bitgrid`, `.ac-bit.b-p/b-fc/b-id/b-x(.sel .drag)` (`.n` số bit, `.v` giá trị), `.ac-bitlegend` |
| Workflow | `.ac-wf` (3 cột), `.ac-wf-palette .grp`, `.ac-wf-pitem.t-trigger/t-cond/t-action/t-approval`, `.ac-wf-canvas > .ac-wf-stage`, `.ac-wf-edges path.ac-wf-edge(.sel .run .draft)`, `.ac-wf-node.t-*(.sel) .hd .bd`, `.ac-wf-port.in/.out`, `.ac-wf-props`, `.ac-wf-toolbar`, `.ac-runs.ac-runs-ctl/ac-runs-srv` |
| Mặt bằng | `.ac-fp`, `svg.ac-fp-svg .room/.wall/.lbl`, `g.ac-fp-door.s-<state>(.sel)`, `.ac-fp-cam`, `.ac-fp-rdr`, `.ac-fp-pop .hd .bd .acts` |
| Bảng công | `.ac-ts-wrap`, `table.ac-ts(.locked)` `th.we`, `.nm`, `td.c(.late .edited) .ot`, `td.tot`, `.ts-P/.ts-half/.ts-V/.ts-PN/.ts-O/.ts-L/.ts-CT/.ts-OFF/.ts-future`, `.ac-ts-legend` |
| Lịch tuần | `.ac-week` (44px + 48 ô nửa giờ), `.c(.on .hol .paint .hr)`, `.dl`, `.h` |
| Ma trận | `.ac-matrix-wrap`, `table.ac-matrix th.rot > div`, `.rh`, `td.cell(.on .s2 .s3 .drop)` |
| Báo động | `.ac-prio-1..4`, `.ac-alarm-row.p1..p4`, `ol.ac-sop li(.done)` |
| Controller | `.ac-power .ok/.bad/.unk`, `.ac-bar(.w100) .ac-bar-ok/warn/danger/primary`, `.ac-lic-meter` |
| Nguy hiểm & xác thực | `.ac-danger-zone .hd .row-act`, `.modal-header.ac-hd-danger`, `.ac-otp input`, `.ac-otp-input` |
| Màn bảo vệ | `body.ac-guard-on`, `.ac-guard`, `.ac-guard-bar`, `.ac-guard-grid.l1/l2/l4`, `.ac-gpane .who .meta .verdict.ok/danger/warn/muted`, `.ac-guard-strip .it`, `.ac-layout-pick` |
| Tiện ích | `.ac-unlicensed`, `.ac-kv-grid`, `.ac-occ`, `.ac-split` (340px + phần còn lại, xếp chồng < 1200px), `.ac-sticky`, `.ac-svc`, `.ac-hint`, `.ac-code` |
| Chế độ tối P2 | `body.tek-dark` |

## 5. Quy ước trang

```html
<!DOCTYPE html><html lang="vi"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tên trang · TEK Access Control</title>
<link rel="stylesheet" href="../shared/vendor/bootstrap/bootstrap.min.css">
<link rel="stylesheet" href="../shared/vendor/fontawesome/css/all.min.css">
<link rel="stylesheet" href="../shared/vendor/datatables/dataTables.bootstrap5.css">
<link rel="stylesheet" href="../shared/css/tek.css">
<link rel="stylesheet" href="../shared/css/ac.css"></head>
<body data-page="doors">                     <!-- = key trong MENU -->
<div id="tek-shell"></div>
<div class="tek-main"><div id="head"></div><div id="note"></div> … </div>
<script src="../shared/vendor/js/jquery-3.7.1.js"></script>
<script src="../shared/vendor/bootstrap/bootstrap.bundle.min.js"></script>
<script src="../shared/vendor/datatables/dataTables.js"></script>
<script src="../shared/vendor/datatables/dataTables.bootstrap5.js"></script>
<script src="../shared/vendor/js/chart.js"></script>
<script src="../shared/js/tek-i18n.js"></script>
<script src="../shared/js/tek-data.js"></script>
<script src="../shared/js/tek-ui.js"></script>
<script src="../shared/js/tek-shell.js"></script>
<script> const U = TEK.ui, D = TEK.data, S = TEK.shell; const sids = S.siteIds(); … </script>
```

1. `pagehead` gồm tiêu đề (bọc `<span data-i18n="nav.xxx">`), phụ đề có số liệu thật, nút hành động; ô vàng `mockNote` nêu lý do thiết kế + khác iSecure + API dự kiến.
2. Lọc dữ liệu theo `S.siteIds()` / `D.inScope()`. Không tự đọc localStorage của shell.
3. Khối theo license dùng `data-lic="flag"`, theo quyền `data-perm="perm"`; P2 gắn `<span class="ph ph-2">P2</span>` và `disabled`.
4. Nút chỉ có icon phải có `title` (+ `data-bs-toggle="tooltip"`). Thao tác điều khiển → `U.confirm({reason:true})`; thao tác phá huỷ → `U.stepUp()`.
5. Khoá i18n riêng của trang: `TEK.i18n.add({vi:{…}, en:{…}})` trong script trang, dùng tiền tố tên trang (`ctl.scan`, `wf.publish`…).
6. Dữ liệu sửa trong trang chỉ sống trong bộ nhớ (reload là mất) — đúng tinh thần bản mock.
7. Kiểm tra: không lỗi console, không cuộn ngang ở 1280px và 1024px. Xong trang → đổi trạng thái trong `index.html` (`'todo'` → `'done'`, ghi người dựng).

## 6. Ánh xạ trang → API dự kiến (ASP.NET Core 8)

| Trang | API / realtime |
|---|---|
| login | `POST /api/auth/login` → `{requiresOtp}`; `POST /api/auth/otp/verify`; `POST /api/auth/otp/resend` (OTP trong IMemoryCache, không ghi DB) |
| dashboard | `GET /api/dashboard/summary?siteIds=`; `GET /api/system/health`; SignalR `/hubs/monitor` |
| live-monitor | SignalR `/hubs/monitor` (`EventReceived`, `DoorStateChanged`, `OccupancyChanged`); `GET /api/doors/states`; `GET /api/zones/{id}/occupants` |
| controllers | `GET /api/controllers`; `POST /api/controllers/scan` (job) + `GET /api/controllers/scan/{jobId}`; `POST /api/controllers/adopt`; `POST /api/controllers`; `POST /api/controllers/{id}/sync` |
| controller-detail | `GET /api/controllers/{id}/topology`; `POST /api/controllers/{id}/modules`; `POST /api/controllers/{id}/time-sync`; `GET /api/controllers/{id}/io`; `POST /api/controllers/{id}/io/relays/{n}`; `POST /api/readers/{id}/test` (led/beep/swipe); `GET /api/controllers/{id}/commands`; `POST /api/controllers/{id}/commands/{cmdId}/retry`; `POST /api/controllers/{id}/reset` (yêu cầu header step-up token) |
| floorplan / door-control | `GET /api/floorplans/{floorId}`; `POST /api/doors/{id}/commands` (`pulse|unlock|lock|normal|reader-off`); `POST /api/sites/{id}/lockdown` (step-up) |
| alarms | `GET /api/alarms?state=`; `POST /api/alarms/{id}/ack|progress|close`; SignalR `AlarmRaised` |
| events | `GET /api/events?from&to&person&door&result&type`; `GET /api/events/export` |
| doors / elevators / biometrics | `GET/PUT /api/doors/{id}`; `PUT /api/doors/bulk`; `GET/PUT /api/elevators/{id}/floor-map`; `GET /api/biometric-devices`; `POST /api/biometric-devices/{id}/sync` |
| schedules / access-levels / zones / card-formats | `/api/schedules`, `/api/holidays`, `/api/access-levels`, `GET /api/access/explain?doorId=&at=` / `?personId=`, `/api/zones`, `/api/interlocks`, `/api/card-formats`, `POST /api/card-formats/decode` |
| workflows | `/api/workflows` (CRUD + versions), `POST /api/workflows/{id}/dry-run`, `POST /api/workflows/{id}/publish`, `GET /api/workflows/{id}/runs` |
| people / credentials / import-center / org | `/api/people`, `/api/people/{id}/history`, `/api/credentials`, `POST /api/imports/{type}` + `GET /api/imports/{id}/errors`, `/api/org/*` |
| ta-* | `/api/ta/shifts`, `/api/ta/rosters`, `/api/ta/policies`, `GET /api/ta/timesheets?month=`, `POST /api/ta/timesheets/recalc`, `POST /api/ta/periods/{id}/lock`, `/api/ta/requests`, `/api/ta/leave` |
| reports | `GET /api/reports`, `POST /api/reports/{code}/preview`, `/export?format=xlsx|pdf|csv`, `/api/report-schedules` |
| sites / tenants / users / roles | `/api/sites`, `/api/tenants` (chỉ R00), `/api/users`, `/api/roles`, `/api/permissions` |
| license | `GET /api/license` (đọc token Ed25519 từ Cổng quản lý license Jointech, không cấp tại đây) |
| audit / settings / backup | `GET /api/audit?verify=true`, `/api/settings/*`, `/api/webhooks`, `/api/api-keys`, `/api/backups`, `POST /api/backups/{id}/restore` (step-up) |
| ESS | `POST /api/ess/login`, `GET /api/ess/me/attendance?month=`, `/api/ess/me/leave`, `/api/ess/me/access` (chỉ đọc) |

## 7. Quyết định thiết kế

1. **Menu ẩn theo license + quyền** (Q04): `lic` và `perm` trên từng mục menu; khối trong trang dùng `data-lic` / `data-perm`. Mô phỏng gói license độc lập với gói hợp đồng của tenant để duyệt đủ màn hình (mặc định ENTERPRISE).
2. **Tenant chỉ Superadmin Jointech thấy**: bộ chọn Tenant ẩn với mọi vai trò khác; tenant 1 site thì bộ chọn Site cố định; nhiều site cần cờ `platform.multisite`.
3. **Superadmin đăng nhập 2 bước** OTP email kỹ thuật Jointech, mã chỉ trong RAM, phiên 60 phút, sai 1 lần sinh lại, cần Internet; người dùng chủ đầu tư dùng TOTP.
4. **Quét IP rồi chọn để nhận controller** thay vì nhập tay; nhận diện HID bằng OUI `00:06:8E`; chặn khi vượt hạn mức cửa license.
5. **Đồng bộ delta + hàng đợi lệnh** thay nút Go-Live của iSecure; controller offline vẫn chạy theo dữ liệu đã nạp, sự kiện tồn đọng tự thu hồi.
6. **Topology dạng cây** là trung tâm trang controller; I/O test chạy ngay trên web, thao tác phá huỷ qua xác thực nâng cấp (mật khẩu + TOTP, reset nhà máy cần 2 người).
7. **Màn bảo vệ** ưu tiên ảnh lớn để đối chiếu mặt người, bố cục 1/2/4 khung theo nhóm cửa.
8. Dữ liệu Việt Nam thật (tên, CCCD che `0791******34`, BHXH, MST, lễ âm lịch); CCCD đầy đủ chỉ hiện khi có quyền `person.pii`.
9. Đồng hồ giả lập (`mockNow`) bắt đầu 11:42 ngày 22/10/2026 để sự kiện sống nối tiếp dữ liệu mẫu buổi sáng.

## 8. Danh sách trang & người dựng

| Nhóm | Trang | Trạng thái | Người dựng |
|---|---|---|---|
| Nền móng | `shared/*` (shell, data, ui, i18n, ac.css, mẫu Excel), `index.html`, `README.md`, `.claude/launch.json` | Đã dựng | Agent nền móng |
| Đăng nhập & tổng quan | `login`, `dashboard` | Đã dựng | Agent nền móng |
| Giám sát | `live-monitor` | Đã dựng | Agent nền móng |
| Thiết bị | `controllers`, `controller-detail` | Đã dựng | Agent nền móng |
| Giám sát | `floorplan`, `alarms`, `door-control`, `events` | Chờ dựng | Vòng 2 |
| Thiết bị | `doors`, `elevators`, `biometrics` | Chờ dựng | Vòng 2 |
| Quyền truy cập | `schedules`, `access-levels`, `zones`, `card-formats`, `workflows` | Chờ dựng | Vòng 2 |
| Nhân sự | `people`, `credentials`, `import-center`, `org` | Chờ dựng | Vòng 2 |
| Chấm công | `ta-shifts`, `ta-policies`, `ta-timesheet`, `ta-requests`, `ta-leave` | Chờ dựng | Vòng 2 |
| Báo cáo & hệ thống | `reports`, `sites`, `tenants`, `users`, `roles`, `license`, `audit`, `settings`, `backup` | Chờ dựng | Vòng 2 |
| ESS | `ess/login`, `ess/my-attendance` | Chờ dựng | Vòng 2 |
| Đề xuất (không dựng) | `badge-designer`, `form-builder`, `visitors` | — | — |

Mẫu Excel: `output/scripts/build_import_template.py` → `shared/templates/TEK-Mau-NhapNhanSu.xlsx` (3 sheet HuongDan / NhanSu / DanhMuc, 23 cột, freeze A3, comment từng tiêu đề, validation trỏ DanhMuc, 2 dòng mẫu vàng, tối đa 5.000 dòng).
