GREAT LOTUS · NIRVANA FRAMEWORK · HỆ ĐỒNG BỘ PROFILE CHROME
B36 PROFILE DASHBOARD
GIÁO TRÌNH SỬ DỤNG, KHỞI TẠO & VẬN HÀNH
Lotus
Profile Fleet — kho profile Chrome trên NAS2 theo WP1 / WP2 /
WP3
Khởi tạo từ đầu · Kết nối worker và n8n · Mổ
xẻ từng thành phần giao diện · Dọn dẹp an toàn · Xử
lý sự cố
Trang https://profiles.lotus1104.synology.me/ chụp ngày 18/09/2026 qua Chrome thật (B35).
Hạng mục |
Nội dung |
Đối tượng đọc |
Người vận hành hệ Great Lotus, kỹ sư triển khai worker Windows, người viết workflow n8n cần pull/push profile, và học viên đào tạo nội bộ |
Phiên bản |
Bản 2 — biên soạn và cập nhật ngày 18/09/2026, đối chiếu trực tiếp mã nguồn và hệ thống đang chạy trên HP1. Bản 2 ghi nhận các lỗi đã sửa cùng ngày (Phụ lục F) |
Đối tượng mô tả |
Container lotus_profile_dashboard (B36) · IP 114.114.114.36 · cổng máy chủ 20360 · công khai tại https://profiles.lotus1104.synology.me/; kèm container trợ lực lotus_profile_broker (B37) |
Phiên bản phần mềm |
API FastAPI version="1.1.0" (app/server.py) · giao diện app.js?v=1.8.1 · image lotus/profile_dashboard:1.0 · B37 lotus/profile_broker:0.1 |
Nguồn sự thật |
B36_Profile_Dashboard/app/*, static/*, B37_Profile_Broker/, docs/tools/profile_store.py, docker-compose.yml, B16_Caddy/Caddyfile và ảnh chụp hệ thống sống qua trình duyệt điều khiển B35 |
Nơi lưu tài liệu |
DCP_PRODUCTION/B36_Profile_Dashboard/Giao_Trinh/ — cùng bố cục với giáo trình thư viện C12 (.docx + .pdf + html/) |
GHI CHÚ · CÁCH ĐỌC GIÁO TRÌNH NÀY Mỗi màn hình được chụp từ hệ thống ĐANG CHẠY và đánh số bằng vòng tròn chỉ tô viền, đặt bên ngoài phần tử để không che chi tiết. Ngay sau mỗi hình là một bảng giải thích từng thành phần mang số trên hình đó. ▸ Hộp NGUY HIỂM màu đỏ = thao tác làm mất dữ liệu hoặc mở đường cho mất dữ liệu. ▸ Hộp ĐÚNG THIẾT KẾ màu xanh lá = hành vi trông như lỗi nhưng là chủ ý. ▸ Hình ghi (mô phỏng) là trạng thái hiện KHÔNG có trên kho thật; được dựng lại chỉ trong trình duyệt bằng cách bọc phản hồi /api/snapshot — không một byte nào ghi xuống NAS, B36 hay B37. Profile mô phỏng mang số UID_9000xx để không lẫn với số thật. |
Great Lotus chạy hàng loạt profile Chrome trên các máy worker Windows ở ba workpool. Mỗi profile là một kho trạng thái đắt giá — cookie đăng nhập, lịch sử, cấu hình — nên nó được cất tập trung trên NAS2 dưới dạng gói .tar, và mỗi lần chạy worker lại kéo về, chạy xong đẩy lên. B36 Profile Dashboard là tấm gương soi toàn bộ kho đó: có bao nhiêu profile, cái nào đang chạy trên máy nào, cái nào lỗi, cái nào bị khoá treo, và thống kê sử dụng theo ngày/tuần/tháng. Từ phiên bản có broker B37, nó còn là bảng điều khiển: khôi phục profile về worker, gỡ khoá treo, đóng Chrome bị treo, cách ly, xoá hẳn và dọn dẹp.
Cuốn sách trả lời ba câu hỏi theo đúng thứ tự người mới gặp phải: dựng nó từ đầu thế nào (Phần III), nối nó với worker và n8n thế nào (chương 7–8), và đọc từng nút trên giao diện ra sao (Phần IV). Các phần sau là vận hành, an toàn, sự cố và bài thực hành.
ĐÚNG THIẾT KẾ · KỶ LUẬT NGUỒN Mọi con số, tên phần tử, hành vi mô tả trong sách đều truy ngược được về một trong ba nguồn: (1) mã nguồn đang chạy (đọc thẳng file, không nhớ lại); (2) tài liệu kỹ thuật nội bộ có số đo thật (README.md của B36/B37, docs/, sổ ghi nhớ vận hành); (3) ảnh chụp và lệnh kiểm chứng trên hệ thống sống ngày 18/09/2026. Chỗ nào không truy được thì bỏ, không lấp bằng một con số nghe hợp lý. |
Ảnh trong sách chụp ngày 18/09/2026, sau đợt sửa lỗi buổi tối cùng ngày. Bảng dưới là số đo thật lúc đó, để người đọc về sau biết sách mô tả hệ thống ở thời điểm nào.
Hạng mục |
Giá trị đo được |
Lệnh / nguồn |
B36 container |
Up 13 days, image lotus/profile_dashboard:1.0 |
docker ps |
Trang công khai |
không kèm mật khẩu: HTTP 401; có tài khoản: HTTP 200 (basic_auth ở Caddy từ 18/09) |
curl -u |
Kho NAS2 |
mounted: true, store: /nas |
GET /api/health |
Số profile |
2, cả hai Sẵn sàng, cùng WP3, tổng 9,9 MB (10 383 360 byte) |
GET /api/summary |
Profile |
UID_000010_GPM_Profile, UID_000011_GPM_Profile — mỗi gói 5 191 680 byte, 455 mục |
tar tf trong container |
Toàn vẹn |
sha256 của cả hai .tar khớp manifest.json |
sha256sum |
Lần ghi kho cuối |
2026-08-15T02:49:24Z — 34 ngày không có push/pull nào |
manifest.updated |
Sổ lịch sử |
runs.jsonl 832 sự kiện (gồm dữ liệu demo tháng 7–8, xem 10.4) |
GET /api/stats |
B37 broker |
tạo lại 18/09 để đọc B37_MYSQL_PASS từ .env; wp_topics = {WP3: WP3-PC1}, MySQL enabled: true |
GET :20370/health |
Worker WP3 |
Process Manager bật lại 18/09 (tắt từ 16/08); bắt mạch alive: true, rtt_ms: 640 |
GET /api/workers |
Sổ A4 (MySQL) |
2 hàng, cả hai khớp profile trên NAS (id 21, 22) |
GET /api/mysql |
Fleet Monitor B34 |
WP3 / PC_1 online sau khi bật lại Process Manager |
GET :20340/api/state |
LƯU Ý · KHO VẪN ĐANG NGHỈ DÙ WORKER ĐÃ BẬT LẠI Từ 16/08 tới 18/09 máy worker WP3-PC1 không chạy Process Manager; nó đã được bật lại tối 18/09 và trả lời bắt mạch. Nhưng chưa có workflow nào chạy lại, nên kho chưa có lần ghi mới. Vì thế trong sách, các ảnh về trạng thái Đang chạy và Khoá treo là ảnh mô phỏng; còn các thao tác cần worker (Khôi phục, Xử lý treo, quét A1/A2 khi Dọn dẹp) chỉ được chụp tới bước hộp thoại xác nhận. |
Đem tài liệu sẵn có và chính giao diện ra đối chiếu với mã nguồn đang chạy, thấy các điểm lệch dưới đây. Phần lớn đã được sửa ngay trong ngày 18/09/2026; cột cuối ghi tình trạng. Chi tiết ở Phụ lục F.
# |
Chỗ lệch |
Thực tế (đã kiểm chứng) · tình trạng |
1 |
Ngăn kéo chi tiết gợi ý lệnh pull <id> --dest <thư mục> --lease, push <id> --dir <thư mục> --host %COMPUTERNAME%, lease steal <id> --host … |
Cả ba bị profile_store.py từ chối (unrecognized arguments). Đúng là --host đặt trước lệnh con; pull <id> <dest>; push <id> <thư mục>. Đã sửa — mục 11.6 |
2 |
README.md của B36 mô tả dashboard chỉ đọc, không có nút hành động |
Từ 13–14/08 đã có 5 nút một chạm + Dọn dẹp + Xoá hẳn, đi qua B37. Đã sửa README, /help, chân trang |
3 |
README.md §12 nói kho đang chứa 6 profile demo |
Kho giờ chỉ còn 2 profile thật; nhưng runs.jsonl vẫn giữ 776 sự kiện demo (id p_WP…) bên cạnh 56 sự kiện thật. Chưa dọn — chờ duyệt (mục 17.4) |
4 |
Dải trống nằm giữa tiêu đề Profiles và đầu bảng |
Là thanh chọn-nhiều bị CSS display:flex đè thuộc tính hidden. Đã sửa |
5 |
Cột Thời lượng, Cache và nhãn WP trong lịch sử |
Push từ node n8n ghi dur: 0, cache_mb: 0, wp: "". Đã sửa phần wp (tự suy từ tên máy); dur/cache vẫn phụ thuộc workflow truyền vào |
6 |
Hạn lease mặc định |
Ba giá trị khác nhau: profile_store.py 1800 s, .bat 300 s, GUI Process Manager 180 s. Giữ nguyên — xem F6 |
7 |
Trang công khai profiles.lotus1104.synology.me |
Không có xác thực trong khi đã có nút ghi; cổng 443 của WP1 mở ra Internet. Đã sửa: basic_auth ở Caddy (chương 18) |
Mục lục
Mở tài liệu bằng Microsoft Word rồi bấm F9 (hoặc chuột phải > Cập nhật trường) để điền số trang. Bản PDF đi kèm đã có số trang đúng.
⟳ Mở bằng Microsoft Word rồi bấm Ctrl+A → F9 để sinh mục lục kèm số trang.
PHẦN I
TỔNG QUAN
Sản phẩm chính của Great Lotus là một chuỗi tự động: n8n phát lệnh qua MQTT tới worker Windows (Process Manager APP_5); worker nhờ GPM Login mở Chrome kèm cổng CDP; n8n điều khiển ngược qua cổng đó. Profile Chrome — nơi chứa cookie đăng nhập và mọi dấu vết của một danh tính — không được để rải rác trên ổ cứng từng máy, vì máy hỏng là mất, và hai máy không dùng chung được. Lời giải là một kho tập trung trên NAS2.
Kho mà không nhìn thấy thì không vận hành được. B36 trả lời những câu mà người vận hành hỏi hằng ngày:
Kho có bao nhiêu profile, nặng bao nhiêu? — Bốn ô tổng hợp đầu trang và cột Cỡ.
Profile nào đang chạy, trên máy nào, còn bao lâu nữa hết hạn giữ chỗ? — Cột Đang chạy ở.
Profile nào hỏng, vì sao? — Trạng thái Cách ly + bảng cách ly kèm lý do.
Máy nào chết giữa chừng mà vẫn giữ profile? — Trạng thái Khoá treo.
Tuần này chạy bao nhiêu lượt, dồn vào profile/máy nào? — Khu Thống kê sử dụng.
Muốn đem profile về lại worker, gỡ khoá, xoá, dọn rác? — Các nút hành động (qua B37).
Vai trò |
Dùng B36 để |
Chương nên đọc |
Người vận hành |
theo dõi kho hằng ngày, xử lý khoá treo và profile lỗi, dọn dẹp |
9–14, 16–17, 20 |
Kỹ sư triển khai |
dựng kho NAS2, container B36/B37, route Caddy từ đầu |
3–6, 18 |
Người lo worker |
nối Process Manager trên máy Windows với kho |
7, 20 |
Người viết workflow |
gọi pull/push profile từ node C12 của n8n |
8, 17.3 |
Học viên |
hiểu mô hình năm nơi + làm bài thực hành |
1–2, 21 |
Muốn dùng đúng B36 phải nắm mô hình năm nơi (chốt ngày 14/08/2026). Một profile cùng lúc hiện diện ở năm chỗ, mỗi chỗ một vai; tài liệu, mã nguồn và giao diện đều gọi bằng mã kèm tên để đọc tới đâu hiểu tới đó.
Hình 1.1. Năm nơi của một profile và hệ hình SAO quanh A3 (NAS).
Mã |
Nơi |
Vai trò |
Ai được ghi |
A1 (GPM) |
GPM Login trên worker, API 127.0.0.1:19995 |
danh tính + entry để mở trình duyệt |
worker (qua API localhost) |
A2 (C_Temp) |
C:\ZGPM_Loaded_Profiles\<mã>-<ngày> |
dữ liệu lúc chạy — vứt được |
worker |
A3 (NAS) |
\\172.16.20.201\lotus_profiles\profiles\<id>.tar |
CHÂN LÝ dữ liệu |
worker (push) + B37 |
A4 (MySQL) |
bảng G01_GREAT_LOTUS_D02_Port_Pool.G01_GREAT_LOTUS_D03_GPM_Profile |
sổ profile ↔ IP ↔ email cho n8n |
n8n + B37 |
A5 (WebProfile) |
chính B36 — profiles.lotus1104.synology.me |
gương + bảng điều khiển |
không ai (chỉ đọc kho) |
NGUY HIỂM · ĐIỂM KHÔNG QUAY LẠI DUY NHẤT Xoá nhầm A1, A2, A4 hay A5 đều cứu được từ A3. Nhưng xoá A3 khi A2 đã trống = mất vĩnh viễn. Vì vậy mọi nút xoá trên B36 mặc định chuyển sang cách ly hoặc thùng rác _deleted/, không xoá thật — trừ khi chính bạn tick Xoá VĨNH VIỄN. |
GHI CHÚ · CÁCH GỌI CŨ Trước 14/08/2026 thiết kế bỏ sót MySQL, nên web dashboard mang mã A4. Gặp tài liệu hay commit cũ ghi "A4 = dashboard" thì hiểu là A5 (WebProfile) ngày nay. |
B36 chỉ đọc kho. Nó mount NAS2 bằng tài khoản WP2_GPM_Profile_RO và cờ ro — hai lớp chặn ghi độc lập, nên bản thân B36 không thể làm hỏng kho. Mọi thao tác ghi (khôi phục, gỡ khoá, xoá, dọn dẹp) được B36 chuyển tiếp sang B37 Profile Broker — container duy nhất có quyền ghi NAS (WP2_GPM_Profile_RW), nói MQTT với worker và được xoá hàng MySQL.
Thành phần |
Đọc kho |
Ghi kho |
Nói MQTT |
Ghi MySQL |
B36 (A5) |
có (RO) |
không |
không |
không |
B37 broker |
có |
có (RW) |
có |
có (user bot) |
Worker (profile_store.py) |
có |
có (push, lease) |
có (nhận lệnh) |
không |
n8n |
gián tiếp |
gián tiếp (qua worker) |
có (phát lệnh) |
có |
ĐÚNG THIẾT KẾ · ĐÚNG THIẾT KẾ Trình duyệt không bao giờ gọi thẳng B37. B37 chỉ nghe trong LAN/VPN (:20370); mọi nút bấm đi qua POST /api/command/<lệnh> của B36, B36 mới gọi B37 server-to-server. |
Không mở file .tar. — Mọi thông tin đọc từ manifest.json, runs.jsonl, tên file trong _locks/ và quarantine/, cùng kích thước file — mở tar qua SMB quá đắt.
Không tự gọi GPM. — API GPM là 127.0.0.1 trên từng worker; HP1 không với tới. Việc đụng GPM luôn do worker thi hành theo lệnh MQTT.
Không giữ cơ sở dữ liệu riêng. — Tắt B36 không mất gì; bật lại là đọc lại kho.
Không có đăng nhập. — Xem chương 18 về hệ quả khi trang mở ra Internet.
Từ 14/08/2026, ID của mọi profile do hệ thống tạo có dạng UID_000001_GPM_Profile — sáu chữ số tăng dần, không tái sử dụng. Chuỗi này là tên file ở A3 (<id>.tar), cột profile_id ở A4, khoá trong .lotus_profile_map.json ở A2, dòng hiển thị ở A5 — và là TÊN profile trong GPM (A1). GPM vẫn dùng UUID nội bộ, nhưng cầu nối giữa A1 và phần còn lại là tên: hỏi GPM "profile nào tên UID_000003_GPM_Profile" là ra UUID.
Cấp số ở đâu? — profile_store.py uid next — nguyên tử trên SMB bằng file _uid/claims/<shard>/<tên>.claim tạo với O_CREAT|O_EXCL. n8n xin trực tiếp qua GET /uid/next?count=N của B37.
Vì sao là tên? — Không có trạng thái để hỏng: máy trắng, cài lại GPM, khôi phục từ NAS xong tên lại trùng là cầu nối tự liền.
Ý nghĩa thật: con dấu sở hữu. — Nút Dọn dẹp chỉ được xoá profile GPM mang dấu này; tên khác (LOTUS_…, tên tự đặt tay) chỉ được báo cáo.
LƯU Ý · ĐỔI TÊN TRONG GPM = ĐỔI ID Sửa tay tên UID_… trong GPM là cắt cầu nối. Dọn dẹp thấy mất con dấu nên chỉ báo cáo, không xoá — mất liên kết chứ không mất dữ liệu. Xem Phụ lục B. |
Kho lưu mỗi profile thành một gói TAR không nén. Lý do ghi trong đầu profile_store.py: profile 50 MB xả nén TAR mất 1,45 s, 0 % CPU; ZIP/ZSTD mất 3,12 s vì tốn CPU giải nén. Chrome không bao giờ chạy thẳng trên share (random I/O qua SMB đo được 166,7 s) — luôn xả ra SSD cục bộ trước.
Khi đẩy lên, gói được ghi vào incoming/<id>.<host>.tmp rồi os.replace sang profiles/<id>.tar. Đổi tên trong cùng một share là nguyên tử, nên người đọc không bao giờ thấy một gói ghi dở. Số đo thật qua VPN (05/08/2026, gói 50 MB): đóng gói 0,44 s · upload 4,84 s (~10 MB/s) · pull 0,24 s · xả 0,09 s.
Hai máy chạy cùng một profile rồi cùng push là ghi đè lẫn nhau. Chống việc đó bằng lease: trước khi pull để chạy, worker tạo _locks/<id>.lock bằng O_EXCL (thử 10 tiến trình đua trên NAS2: đúng 1 thắng). Lease có hạn (TTL); trong lúc profile mở, một luồng nền gia hạn mỗi TTL/3 giây. Push tự từ chối nếu máy khác đang giữ lease còn hạn.
Hình 2.1. Vòng đời một phiên profile trên worker.
Hình 2.2. Lease hết hạn ra sao khi máy crash (ví dụ TTL 300 s).
Nơi đặt TTL |
Giá trị mặc định |
Nguồn |
profile_store.py (LEASE_TTL_SEC) |
1800 s |
docs/tools/profile_store.py |
.bat khởi động worker (LOTUS_PROFILE_LEASE_TTL) |
300 s |
ghi nhớ vận hành 05/08/2026 |
GUI Process Manager, ô Lease TTL (sec) |
180 s |
_default_values() của GUI |
MẸO · CHỌN TTL Có heartbeat nên TTL ngắn vẫn an toàn cho phiên dài. TTL ngắn = máy crash thì chỉ vài phút sau máy khác đã giành lại được. Máy crash rồi khởi động lại thì đòi lại lease của chính nó ngay (same-host reclaim). |
Tệp |
Là gì |
B36 dùng để |
_index/manifest.json |
một bản ghi mỗi profile: size, sha256, status, work_pool, runs, lần chạy cuối, cache |
cột bảng + ngăn kéo chi tiết |
_index/runs.jsonl |
sổ chỉ-thêm, mỗi lần push một dòng {ts,id,host,wp,dur,cache_mb,size} |
khu thống kê |
_locks/<id>.lock |
ai giữ, lấy lúc nào, gia hạn lúc nào, expires_ts |
Đang chạy / Khoá treo |
quarantine/<id>.<ts>.tar |
bản lỗi đã tách khỏi kho chính |
Cách ly + bảng cách ly |
_deleted/ |
thùng rác của Xoá hẳn (và bản sao hàng MySQL đã xoá) |
không hiển thị |
B36 không đọc trạng thái từ đâu cả — nó suy ra trạng thái theo đúng thứ tự trong store_reader.snapshot():
Hình 2.3. Luật suy trạng thái, kiểm từ trên xuống.
Trạng thái |
Nhãn trên giao diện |
Nghĩa |
Cần làm |
running |
Đang chạy (xanh lá) |
có máy giữ lease còn hạn |
không đụng vào |
idle |
Sẵn sàng (xanh dương) |
có .tar, không ai chạy |
không cần |
quarantined |
Cách ly (đỏ) |
bản lỗi đã tách khỏi kho chính |
điều tra, thay bản tốt |
stale-lock |
Khoá treo (vàng) |
lease đã hết hạn — máy giữ có thể đã chết |
kiểm máy, rồi gỡ khoá |
missing |
Thiếu file (xám) |
manifest có nhưng không thấy .tar |
push lại hoặc dọn |
PHẦN II
KIẾN TRÚC & HẠ TẦNG
Hình 3.1. Các khối và đường nối thật của B36/B37 trên HP1.
Mọi khối trong hình đều khai trong DCP_PRODUCTION/docker-compose.yml hoặc B16_Caddy/Caddyfile, trừ NAS2, broker MQTT và worker là máy ngoài HP1.
Thuộc tính |
Giá trị |
Ghi chú |
Tên container |
lotus_profile_dashboard |
hostname PROFILE_DASHBOARD |
Image |
lotus/profile_dashboard:1.0 |
build từ B36_Profile_Dashboard/Dockerfile (python:3.12-slim) |
IP nội bộ |
114.114.114.36 |
mạng GreatLotus_Net |
Cổng |
20360:8080 |
quy ước Bxx: B36 → .36 → 20360 |
Tiến trình |
uvicorn server:app --host 0.0.0.0 --port 8080 |
không bật --reload |
Thư viện |
fastapi==0.115.6, uvicorn[standard]==0.34.0 |
app/requirements.txt; giao diện không dùng CDN |
Mount |
app/ → /app/app:ro, static/ → /app/static:ro, nas2_profiles_ro:/nas:ro |
code bind-mount chỉ đọc |
Múi giờ |
TZ=Asia/Ho_Chi_Minh + 2 mount zoneinfo |
đủ 3 dòng theo quy định UTC+7 |
Khởi động lại |
restart: unless-stopped |
— |
Biến |
Mặc định |
Ý nghĩa |
PROFILE_STORE_DIR |
/nas |
gốc kho trong container |
STATIC_DIR |
/app/static |
nơi chứa SPA |
SNAPSHOT_CACHE_SEC |
2 |
gộp các lần poll /api/snapshot của nhiều tab trong 2 s |
STATS_CACHE_SEC |
30 |
cache /api/stats — thống kê không cần tươi từng giây |
B37_URL |
http://114.114.114.37:8080 |
nơi chuyển tiếp lệnh ghi |
B36_Profile_Dashboard/ ├── Dockerfile python:3.12-slim, pip fastapi/uvicorn, CMD uvicorn :8080 ├── README.md tài liệu kỹ thuật (cập nhật 18/09 — ranh giới B36 đọc / B37 ghi) ├── app/ │ ├── requirements.txt │ ├── server.py 13 endpoint + proxy lệnh sang B37 + phục vụ SPA │ └── store_reader.py đọc kho: manifest/runs/locks/quarantine -> snapshot + stats ├── static/ │ ├── index.html khung trang chính │ ├── app.js toàn bộ logic giao diện (vanilla JS, 1 328 dòng) │ ├── style.css nền tối/sáng, huy hiệu, ngăn kéo, hộp thoại │ ├── help.html + help.css trang /help │ └── manual.html hướng dẫn đồng bộ năm nơi (HDID_00008) └── Giao_Trinh/ giáo trình này |
MẸO · SỬA CODE CÓ CẦN BUILD LẠI? Không. app/ và static/ được bind-mount. Đổi file .py thì docker compose restart lotus_profile_dashboard (uvicorn không tự nạp lại); đổi html/js/css thì chỉ cần F5. Chỉ khi đổi Dockerfile hay requirements.txt mới phải up -d --build. |
Thuộc tính |
Giá trị |
Tên / image |
lotus_profile_broker · lotus/profile_broker:0.1 |
IP / cổng |
114.114.114.37 · 20370:8080 — chỉ trong LAN/VPN |
Mount kho |
nas2_profiles_rw:/nas — ghi được, tài khoản WP2_GPM_Profile_RW |
Mount công cụ |
${REPO_ROOT}/docs/tools/profile_store.py:/app/profile_store.py:ro |
MQTT |
B37_MQTT_HOST/PORT/USER/PASS lấy từ khoá MQTT_BROKER_*_SGP trong .env (VPS 128.199.200.17:1883) |
MySQL |
B37_MYSQL_HOST=114.114.114.2, PORT=3306, USER=bot, DB=G01_GREAT_LOTUS_D02_Port_Pool, TABLE=G01_GREAT_LOTUS_D03_GPM_Profile; mật khẩu B37_MYSQL_PASS=${B37_MYSQL_PASS} lấy từ .env |
Bảng WP → topic |
B37_WP_TOPICS, mặc định {"WP3":"WP3-PC1"} |
Bắt mạch worker |
B37_PROBE_TIMEOUT mặc định 6 s |
Kiểm thử |
149 testcase: bash B37_Profile_Broker/verify/run_verify.sh |
B36 có tài liệu tương tác tự sinh tại /docs (Swagger), /redoc và /openapi.json. Toàn bộ endpoint, đọc thẳng từ app/server.py:
Phương thức |
Đường dẫn |
Trả về / việc |
Cache |
GET |
/api/health |
{ok, mounted, store, server_time} |
không |
GET |
/api/snapshot |
profiles[] + summary + work_pools + quarantine[] |
2 s |
GET |
/api/summary |
chỉ tổng hợp + phân theo WP |
2 s |
GET |
/api/profiles?wp=&status=&q= |
danh sách có lọc |
2 s |
GET |
/api/profiles/{id} |
một profile, 404 nếu không có |
2 s |
GET |
/api/locks |
{running[], stale_lock[]} |
2 s |
GET |
/api/quarantine |
{count, quarantine[], profiles[]} |
2 s |
GET |
/api/stats |
series.{day,week,month} + windows.{day,week,month} |
30 s |
GET |
/api/runs?limit=200 |
sự kiện gần nhất từ runs.jsonl (1–5000) |
không |
GET |
/api/mysql |
sổ A4 + phân loại, lấy qua B37 |
không |
GET |
/api/broker |
/health của B37 (có wp_topics) |
không |
GET |
/api/workers?timeout= |
bắt mạch song song mọi WP qua B37 |
không |
POST |
/api/command/{lệnh} |
chuyển tiếp sang B37: restore, kill_chrome, release_lock, delete_nas, cleanup, purge |
không |
GET |
/, /help, /static/* |
giao diện |
— |
GHI CHÚ · THỜI GIAN CHỜ KHI CHUYỂN LỆNH B36 chờ B37 tối đa 140 s cho mỗi lệnh, riêng cleanup 220 s — cố ý dài hơn timeout MQTT của B37 (cleanup 180 s), để B36 không bỏ cuộc trước khi worker kịp trả lời. B37 tắt hay không tới được thì B36 trả 502 kèm B37 không phản hồi: …. |
Hình 4.1. Cây thư mục của share lotus_profiles.
Cây do profile_store.py init tạo (SUBDIRS = profiles, incoming, quarantine, _index, _locks, _uid, _deleted). Trên kho thật ngày 18/09 còn có _leases/ rỗng — tàn tích của phiên bản B37 đầu tiên từng tự dựng hệ khoá riêng rồi bỏ vì gây split-brain. Chỉ có một hệ khoá: _locks/.
Trích nguyên văn một bản ghi trong kho ngày 18/09/2026:
"UID_000011_GPM_Profile": { "size": 5191680, "sha256": "21c263c8cf097bfc4685f98b69378411311535703114e459f1ba502fbce4bc45", "status": "active", "mtime": "2026-08-15T02:49:24Z", "last_host": "WP3-PC1", "work_pool": "", "runs": 3, "last_run_time": "2026-08-15T02:49:22Z", "last_run_machine": "WP3-PC1", "last_run_duration_sec": 0, "cache_limit_mb": 50, "last_actual_cache_mb": 0.0, "first_seen": "2026-08-15T02:49:14Z" } |
Trường |
Ý nghĩa |
Hiện ở giao diện |
size, sha256 |
kích thước và băm của .tar lúc push |
cột Cỡ; sha256 trong ngăn kéo (16 ký tự đầu) |
status |
active hoặc quarantined |
góp phần suy trạng thái |
mtime |
lần kho được cập nhật |
Cập nhật kho |
work_pool |
env LOTUS_WORK_POOL; thiếu thì giữ giá trị cũ, rồi suy từ tên máy WPn |
cột WP (rỗng thì B36 đoán từ tên máy) |
runs |
tổng số lần push |
cột Lần chạy |
last_run_* |
thời điểm, máy, thời lượng lần cuối |
Chạy gần nhất, Thời lượng |
cache_limit_mb, last_actual_cache_mb |
trần cache AIMD và cache đo được |
cột Cache |
quarantine_* |
chỉ có khi bị cách ly: thời điểm, lý do, tên file |
bảng cách ly |
LƯU Ý · WORK_POOL RỖNG Cả hai bản ghi thật có work_pool: "" vì biến LOTUS_WORK_POOL chưa được đặt trên WP3-PC1. B36 vẫn hiện đúng WP3 nhờ đoán từ tên máy (_HOST_WP trong store_reader.py: chuỗi _WP3/wp3 → WP3). Tên máy không khớp mẫu nào thì hiện WP?. Từ 18/09/2026, profile_store.py tự lấy theo thứ tự: biến LOTUS_WORK_POOL → work_pool cũ trong manifest → chữ WPn trong tên máy (WP3-PC1 → WP3), nên các lần push sau sẽ không còn rỗng. Máy worker phải có bản profile_store.py mới (git pull) thì mới có tác dụng. |
# _index/runs.jsonl — dòng thật cuối cùng {"ts": "2026-08-15T02:49:22Z", "id": "UID_000011_GPM_Profile", "host": "WP3-PC1", "wp": "", "dur": 0, "cache_mb": 0.0, "size": 5191680}
# _locks/<id>.lock — định dạng (README B36 §4) {"host": "WP3_PC6", "owner": "WP3_PC6/pid12345", "acquired_at": "...", "acquired_ts": 1.7e9, "renewed_at": "...", "expires_ts": 1.7e9} |
B36 tính ttl_left_sec = expires_ts − bây giờ và expired = expires_ts ≤ bây giờ ngay lúc đọc; đó là con số lease còn … hiện ở cột Đang chạy ở.
Mẫu tên máy (regex, không phân biệt hoa thường) |
Workpool |
\bwp1\b, bắt đầu HP1, chứa NUC1, _WP1 |
WP1 |
\bwp2\b, chứa PC176, _WP2, MAC1 |
WP2 |
\bwp3\b, _WP3 |
WP3 |
không khớp |
? — hiển thị WP? |
Hình 5.1. B36/B37 ở WP1, kho ở WP2, worker hiện có ở WP3.
Máy |
Địa chỉ |
Vai trò với B36 |
HP1 |
172.16.10.220 (WP1) |
chạy Docker Compose: B16 Caddy, B36, B37 |
NAS2 (White NAS) |
172.16.20.201 (WP2), DSM :5000 |
share lotus_profiles trên /volume1 |
WP3-PC1 |
172.16.30.101 (WP3) |
worker: Process Manager + GPM, topic MQTT WP3-PC1 |
MQTT broker |
128.199.200.17:1883 (VPS ngoài) |
B37 ⇄ worker; phản hồi trên RES_<topic> |
Tài khoản |
Quyền |
Ai dùng |
Cất ở đâu |
WP2_GPM_Profile_RO |
chỉ đọc |
B36 (volume nas2_profiles_ro) |
.env: WP2_GPM_PROFILE_RO_USER/PASS |
WP2_GPM_Profile_RW |
đọc + ghi |
B37 (volume nas2_profiles_rw), mọi worker |
.env: WP2_GPM_PROFILE_RW_USER/PASS; trên worker: Windows Credential Manager (cmdkey) |
tài khoản admin DSM |
quản trị |
chỉ khi tạo share / tài khoản |
không nằm trong hệ |
ĐÚNG THIẾT KẾ · HAI LỚP CHẶN GHI Ở B36 Tài khoản RO và tuỳ chọn mount ro,file_mode=0444,dir_mode=0555. Đã kiểm trên mount thật (05/08/2026): RW ghi được, RO bị chặn ghi. |
HP1 — mọi mật khẩu NAS/MQTT ở DCP_PRODUCTION/.env (gitignored); compose chỉ tham chiếu ${…}.
Worker — GUI lưu bằng cmdkey /add:172.16.20.201 /user:… /pass:…; profile_store.py không tự xác thực, chỉ chạm vào đường dẫn đã sẵn sàng.
MySQL của B37 — từ 18/09/2026 compose chỉ còn B37_MYSQL_PASS=${B37_MYSQL_PASS}, giá trị nằm trong .env (trước đó ghi thẳng trong compose — mục 18.3).
Đăng nhập trang công khai — B36_WEB_USER / B36_WEB_PASS trong .env; Caddyfile chỉ giữ băm bcrypt (mục 6.6).
PHẦN III
KHỞI TẠO BAN ĐẦU & KẾT NỐI
Chương này dựng lại toàn bộ hệ từ một NAS trống và một HP1 chưa có B36/B37. Làm đúng thứ tự: mỗi bước dựa trên kết quả bước trước. Mỗi bước có một lệnh kiểm tra — chưa qua kiểm tra thì đừng sang bước sau.
Hình 6.1. Tám bước khởi tạo và bên thực hiện.
GHI CHÚ · HỆ ĐANG CHẠY THÌ BỎ QUA GÌ? Ngày 18/09/2026 bước 1–6 đã xong và đang chạy. Thêm một máy worker mới thì chỉ làm bước 7–8 (chương 7). Thêm một workpool mới thì thêm cả mục 7.6 (khai topic cho B37). |
1. Đăng nhập DSM của NAS2 tại http://172.16.20.201:5000 bằng tài khoản quản trị.
2. Control Panel > Shared Folder > Create: tên lotus_profiles, đặt trên /volume1.
3. Bật SMB (Control Panel > File Services > SMB) nếu chưa bật; khuyến nghị tối thiểu SMB 3.
Share \\172.16.20.201\lotus_profiles hiện có được tạo ngày 05/08/2026 qua API SYNO.Core.Share của DSM với tài khoản quản trị — cách bằng giao diện ở trên cho cùng kết quả.
smbclient -L //172.16.20.201 -U <tài_khoản> # thấy lotus_profiles trong danh sách |
Tài khoản |
Quyền trên lotus_profiles |
Dùng cho |
WP2_GPM_Profile_RW |
Read/Write |
worker (push/pull/lease) và B37 |
WP2_GPM_Profile_RO |
Read only |
B36 và mọi công cụ kiểm kê |
LƯU Ý · ĐỪNG DÙNG MỘT TÀI KHOẢN CHO MỌI THỨ Tách đọc/ghi là lý do B37 tồn tại. Nếu B36 mount bằng tài khoản RW, một lỗi trong B36 đủ làm hỏng kho; còn với RO thì NAS từ chối mọi lệnh ghi dù mã có sai. |
# ghi mật khẩu vào file tạm quyền 600 — KHÔNG gõ mật khẩu lên dòng lệnh install -m 600 /dev/null /root/.nas2_test && printf 'username=WP2_GPM_Profile_RO\npassword=%s\n' "$PASS" > /root/.nas2_test mkdir -p /mnt/nas2_test mount -t cifs //172.16.20.201/lotus_profiles /mnt/nas2_test -o credentials=/root/.nas2_test,vers=3.0,ro touch /mnt/nas2_test/x # phải báo lỗi: tài khoản RO không được ghi umount /mnt/nas2_test && shred -u /root/.nas2_test |
Mount bằng tài khoản RW rồi chạy init một lần. Lệnh tạo đủ các thư mục chuẩn và một manifest.json rỗng; chạy lại cũng không hại gì.
mount -t cifs //172.16.20.201/lotus_profiles /mnt/lotus_profiles \ -o credentials=/root/.smbcred_rw,vers=3.0 python3 docs/tools/profile_store.py --store /mnt/lotus_profiles init python3 docs/tools/profile_store.py --store /mnt/lotus_profiles list # kho rỗng, không lỗi |
MẸO · THỨ TỰ THAM SỐ CỦA PROFILE_STORE.PY --store và --host là tham số toàn cục, phải đứng trước lệnh con: profile_store.py --store X --host Y push …. Đặt sau lệnh con thì argparse báo unrecognized arguments (đã kiểm chứng). Ngăn kéo chi tiết của B36 in đúng thứ tự này từ bản 1.8.1. |
Mở DCP_PRODUCTION/.env (gitignored). Các khoá B36/B37 dùng — tên khoá như dưới, giá trị điền theo tài khoản thật:
DCP_ROOT=<đường dẫn DCP_PRODUCTION> REPO_ROOT=<gốc repo PRODUCTION_HOST_PC> # B37 mount docs/tools/profile_store.py từ đây NAS2_PROFILE_SHARE=//172.16.20.201/lotus_profiles WP2_GPM_PROFILE_RO_USER=WP2_GPM_Profile_RO WP2_GPM_PROFILE_RO_PASS=<mật khẩu RO> WP2_GPM_PROFILE_RW_USER=WP2_GPM_Profile_RW WP2_GPM_PROFILE_RW_PASS=<mật khẩu RW> MQTT_BROKER_HOST_SGP=128.199.200.17 MQTT_BROKER_PORT_SGP=1883 MQTT_BROKER_USER_SGP=<user MQTT> MQTT_BROKER_PASS_SGP=<mật khẩu MQTT> |
Trong docker-compose.yml, khối volumes: khai hai volume CIFS — daemon Docker tự mount SMB, container không cần chạy privileged:
volumes: nas2_profiles_ro: driver: local driver_opts: type: cifs device: ${NAS2_PROFILE_SHARE} o: "username=${WP2_GPM_PROFILE_RO_USER},password=${WP2_GPM_PROFILE_RO_PASS},vers=3.0,ro, uid=0,gid=0,file_mode=0444,dir_mode=0555,nobrl" nas2_profiles_rw: # CHỈ B37 dùng driver: local driver_opts: type: cifs device: ${NAS2_PROFILE_SHARE} o: "username=${WP2_GPM_PROFILE_RW_USER},password=${WP2_GPM_PROFILE_RW_PASS},vers=3.0, uid=0,gid=0,file_mode=0644,dir_mode=0755,nobrl" |
(Trong file thật, chuỗi o: nằm trên một dòng; ở đây ngắt cho vừa trang.) Khối service B36 rút gọn:
lotus_profile_dashboard: container_name: lotus_profile_dashboard build: { context: ${DCP_ROOT}/B36_Profile_Dashboard } image: lotus/profile_dashboard:1.0 volumes: - /usr/share/zoneinfo/Asia/Ho_Chi_Minh:/etc/localtime:ro - /usr/share/zoneinfo:/usr/share/zoneinfo:ro - ${DCP_ROOT}/B36_Profile_Dashboard/app:/app/app:ro - ${DCP_ROOT}/B36_Profile_Dashboard/static:/app/static:ro - nas2_profiles_ro:/nas:ro environment: - TZ=Asia/Ho_Chi_Minh - PROFILE_STORE_DIR=/nas - SNAPSHOT_CACHE_SEC=2 - B37_URL=http://114.114.114.37:8080 ports: ["20360:8080"] restart: unless-stopped networks: { GreatLotus_Net: { ipv4_address: '114.114.114.36' } } |
Khối B37 tương tự với nas2_profiles_rw:/nas, mount thêm ${REPO_ROOT}/docs/tools/profile_store.py:/app/profile_store.py:ro, các biến B37_MQTT_* và B37_MYSQL_* (mục 3.4), cổng 20370:8080, IP 114.114.114.37. Mật khẩu MySQL cũng tham chiếu .env như mọi mật khẩu khác: B37_MYSQL_PASS=${B37_MYSQL_PASS}.
NGUY HIỂM · MÚI GIỜ: ĐỦ BA DÒNG Mọi container mới phải có TZ=Asia/Ho_Chi_Minh và hai mount zoneinfo. Thiếu mount thứ hai trên image Alpine/musl thì giờ thành +0000. Đổi múi giờ phải up -d (tạo lại), restart không ăn. |
cd DCP_PRODUCTION docker compose config >/dev/null && echo CONFIG_OK # luôn validate trước khi up docker compose up -d --build lotus_profile_broker lotus_profile_dashboard curl -s http://127.0.0.1:20360/api/health # {"ok":true,"mounted":true,"store":"/nas","server_time":"..."} curl -s http://127.0.0.1:20370/health # {"ok":true,"service":"B37_profile_broker","store":"/nas","locks_active":0, # "wp_topics":{"WP3":"WP3-PC1"},"mysql":{"enabled":true,...}} |
LƯU Ý · MOUNTED: FALSE mounted chỉ đúng khi /nas/profiles và /nas/_index cùng tồn tại (StoreReader.mounted). Sai thì hoặc chưa chạy init (bước 3), hoặc volume CIFS mount hỏng — xem mục 20.1. |
# B16_Caddy/Caddyfile — khối hiện hành (từ 18/09/2026 có đăng nhập) profiles.lotus1104.synology.me { basic_auth { lotus <băm bcrypt — KHÔNG phải mật khẩu> } reverse_proxy lotus_profile_dashboard:8080 } |
1. Chọn mật khẩu, ghi vào .env cạnh compose dưới hai khoá B36_WEB_USER và B36_WEB_PASS để người vận hành tra lại được.
2. Tạo băm: docker exec lotus_caddy caddy hash-password --plaintext '<mật khẩu>', dán chuỗi $2a$14$… vào khối trên. Caddyfile chỉ giữ băm, không giữ mật khẩu.
3. Thêm khối vào B16_Caddy/Caddyfile, rồi docker exec lotus_caddy caddy validate --config /etc/caddy/Caddyfile phải in Valid configuration.
4. Chạy docker restart lotus_caddy — không dùng caddy reload (reload không nạp route mới).
5. Caddy tự xin chứng chỉ Let's Encrypt (HTTP-01). DNS *.lotus1104.synology.me là wildcard nên không cần khai tên miền mới.
6. Kiểm từ trong LAN: phải map tên miền về Caddy vì hairpin NAT chặn đường qua IP public (--host-resolver-rules=MAP profiles.lotus1104.synology.me 114.114.114.16 cho Chrome, hoặc curl --resolve profiles.lotus1104.synology.me:443:<IP Caddy>).
# kiểm tra (từ HP1; 114.114.114.16 là Caddy) curl -sk -o /dev/null -w '%{http_code}\n' --resolve profiles.lotus1104.synology.me:443:114.114.114.16 \ https://profiles.lotus1104.synology.me/ # 401 curl -sk -o /dev/null -w '%{http_code}\n' --resolve profiles.lotus1104.synology.me:443:114.114.114.16 \ -u "lotus:$B36_WEB_PASS" https://profiles.lotus1104.synology.me/api/health # 200 |
NGUY HIỂM · KHÔNG BỎ ĐĂNG NHẬP B36 có nút Xoá hẳn và Dọn dẹp, còn cổng 443 của WP1 mở ra Internet. Trước 18/09/2026 route này không có xác thực (mục 18.2). Truy cập trong LAN qua http://172.16.10.220:20360 không đi qua Caddy nên không hỏi mật khẩu — cổng đó không được forward ra ngoài. |
Bước 7 (nối worker) là cả chương 7. Sau khi xong, kiểm toàn tuyến:
# |
Lệnh / thao tác |
Kết quả đúng |
1 |
curl -s :20360/api/health |
mounted: true |
2 |
curl -s :20360/api/broker |
ok: true và wp_topics có WP của máy mới |
3 |
curl -s :20360/api/workers |
alive: true kèm rtt_ms vài chục tới vài trăm |
4 |
curl -s :20360/api/mysql |
ok: true; orphan_free rỗng |
5 |
chạy một workflow pull → mở → đóng → push |
profile hiện trên bảng, Lần chạy tăng 1 |
6 |
mở https://profiles.lotus1104.synology.me/ |
chấm NAS xanh, chấm Live xanh |
Thứ cần có |
Ở đâu |
Kiểm tra |
docs/tools/profile_store.py |
trong repo trên worker (git-tracked) |
python docs\tools\profile_store.py --help |
Library/B05_GPM_Login_Manager/profile_nas_sync.py |
gitignored ⇒ chép bằng scp |
so sha256 hai đầu |
Process Manager APP_5 có pull_profile/push_profile/restore_profile/ping |
Application/APP_5_…/_core/ (gitignored) |
gửi manager.ping, không thấy Unsupported function |
Credential NAS RW |
Windows Credential Manager |
cmdkey /list:172.16.20.201 |
Thư mục profile GPM |
mặc định C:\ZGPM_Loaded_Profiles |
tồn tại, GPM đang dùng |
LƯU Ý · FILE GIT-TRACKED VẪN CÓ THỂ THIẾU Vụ 12/08/2026: profile_store.py là file tracked nhưng worker checkout cũ nên không có. Luôn kiểm tồn tại thật trên worker; với file gitignored: so hash → scp → xoá __pycache__ → khởi động lại một instance → probe (Phụ lục C). |
Từ 10/08/2026, cấu hình NAS nằm trong GUI Process Manager thay cho khối set LOTUS_PROFILE_* trong .bat. Khi bấm Kết nối hoặc Apply, GUI đặt lại os.environ tương ứng nên profile_nas_sync.py (đọc biến môi trường lúc chạy) dùng đúng giá trị.
Hình 7.1. Tab Main, mục NAS Profile Connection (chưa cấu hình — ô hiện chữ gợi ý).
# |
Thành phần |
Biến môi trường |
Giá trị đúng cho WP3 |
Ghi chú |
1 |
Bật đồng bộ NAS |
LOTUS_PROFILE_NAS_SYNC |
tick nếu muốn hook tự động |
bật: tự pull trước khi mở, push sau khi đóng. Node C12 pull/push vẫn chạy độc lập |
2 |
Profile Store (UNC) |
LOTUS_PROFILE_STORE |
\\172.16.20.201\lotus_profiles |
UNC hoặc thư mục đã mount |
3 |
NAS Username |
— |
WP2_GPM_Profile_RW |
phải là tài khoản ghi được |
4 |
NAS Password |
— |
mật khẩu RW |
chỉ dùng để lưu qua cmdkey; ô hiện • |
5 |
Local Profile Dir |
LOTUS_GPM_PROFILE_DIR |
C:\ZGPM_Loaded_Profiles |
thư mục GPM giữ folder profile; bấm Kết nối sẽ tự tạo nếu chưa có |
6 |
Kết nối / Test NAS |
— |
— |
tạo Local Dir → cmdkey /add → thử os.path.isdir(store) |
7 |
Đèn + dòng trạng thái |
— |
xanh NAS OK: <store> |
đỏ: Không truy cập được kho: … (kiểm tra user/mật khẩu/mạng) |
GHI CHÚ · VỀ ẢNH CHỤP Ảnh chụp bằng chính mã GUI chạy trên màn hình X của B35 (Linux) nên các ô NAS để trống và hiện chữ gợi ý. Trên worker Windows đã cấu hình, đèn thứ ba ở góc phải trên cùng đổi thành NAS: đã kết nối. |
MẸO · NÚT CONNECT CŨNG KẾT NỐI NAS Nút Connect lớn ở dưới cùng kết nối MQTT/Fleet và tự chạy Kết nối / Test NAS nếu đã điền đủ Store + Username + Local Dir. NAS chạy ở luồng nền riêng nên không chặn MQTT. |
Hình 7.2. Tab Advanced, mục NAS Sync Options (cuộn xuống cuối).
# |
Ô |
Biến |
Khuyến nghị |
1 |
Profile Folder Glob |
LOTUS_GPM_PROFILE_GLOB |
{id} là mặc định; đổi sang {id}-* nếu GPM đặt thư mục có hậu tố ngày. Từ 14/08 pull/push hỏi thẳng GPM profile_path nên ô này chỉ còn là đường lui |
2 |
Host Label (lease/audit) |
LOTUS_PROFILE_HOST |
để trống = tên máy. Đặt đúng tên máy (VD WP3-PC1) vì chuỗi này hiện ở cột Đang chạy ở và được dùng để đoán WP |
3 |
Lease TTL (sec) |
LOTUS_PROFILE_LEASE_TTL |
300 (khớp .bat và placeholder node C12) |
4 |
Lease Renew (sec) |
LOTUS_PROFILE_LEASE_RENEW_SEC |
để trống = TTL/3 |
GHI CHÚ · BIẾN CHƯA CÓ Ô TRÊN GUI: LOTUS_WORK_POOL GUI chưa có ô cho biến này, nên hai profile thật trên kho đang có work_pool rỗng. Từ 18/09/2026 profile_store.py tự suy WP khi thiếu biến: giữ giá trị cũ trong manifest, hoặc lấy chữ WPn trong tên máy (WP3-PC1 → WP3). Máy có tên không chứa WPn thì vẫn đặt trong .bat: set LOTUS_WORK_POOL=WP3. Bản profile_store.py mới đến worker bằng git pull. |
rem 2_GPM_multi_remote_browser_process_manager_gui.bat (dùng setlocal -> python kế thừa env) set LOTUS_PROFILE_NAS_SYNC=1 set LOTUS_PROFILE_STORE=\\172.16.20.201\lotus_profiles set LOTUS_GPM_PROFILE_DIR=C:\ZGPM_Loaded_Profiles set LOTUS_PROFILE_HOST=WP3-PC1 set LOTUS_PROFILE_LEASE_TTL=300 set LOTUS_WORK_POOL=WP3 rem credential: MỘT LẦN, không nhúng mật khẩu vào .bat cmdkey /add:172.16.20.201 /user:WP2_GPM_Profile_RW /pass |
.bat hiện hành còn có khoá một instance: PowerShell giết mọi Process Manager cũ theo tên script trước khi mở cái mới. Hai instance cùng nghe một topic là nguồn gốc lỗi Unsupported function push_profile ngày 10/08 (Phụ lục C).
curl -s http://127.0.0.1:20360/api/workers # worker sống: # {"ok":true,"probe_timeout_sec":6.0,"workers":{"WP3":{"alive":true,"rtt_ms":...}}} # worker tắt (thực tế ngày 18/09/2026): # {"WP3":{"topic":"WP3-PC1","alive":false,"rtt_ms":6138,"error":"timeout"}}
# probe tay theo đúng topic (công cụ git-tracked): GPM_MQTT_TOPIC=WP3-PC1 python3 docs/tools/mqtt_send.py '<action_json>' '<inputs_json>' |
LƯU Ý · TIMEOUT CHƯA CHẮC LÀ MÁY CHẾT Kiểm topic trước: WP3-PC1 nghe WP3-PC1, không phải WP2_PC176 (topic cũ vẫn còn trong vài tài liệu và làm mặc định của mqtt_send.py). Rồi kiểm có đúng một Process Manager đang chạy. Xem Phụ lục D. |
1. Dựng worker theo 7.1–7.5 và đọc topic THẬT trong config đang chạy trên máy đó (GPM_multi_remote_browser_process_manager_gui_config.json > values.subscribe_topics).
2. Khai cho B37 trong compose: B37_WP_TOPICS={"WP3":"WP3-PC1","WP2":"<topic thật>"}.
3. docker compose up -d lotus_profile_broker (biến môi trường cần tạo lại container).
4. curl -s :20360/api/broker — wp_topics phải có WP mới.
5. Mở hộp thoại Dọn dẹp: WP mới được tick sẵn và hiện worker trả lời sau … ms.
GHI CHÚ · KHI B37 CHƯA BIẾT TOPIC CỦA MỘT WP Hộp thoại Dọn dẹp vẫn liệt kê WP đó (nếu kho có profile thuộc nó) nhưng không tick sẵn, kèm dòng B37 chưa có topic MQTT cho … — gửi lệnh xuống topic không ai nghe chỉ làm người dùng ngồi chờ hết timeout. |
Node greatLotusPyAutoWeb (thư viện C12) có hai action đặt trên cùng nhóm Manager — manager.pull_profile (hiển thị Pull_Profile_from_NAS) và manager.push_profile (Push_Profile_to_NAS). Cả hai không chạm GPM API: chỉ bảo worker chạy profile_store.py.
Action |
Tham số |
Mặc định / ghi chú |
Pull |
profile_id (bắt buộc) |
ID dạng UID_xxxxxx_GPM_Profile |
Pull |
dest_dir |
trống = LOTUS_GPM_PROFILE_DIR trên worker |
Pull |
acquire_lease |
bật: giành lease + heartbeat; node Push sẽ nhả |
Pull |
host, lease_ttl, store |
trống = tên máy / 300 s / LOTUS_PROFILE_STORE |
Push |
profile_id (bắt buộc) |
— |
Push |
profile_dir |
trống = tự dò theo LOTUS_GPM_PROFILE_DIR + glob |
Push |
duration, actual_cache_mb |
nên điền — để trống thì lịch sử ghi dur 0, cache 0 |
Push |
release_lease |
bật: nhả lease sau khi push |
LƯU Ý · VÌ SAO CỘT THỜI LƯỢNG VÀ CACHE TOÀN SỐ 0 Hai tham số duration và actual_cache_mb của Push không được workflow hiện tại điền ⇒ runs.jsonl ghi dur: 0, cache_mb: 0.0 ⇒ biểu đồ và cột Thời lượng mất ý nghĩa, AIMD không chạy (hạn mức kẹt 50 MB). Truyền thời gian mở→đóng và cache đo được vào hai ô này. |
1. Pull Profile (lease bật) — tải .tar mới nhất, xả vào thư mục GPM thật, trả dest_dir và addination_args (đọc từ config.json trong gói).
2. Open GPM Profile — node tự đọc config.json (load_args_from_config mặc định bật) để ghép --disk-cache-size, --media-cache-size=1, --disable-pdf-viewer.
3. Các bước tự động hoá.
4. Close Profile — để Chrome flush xong dữ liệu.
5. Push Profile (release bật) — đóng gói, upload nguyên tử, nhả lease.
NGUY HIỂM · PROXY KHÔNG ĐI THEO LỆNH MỞ open_profile gọi GET /profiles/start/<id> của GPM, không nhận raw_proxy. Proxy là thuộc tính lưu sẵn của profile. Muốn đổi proxy phải update_profile (hoặc create_profile) có raw_proxy trước khi mở. |
Gọi create_profile với profile_name để trống (hoặc AUTO): worker tự xin UID_… qua profile_store uid next. Từ 14/08/2026 worker còn bỏ qua mọi tên caller gửi và luôn xin UID — trừ khi tên đã đúng chuẩn UID (đường khôi phục) hoặc caller bật keep_name=true. Xin UID thất bại thì không tạo profile.
// phản hồi create_profile — id chính danh nằm ngay tầng ngoài { "manager_action": "create_profile", "profile_id": "UID_000003_GPM_Profile", "lotus_id": "UID_000003_GPM_Profile", "gpm_id": "8a9f3f11-…", "gpm_http": { … } } |
Workflow IP_Pool → GPM Profile → NAS (WP2) (id Lv1RQyThF9U9cT8Q, đang INACTIVE) đọc data.profile_id rồi mang đúng chuỗi đó đi push NAS, ghi MySQL (nhớ kèm link_email = JSON_ARRAY() vì cột NOT NULL) và mở/đóng profile.
LƯU Ý · TẠO BẰNG GPM GUI THÌ KHO KHÔNG BIẾT Profile tạo tay trong phần mềm GPM không đi qua worker nên không được đẩy lên NAS — đó là lý do "tạo profile mà NAS vẫn rỗng". Tên tự đặt cũng không mang con dấu UID nên Dọn dẹp sẽ không tự xử. |
PHẦN IV
GIAO DIỆN — MỔ XẺ TỪNG THÀNH PHẦN
Trang chính là một trang đơn (SPA) viết bằng JavaScript thuần, không thư viện ngoài. Mở http://172.16.10.220:20360/ trong LAN hoặc https://profiles.lotus1104.synology.me/. Trang có sáu vùng, từ trên xuống:
Hình 9.1. Toàn trang ở nền tối, dữ liệu thật ngày 18/09/2026.
# |
Vùng |
Nội dung |
Mục |
1 |
Thanh đầu trang |
tên, đường dẫn kho, đèn NAS/Live, 6 nút |
9.2 |
2 |
Ô tổng hợp |
5 ô đếm theo trạng thái — bấm để lọc |
9.3 |
3 |
Thanh lọc |
chip Workpool, chip trạng thái, ô tìm |
9.4 |
4 |
Bảng Profiles |
một dòng mỗi profile, 10 cột |
9.5 |
5 |
Thống kê sử dụng |
biểu đồ + 3 bảng xếp hạng |
chương 10 |
6 |
Chân trang |
nhắc nguồn dữ liệu, liên kết Hướng dẫn và /docs |
9.9 |
GHI CHÚ · TỰ LÀM MỚI Trang gọi /api/snapshot mỗi 4 giây và /api/stats mỗi 60 giây (REFRESH_MS, STATS_MS trong app.js). Server gộp các lần gọi trong 2 s (snapshot) và 30 s (stats), nên mở nhiều tab cũng không quét SMB dồn dập. |
Hình 9.2. Thanh đầu trang — 11 thành phần.
# |
Thành phần |
Công dụng |
Lưu ý |
1 |
Tên Lotus Profile Fleet |
nhận diện trang; tab trình duyệt ghi Lotus Profile Fleet — NAS2 |
— |
2 |
Dòng đường dẫn kho |
\\172.16.20.201\lotus_profiles |
chữ cố định trong HTML, không đọc từ cấu hình |
3 |
Đèn NAS |
xanh = kho đã mount (mounted: true); đỏ = chưa mount |
đỏ kèm dải báo lỗi (mục 14.4) |
4 |
Đèn Live |
xanh = lần gọi gần nhất thành công |
đổi chữ Tạm dừng khi dừng; đỏ + lỗi mạng khi gọi hỏng |
5 |
cập nhật kho: … trước |
tuổi của manifest.updated — lần ghi kho cuối |
34 ngày trước = không có push nào từ 15/08 |
6 |
Nút chổi — Dọn dẹp |
mở hộp thoại dọn dữ liệu mồ côi, lấy NAS làm chuẩn |
chương 13 |
7 |
Nút sách — Hướng dẫn đồng bộ |
mở /static/manual.html |
mục 15.2 |
8 |
Nút dấu hỏi — Trợ giúp |
mở /help |
mục 15.1 |
9 |
Nút bánh răng — API |
mở /docs (Swagger) |
mục 15.3 |
10 |
Nút tạm dừng / tiếp tục |
ngừng hoặc tiếp tục tự làm mới |
mục 9.6 |
11 |
Nút mặt trời / mặt trăng |
đổi nền sáng/tối |
nhớ lựa chọn trong localStorage (b36-theme) |
Hình 9.3. Năm ô tổng hợp — mỗi ô là một nút lọc.
# |
Ô |
Số hiển thị |
Dòng phụ |
Bấm vào |
1 |
Tổng profile |
summary.total |
tổng dung lượng các .tar |
bỏ lọc trạng thái |
2 |
Đang chạy |
summary.running |
bấm để lọc |
lọc running |
3 |
Sẵn sàng |
summary.idle |
bấm để lọc |
lọc idle |
4 |
Cách ly (lỗi) |
summary.quarantined |
bấm để lọc |
lọc quarantined |
5 |
Khoá treo |
summary.stale_lock |
lease hết hạn — cần chú ý |
lọc stale-lock |
Bấm ô đang được chọn lần nữa thì bỏ lọc; ô đang lọc có viền sáng. Sau khi bấm, trang tự cuộn xuống bảng.
GHI CHÚ · KHÔNG CÓ Ô THIẾU FILE Trạng thái Thiếu file không có ô riêng; nó chỉ xuất hiện dưới dạng chip lọc khi có ít nhất một profile ở trạng thái đó (hình 14.1). |
Hình 9.4. Chip Workpool, chip trạng thái, ô tìm, bộ đếm dòng.
# |
Thành phần |
Công dụng |
Lưu ý |
1 |
Tất cả WP n |
bỏ lọc Workpool |
số = tổng profile |
2 |
Chip WP (ở đây: WP3 2) |
chỉ hiện profile của một WP |
chỉ có chip cho WP đang có profile; WP không rõ hiện WP? |
3 |
Mọi trạng thái |
bỏ lọc trạng thái |
— |
4 |
Đang chạy n |
lọc running |
luôn hiện, kể cả khi bằng 0 |
5 |
Sẵn sàng n |
lọc idle |
luôn hiện; các chip Cách ly/Khoá treo/Thiếu file chỉ hiện khi > 0 |
6 |
Ô Lọc theo id / máy… |
lọc theo chuỗi con, không phân biệt hoa thường |
so với id, máy chạy gần nhất, và máy đang giữ lease |
7 |
(n) cạnh chữ Profiles |
số dòng đang hiện sau khi lọc |
— |
8 |
Dòng gợi ý |
bấm một dòng để xem chi tiết … tick ô vuông để xóa hàng loạt |
— |
Hình 9.5. Gõ 000011 vào ô tìm: còn 1 dòng.
Hình 9.6. Bấm ô Đang chạy khi không có profile nào chạy: bảng báo không có profile khớp.
Hình 9.7. Đầu bảng: ô chọn tất cả và chín cột dữ liệu.
# |
Cột |
Nguồn |
Cách hiển thị |
1 |
Ô chọn (đầu bảng) |
— |
chọn/bỏ chọn mọi dòng đang hiện; nửa chọn khi chỉ tick một phần |
2 |
Profile |
id |
chữ đơn cách, xanh; bấm dòng mở ngăn kéo |
3 |
WP |
work_pool hoặc đoán từ tên máy |
huy hiệu màu: WP1 xanh dương, WP2 xanh lá, WP3 vàng |
4 |
Trạng thái |
suy ra (mục 2.5) |
huy hiệu màu; rê chuột thấy câu giải thích |
5 |
Đang chạy ở |
lock |
tên máy + lease còn Xm Ys; khoá treo: chữ vàng lease HẾT HẠN; không có: — |
6 |
Cỡ |
kích thước .tar trên đĩa |
B / KB / MB / GB |
7 |
Lần chạy |
runs |
số nguyên, — nếu chưa có |
8 |
Chạy gần nhất |
last_run_time |
tương đối (34 ngày trước); rê chuột thấy giờ UTC |
9 |
Thời lượng |
last_run_duration_sec |
s, m s, h m |
10 |
Cache |
last_actual_cache_mb / cache_limit_mb |
0/50 MB; dấu ⓘ mở /help#cache |
Sắp xếp: bấm tiêu đề cột để sắp tăng, bấm lần nữa để giảm. Mặc định sắp theo Profile tăng dần. Giá trị trống luôn dồn về một phía (chữ coi như ~, số coi như -1).
Hình 9.8. Bấm hai lần tiêu đề Chạy gần nhất: sắp mới nhất lên trên.
GHI CHÚ · DẢI TRỐNG PHÍA TRÊN ĐẦU BẢNG (ĐÃ SỬA) Trước bản 1.8.1, một dải mỏng nằm giữa dòng Profiles và đầu bảng: đó là thanh chọn-nhiều bị ẩn không trọn vì CSS .bulk-bar{display:flex} thắng thuộc tính hidden. Ngày 18/09/2026 đã thêm .bulk-bar[hidden]{display:none!important}; giờ thanh chỉ xuất hiện khi có dòng được tick (mục 9.8). Còn thấy dải trống thì trình duyệt đang giữ CSS cũ — tải lại trang bằng Ctrl+F5. |
Hình 9.9. Đang tạm dừng: đèn Live đổi chữ, nút thành dấu tam giác.
Tạm dừng hữu ích khi đang đọc một dòng mà không muốn bảng nhảy. Chỉ dừng /api/snapshot; thống kê vẫn tự làm mới mỗi 60 giây. Bấm lại để tiếp tục — trang gọi ngay một lượt.
Hình 9.10. Cùng trang ở nền sáng.
Hình 9.11. Tick ô đầu bảng: cả hai dòng được chọn, thanh hàng loạt hiện ra.
# |
Thành phần |
Công dụng |
1 |
Ô chọn tất cả |
tick mọi dòng đang hiện theo bộ lọc |
2 |
Dòng đã chọn |
nền vàng nhạt |
3 |
Đã chọn n profile |
bộ đếm |
4 |
Xoá hẳn (A1 (GPM)→A5 (WebProfile)) |
mở hộp thoại xoá hàng loạt (mục 12.5) |
5 |
Bỏ chọn |
xoá lựa chọn |
MẸO · TICK Ô KHÔNG MỞ NGĂN KÉO Ô tick nằm trong dòng có sự kiện mở ngăn kéo; mã chặn nổi bọt (stopPropagation) nên tick chỉ chọn dòng. Bấm vào bất kỳ chỗ nào khác của dòng mới mở ngăn kéo. |
Nhắc nguồn dữ liệu (_index/manifest.json + _index/runs.jsonl + _locks/ + quarantine/) và hai liên kết Hướng dẫn & API (/help) và /docs. Từ bản 1.8.1 câu này ghi "đọc kho NAS2 (mount chỉ đọc); hành động ghi đi qua B37" — thay cho câu cũ "không sửa gì trên kho" vốn chỉ đúng với riêng B36.
Hình 10.1. Chế độ Ngày: 30 cột, mỗi cột một ngày.
# |
Thành phần |
Công dụng |
1 |
Ngày / Tuần / Tháng |
đổi cửa sổ: 30 ngày gần nhất · 12 tuần ISO · 12 tháng |
2 |
Dòng tóm tắt |
n lần chạy <hôm nay / 7 ngày qua / 30 ngày qua> · Σ thời lượng · tổng lịch sử N lần |
3 |
Chú giải |
màu WP1, WP2, WP3 và xám WP? (chưa biết) |
4 |
Biểu đồ cột |
số lần push mỗi khoảng, xếp chồng theo WP; rê chuột thấy nhãn · WP: n lần |
5 |
Profile dùng nhiều nhất |
top 10 trong cửa sổ; bấm một dòng mở ngăn kéo của profile đó |
6 |
Theo Workpool |
số lượt của WP1/WP2/WP3 |
7 |
Máy chạy nhiều nhất |
top 10 tên máy |
Một lần chạy là một dòng trong runs.jsonl, tức một lần push. Cửa sổ hôm nay tính theo ngày UTC (store_reader.stats() dùng datetime.now(timezone.utc).date()), nên từ 00:00 tới 07:00 giờ Việt Nam, "hôm nay" vẫn là hôm qua theo lịch Việt Nam.
Hình 10.2. Chế độ Tuần: 12 tuần ISO gần nhất (nhãn W27…W38).
Hình 10.3. Chế độ Tháng: dữ liệu tập trung ở 07/26 và 08/26.
Chế độ Ngày và Tuần trống vì không có push nào trong 30 ngày qua. Chế độ Tháng cho thấy hai cột lớn ở 07/26 và 08/26 — phần lớn là dữ liệu demo: runs.jsonl có 832 sự kiện, trong đó 776 sự kiện (id p_WP…) được bơm ngày 06/08 để minh hoạ giao diện; phần kho demo đã được dọn nhưng sổ lịch sử thì chưa. Phần màu xám là sự kiện thật có wp rỗng.
GHI CHÚ · MÀU XÁM TRONG CHÚ GIẢI Biểu đồ vẽ nhóm ? bằng màu xám #8b949e. Trước bản 1.8.1 chú giải chỉ liệt kê WP1–WP3; nay có thêm mục WP? (chưa biết). Xám = sự kiện push ghi wp rỗng. Từ 18/09/2026 profile_store.py tự suy WP từ tên máy, nên các lần push mới sẽ không còn rơi vào nhóm này. |
MẸO · MUỐN BIỂU ĐỒ SẠCH Lọc bỏ các dòng id p_WP* khỏi _index/runs.jsonl (sao lưu trước; cần mount RW) — mục 17.4. |
Bấm một dòng của bảng Profiles, bảng Cách ly hoặc danh sách Profile dùng nhiều nhất. Ngăn kéo trượt ra bên phải; đóng bằng nút ✕, phím Esc hoặc bấm vùng tối bên trái. Nội dung tự làm mới theo nhịp 4 giây; nếu profile biến mất khỏi kho, ngăn kéo tự đóng.
Hình 11.1. Ngăn kéo của một profile Sẵn sàng (dữ liệu thật).
# |
Thành phần |
Công dụng |
1 |
ID profile |
tiêu đề ngăn kéo |
2 |
Huy hiệu WP + trạng thái |
cùng màu với bảng |
3 |
Nút ✕ |
đóng ngăn kéo |
4 |
Khôi phục về worker |
đem bản NAS về máy worker của WP — mục 12.1 |
5 |
Xoá khỏi kho → Cách ly |
chuyển .tar sang quarantine/ — mục 12.3 |
6 |
Xoá hẳn (mọi nơi) |
xoá ở A1…A5 — mục 12.5 |
7 |
Hộp hướng dẫn |
giải thích trạng thái + việc cần làm; với vài trạng thái có kèm lệnh CLI |
Hình 11.2. Phần dưới ngăn kéo: 11 dòng chi tiết và ghi chú chân.
Dòng |
Lấy từ |
Ví dụ thật |
Trạng thái |
suy ra |
Sẵn sàng |
Workpool |
work_pool |
WP3 |
Kích thước .tar |
kích thước file |
5.0 MB |
Số lần chạy |
runs |
3 |
Chạy gần nhất |
last_run_time |
2026-08-15 02:49:22 UTC (34 ngày trước) |
Máy chạy gần nhất |
last_run_machine |
WP3-PC1 |
Thời lượng lần cuối |
last_run_duration_sec |
0s |
Cache (thực/hạn mức) |
last_actual_cache_mb / cache_limit_mb |
0 / 50 MB |
Lần đầu thấy |
first_seen |
2026-08-15 02:49:14 UTC |
Cập nhật kho |
mtime |
2026-08-15 02:49:24 UTC |
sha256 |
16 ký tự đầu |
21c263c8cf097bfc… |
Tập nút do hàm actionsFor() trong app.js quyết định. Nút Xoá hẳn luôn có — kể cả với Thiếu file, vốn là ca cần nó nhất (một dòng rác trên bảng).
Trạng thái |
Gỡ khoá treo |
Xử lý treo |
Khôi phục |
Xoá → Cách ly |
Xoá hẳn |
Sẵn sàng |
|
|
có |
có |
có |
Đang chạy |
|
có |
|
có |
có |
Khoá treo |
có |
có |
có |
có |
có |
Cách ly |
|
|
có |
|
có |
Thiếu file |
|
|
|
|
có |
Hình 11.3. Profile đang chạy trên WP3-PC1, lease còn 4 phút 40 giây (mô phỏng).
# |
Thành phần |
Ý nghĩa |
1 |
Khối hành động |
chỉ có Xử lý treo, Xoá → Cách ly, Xoá hẳn |
2 |
Xử lý treo (đóng) |
gửi close_profile xuống worker |
3 |
Hộp xanh |
bình thường không cần làm gì; chỉ can thiệp khi chắc máy đã treo |
Hình 11.4. Lease của WP3-PC2 đã hết hạn (mô phỏng).
# |
Thành phần |
Ý nghĩa |
1 |
Khối hành động |
đủ 5 nút: Gỡ khoá treo, Xử lý treo, Khôi phục, Xoá → Cách ly, Xoá hẳn |
2 |
Hộp vàng |
máy còn giữ lease nhưng đã hết hạn — có heartbeat nên treo = đã chết thật |
3 |
Ba bước |
kiểm máy trên Fleet Monitor → nếu máy khởi động lại thì tự đòi lại → nếu chết hẳn thì steal |
4 |
Khối lệnh + nút Copy |
lệnh CLI chạy trên máy sẽ tiếp quản — giải thích ở 11.6 |
Ngăn kéo in các lệnh tương đương cho thao tác tay, kèm nút Copy. Tới bản 1.8.0 các lệnh này sai cú pháp (--host đặt sau lệnh con, --dest/--dir không tồn tại) và profile_store.py từ chối cả ba. Bản 1.8.1 (18/09/2026) đã sửa; bảy lệnh dưới đây đã chạy thử trên một kho tạm, tất cả trả mã 0:
Việc |
Lệnh giao diện in (bản 1.8.1) |
Bản 1.8.0 in (bị từ chối) |
Xem ai giữ |
python profile_store.py --store "\\172.16.20.201\lotus_profiles" lease status <id> |
giống nhau |
Giành lại lease |
python profile_store.py --store "…" --host %COMPUTERNAME% lease steal <id> |
… lease steal <id> --host %COMPUTERNAME% |
Kéo về + giữ lease |
python profile_store.py --store "…" --host %COMPUTERNAME% pull <id> <thu_muc> --lease |
… pull <id> --dest <thu_muc> --lease |
Đẩy bản tốt lên |
python profile_store.py --store "…" --host %COMPUTERNAME% push <id> <thu_muc> |
… push <id> --dir <thu_muc> --host %COMPUTERNAME% |
Kiểm toàn vẹn / liệt kê |
… verify <id> · … list |
giống nhau |
LƯU Ý · LỆNH ĐÃ CHÉP TỪ BẢN CŨ Ai đã lưu lệnh chép từ giao diện trước 18/09/2026 (ghi chú, script, tin nhắn) thì kiểm lại: có --dest, --dir hoặc --host nằm sau lệnh con là bản cũ, sẽ báo unrecognized arguments. |
MẸO · XẢ THẲNG VÀO THƯ MỤC GPM pull <id> <dest> xả vào <dest>/<id>. Muốn xả thẳng vào thư mục GPM thật (tên dạng <mã>-<ngày>) thì thêm --into <thư mục GPM>. |
Hình 11.5. Profile bị cách ly kèm lý do (mô phỏng).
# |
Thành phần |
Ý nghĩa |
1 |
Khối hành động |
Khôi phục về worker + Xoá hẳn |
2 |
Hộp đỏ |
lý do (quarantine_reason) và tên file trong quarantine/ |
3 |
Bốn bước |
tải bản lỗi về xem → sửa được thì push đè → không cứu được thì tạo mới → verify |
4 |
Khối lệnh |
đường dẫn SMB tới bản lỗi + lệnh push/verify (sửa cú pháp như 11.6) |
Hình 11.6. Manifest có mà không thấy .tar (mô phỏng).
# |
Thành phần |
Ý nghĩa |
1 |
Khối hành động |
chỉ còn Xoá hẳn (mọi nơi) |
2 |
Hộp vàng |
dữ liệu lệch: xoá tay, upload hỏng, hoặc vừa bị cách ly |
3 |
Ba bước |
kiểm quarantine/ → có bản tốt thì push lại → bỏ thì để nguyên hoặc Dọn dẹp |
4 |
Khối lệnh |
list + push |
5 |
Nút Copy |
chép cả khối lệnh; đổi chữ Đã copy ✓ trong 1,4 giây |
Mọi nút hành động mở một hộp xác nhận trước. Hủy, phím Esc hoặc bấm ra ngoài đều đóng mà không gửi gì. Hộp thao tác phá huỷ có viền đỏ và nút Xác nhận xóa.
Hình 12.1. Đường đi của một nút bấm từ trình duyệt tới worker.
Hình 12.2. Hộp xác nhận Khôi phục.
# |
Thành phần |
Ý nghĩa |
1 |
Tiêu đề |
tên nút đã bấm |
2 |
Nội dung |
profile nào, về worker của WP nào; worker tạo lại profile trong GPM nếu A1 thiếu rồi tải data về |
3 |
Hủy |
đóng, không gửi |
4 |
Xác nhận |
gửi POST /api/command/restore {profile_id, work_pool} |
Worker thi hành restore_profile: pull kèm lease và xả vào đúng thư mục GPM (hỏi GPM profile_path, hoặc tra .lotus_profile_map.json). 99 % ca GPM còn entry nên chỉ cần pull; ca máy trắng thì tạo lại entry GPM — nhánh này còn nợ kỹ thuật (Phụ lục E).
ĐÚNG THIẾT KẾ · THÀNH CÔNG THẬT LÀ GÌ Với lệnh đi qua worker (Khôi phục, Xử lý treo), giao diện chỉ báo ✓ khi B37 nhận được phản hồi (ok: true) và worker trả return: "OK". Worker trả NG thì toast đỏ kèm lỗi; không ai trả lời thì toast worker không phản hồi (timeout). |
Hình 12.3. Sau khi xác nhận: nút chuyển Đang gửi… và bị khoá tới khi có kết quả.
Hình 12.4. Hộp xác nhận Xử lý treo (mô phỏng).
Gửi close_profile qua MQTT; worker đóng profile qua GPM API. Dùng khi profile treo, không phản hồi. Không cần remote_port — giao diện chỉ gửi profile_id và work_pool.
Hình 12.5. Hộp xác nhận Xoá khỏi kho (viền đỏ).
# |
Thành phần |
Ý nghĩa |
1 |
Tiêu đề |
Xóa khỏi kho → Cách ly |
2 |
Nội dung |
bản này chuyển vào cách ly — hồi được; cảnh báo A3 có thể là bản sao cuối cùng |
3 |
Hủy |
— |
4 |
Xác nhận xóa |
gửi delete_nas {profile_id, confirm: true}; B37 tự làm, không cần worker |
Hình 12.6. Hộp xác nhận Gỡ khoá treo (mô phỏng).
NGUY HIỂM · CHỈ GỠ KHI CHẮC MÁY ĐÃ CHẾT B37 xoá thẳng _locks/<id>.lock trên NAS. Gỡ nhầm khi máy kia vẫn chạy = mở đường cho hai máy cùng ghi (clobber) — bản push sau đè mất bản trước. Kiểm Fleet Monitor B34 trước. |
Hình 12.7. Xoá hẳn một profile từ ngăn kéo.
# |
Thành phần |
Ý nghĩa |
1 |
Tiêu đề |
Xóa hẳn n profile |
2 |
Hộp cảnh báo |
xoá ở MỌI NƠI: entry A1, thư mục A2, .tar A3, hàng A4, dòng A5 |
3 |
Danh sách theo WP |
profile gom theo Workpool; WP không rõ thì chỉ xoá được A3/A4 |
4 |
Xoá VĨNH VIỄN |
mặc định tắt: .tar sang _deleted/ (hồi được bằng tay trên NAS) |
5 |
Xoá cả profile đang chạy (force) |
mặc định tắt: bỏ qua chốt lock còn hạn |
6 |
Xác nhận xóa |
gửi purge {profile_ids[], work_pool, confirm, hard, force} — mỗi WP một bước |
Hình 12.8. Xoá hẳn hai profile đã tick trên bảng.
Thứ |
hard = tắt (mặc định) |
hard = bật |
profiles/<id>.tar |
→ _deleted/<id>.<ts>.purge.tar |
xoá thật |
quarantine/<id>.*.tar |
→ _deleted/ |
xoá thật |
manifest, _locks, _rescue |
xoá bản ghi |
xoá bản ghi |
hàng A4 (MySQL) |
xoá, sao lưu _deleted/mysql/<id>.<ts>.json |
như trái |
entry A1 + thư mục A2 |
worker xoá (trước kho) |
như trái |
GHI CHÚ · THỨ TỰ CÓ CHỦ Ý: WORKER TRƯỚC, KHO SAU Chết giữa chừng thì thứ còn lại là bản NAS — thứ cứu được. Worker không trả lời thì B37 hoãn (skipped) chứ không xoá phía kho, để không sinh đúng loại rác hệ này sinh ra để dọn. Hàng MySQL còn link_email không bị xoá trừ khi bật force. |
Nút chổi trên thanh đầu trang. Nó dọn những thứ không còn liên kết với kho: bản ghi mồ côi, khoá mồ côi, mảnh upload dở, hàng MySQL mồ côi, entry GPM và thư mục cục bộ không còn trên NAS. Luôn xem trước — không xoá gì cho tới khi bạn bấm Thực thi.
Hình 13.1. Luồng Dọn dẹp và ba phạm vi.
Hình 13.2. Ngay khi mở: đang bắt mạch worker.
Hộp thoại gọi /api/workers ngay khi mở (song song, khoảng 6 giây). Máy không trả lời bị bỏ tick sẵn kèm lý do — để bạn biết trước khi bấm, thay vì ngồi chờ hết timeout 180 giây của lệnh nặng.
Hình 13.3. Sau khi bắt mạch: WP3 không phản hồi nên bị bỏ tick.
# |
Thành phần |
Mặc định |
Ý nghĩa |
1 |
Tiêu đề |
— |
Dọn dẹp — bước 1: chọn phạm vi |
2 |
Giới thiệu |
— |
lấy A3 (NAS) làm chuẩn; luôn xem trước |
3 |
Dòng WP |
tick nếu B37 biết topic và worker sống |
quét A1 + A2 trên worker đó; chữ đỏ không phản hồi trong 6s = Process Manager tắt |
4 |
Dọn sổ A4 (MySQL) |
bật |
xoá hàng trỏ tới profile không còn trên NAS |
5 |
Xoá cả hàng MySQL CÒN email |
tắt |
email ↔ IP là dữ liệu nghiệp vụ, mặc định chỉ báo cáo |
6 |
Chỉ đụng profile mang con dấu UID |
bật |
tên khác chỉ báo cáo |
7 |
Xoá cả thư mục A2 không map được id |
tắt |
thư mục lạ quá ngưỡng tuổi |
8 |
Xoá cả thư mục A3 không biết id (force) |
tắt |
nguy hiểm: có thể là data chưa từng push |
9 |
Chỉ đụng thư mục A2 cũ hơn (giờ) |
24 |
0 = bỏ chốt tuổi |
10 |
Xem trước |
— |
chạy với apply: false |
GHI CHÚ · VÌ SAO CÓ CHỐT TUỔI 24 GIỜ Thư mục vừa ghi có thể là phiên vừa đóng mà push còn đang thử lại; xoá nó có khi là xoá bản duy nhất. Chốt tuổi được hỏi trước mọi luật khác. Trước khi có ô này, người dùng bấm dọn mà thư mục vẫn còn và không hiểu vì sao. |
Hình 13.4. Đang chạy bước 1/2 — mỗi phạm vi là một bước riêng.
# |
Thành phần |
Ý nghĩa |
1 |
Tiêu đề |
Đang quét (xem trước)… hoặc Đang dọn dẹp… |
2 |
Dòng tiến độ + đồng hồ |
Bước i/n — … và số giây đã trôi |
3 |
Thanh tiến độ |
đo bằng số bước đã xong, không bịa phần trăm |
4 |
Gợi ý |
mỗi worker thường 5–60 s |
5 |
Danh sách bước |
○ chờ · vòng quay đang chạy · ✓ xong · ✗ lỗi, kèm ghi chú |
6 |
Dừng chờ |
chỉ ngừng chờ ở trình duyệt; lệnh đã gửi vẫn chạy nốt dưới worker |
Trong lúc chạy, phím Esc và bấm ra ngoài bị vô hiệu — đóng nhầm là mất dấu lệnh đang chạy.
Hình 13.5. Kết quả xem trước thật ngày 18/09/2026: không có gì cần dọn.
# |
Thành phần |
Ý nghĩa |
1 |
Tiêu đề |
Dọn dẹp — bước 2: xem trước |
2 |
Dòng tổng |
số mục sẽ xoá ở kho/sổ, MySQL, GPM, thư mục; số giữ lại có chủ ý |
3 |
Thẻ Kho A3 (NAS) |
bản ghi mồ côi · khoá mồ côi · mảnh upload dở cũ · cờ cần cứu mồ côi |
4 |
Thẻ A4 (MySQL) |
hàng sẽ xoá · mồ côi còn email (giữ) · hàng khớp (không đụng) |
5 |
Đóng |
không thực thi |
ĐÚNG THIẾT KẾ · KHÔNG THẤY NÚT THỰC THI — ĐÚNG THIẾT KẾ Khi tổng số mục cần xoá bằng 0, hộp thoại ẩn nút Thực thi. Khi có việc, nút Thực thi dọn dẹp hiện bên cạnh Đóng và chạy lại đúng các bước với apply: true. Nếu có WP được quét, mỗi WP thêm một thẻ: entry GPM sẽ xoá, entry GPM lạ chỉ báo cáo, thư mục A2 sẽ xoá, thư mục giữ lại. |
Lớp |
Nghĩa |
Xử lý |
safe |
đã an toàn trên NAS (push sau lần sửa cuối, .tar tồn tại) |
xoá |
unlinked |
NAS không biết id này |
chỉ xoá khi bật force |
unsafe |
dữ liệu chưa được cứu |
không bao giờ xoá |
running |
đang có lock còn hạn hoặc chrome.exe dùng |
giữ |
orphan |
không map được id |
chỉ xoá khi bật tuỳ chọn thư mục lạ |
young |
mới hơn chốt tuổi |
giữ (không hiện trong danh sách giữ) |
Thư mục có tên bắt đầu bằng . hoặc _ (ví dụ _backup) vô hình với cả Dọn dẹp lẫn janitor.
Hình 14.1. Bốn profile mô phỏng thêm vào hai profile thật.
# |
Thành phần |
Để ý |
1 |
Ô tổng hợp |
đếm cả 6 profile; Tổng tính luôn dung lượng |
2 |
Chip WP |
xuất hiện WP1, WP2 vì kho có profile của các WP đó |
3 |
Chip trạng thái |
xuất hiện Cách ly, Khoá treo, Thiếu file vì mỗi loại > 0 |
4 |
Dòng Đang chạy |
cột Đang chạy ở: tên máy + lease còn 4m 40s |
Hình 14.2. Bảng Cách ly — chỉ hiện khi có ít nhất một bản cách ly (mô phỏng).
# |
Thành phần |
Ý nghĩa |
1 |
Tiêu đề + số lượng |
Cách ly (profile lỗi) (n) |
2 |
cách xử lý → |
mở /help#quarantine |
3 |
Bảng |
Profile · Lý do · Thời điểm (từ tên file YYYYmmdd_HHMMSS) · Cỡ · File; bấm dòng mở ngăn kéo |
Hình 14.3. Chip WP3: còn 4 dòng.
Hình 14.4. Bấm ô Khoá treo: còn đúng dòng có lease hết hạn.
Hình 14.5. Khi B36 không đọc được kho (mô phỏng lỗi Host is down).
# |
Thành phần |
Ý nghĩa |
1 |
Đèn NAS đỏ |
mounted: false |
2 |
Dải báo lỗi |
Kho NAS2 chưa mount được + gợi ý kiểm volume nas2_profiles_ro và tài khoản RO + lỗi gốc |
3 |
Bảng rỗng |
Chưa đọc được kho NAS2. |
Đèn Live vẫn xanh vì B36 vẫn trả lời; chỉ kho phía sau là hỏng. Cách xử lý ở mục 20.1.
Hình 15.1. Đầu trang /help: mục lục bên trái, nội dung bên phải.
Mục |
Nội dung |
1 · Trang này là gì |
vai trò, nguồn dữ liệu, ranh giới B36 đọc / B37 ghi (cập nhật 18/09/2026) |
2 · Các trạng thái & cách xử lý |
5 trạng thái, dấu hiệu, việc cần làm |
3 · CACHE nghĩa là gì? |
cache thực / hạn mức AIMD; vì sao Cache ≤ cỡ .tar |
4 · Đọc biểu đồ thống kê |
ngày/tuần/tháng, xếp chồng theo WP |
5 · Dùng API cho script |
bảng endpoint GET + ví dụ curl; ghi chú về POST /api/command/{action} (chuyển sang B37) và curl -u khi đi qua địa chỉ công khai |
6 · Sự cố thường gặp |
triệu chứng → nguyên nhân → lệnh kiểm (docker exec … ls /nas/_index/manifest.json, docker volume inspect …) |
Hình 15.2. Mục 3 — giải thích cột Cache.
Nút sách trên thanh đầu trang mở /static/manual.html — hướng dẫn người dùng cuối về mô hình năm nơi, cũng được đăng trên web tài liệu với mã HDID_00008. Mười mục:
Mục |
Nội dung |
1 |
Một profile — năm nơi |
2 |
Hệ hình SAO — NAS ở giữa |
3 |
Vòng đời bình thường của một profile |
4 |
Luật vàng |
5 |
Quy tắc đặt tên — cũng chính là ID |
6 |
Lỡ tay xoá thì sao? — ma trận |
7 |
Dùng A5 (WebProfile) — nút một chạm |
8 |
Dọn dẹp & Xoá hẳn |
9 |
Xử lý sự cố thường gặp |
10 |
Làm sao để KHÔNG mất đồng bộ |
Hình 15.3. Mục 2 của manual — hệ hình SAO quanh A3 (NAS).
Hình 15.4. Mục 6 — ma trận lỡ tay xoá ở từng nơi.
Hình 15.5. Swagger UI tự sinh từ FastAPI.
Các endpoint gom theo nhóm meta, profiles, stats, command. Bấm một endpoint > Try it out > Execute để gọi thật từ trình duyệt.
Hình 15.6. Gọi thử GET /api/health: 200, mounted true.
NGUY HIỂM · TRY IT OUT CŨNG CHẠY ĐƯỢC LỆNH GHI Nhóm command trong Swagger là thật: Execute POST /api/command/purge sẽ xoá thật qua B37. Chỉ dùng Swagger cho các endpoint GET. |
PHẦN V
TÁC VỤ & VẬN HÀNH
Mỗi quy trình ghi: khi nào dùng, các bước, kết quả mong đợi. Nút trên giao diện là đường chính; lệnh CLI là đường lui khi B37 hoặc worker không sẵn sàng.
Dùng khi: thư mục A2 trên worker đã bị xoá (Janitor, dọn đĩa) hoặc muốn chạy profile ở máy khác.
1. Mở Dọn dẹp và nhìn dòng WP — hoặc gọi /api/workers — để chắc worker của WP đó đang sống.
2. Bấm dòng profile (trạng thái Sẵn sàng, Khoá treo hoặc Cách ly) > Khôi phục về worker > Xác nhận.
3. Chờ toast. ✓ = worker trả return: OK; thư mục GPM đã có dữ liệu.
Kết quả: Lần chạy không đổi (khôi phục không phải push). Nếu pull có lease, dòng chuyển Đang chạy cho tới khi push/nhả.
1. Đọc tên máy ở cột Đang chạy ở (chữ vàng lease HẾT HẠN).
2. Kiểm máy đó trên Fleet Monitor B34 (http://172.16.10.220:20340): còn online không, còn tiến trình không.
3. Máy đang khởi động lại và sẽ chạy lại profile này ⇒ không làm gì: nó tự đòi lại lease (same-host reclaim).
4. Máy chết hẳn ⇒ ngăn kéo > Gỡ khoá treo > Xác nhận. Dòng về Sẵn sàng ở nhịp làm mới kế tiếp.
5. Cần chạy ngay ở máy khác ⇒ Khôi phục về worker (WP của máy mới) hoặc dùng lệnh lease steal + pull --lease (mục 11.6).
1. Dòng đang Đang chạy nhưng workflow kẹt.
2. Ngăn kéo > Xử lý treo (đóng) > Xác nhận — worker nhận close_profile.
3. Chạy lại workflow. Workflow lỗi giữa chừng thì không push, nên NAS vẫn giữ bản tốt gần nhất.
LƯU Ý · KHÔNG DÙNG QUIT ALL Lệnh Quit All giết luôn trình duyệt của các nhánh song song. Luôn đóng đúng một profile. |
1. Ngăn kéo > Xoá khỏi kho → Cách ly > Xác nhận xoá. Dòng chuyển Cách ly, bảng Cách ly hiện ra.
2. Chép quarantine/<id>.<ts>.tar về máy điều tra (qua SMB), xả ra, mở thử bằng Chrome.
3. Sửa được (thường là xoá file khoá SQLite/Singleton) ⇒ push đè bằng đúng id: profile_store.py --store … --host <máy> push <id> <thư_mục_tốt>.
4. Không cứu được ⇒ tạo profile mới bằng workflow (mang UID mới) hoặc Xoá hẳn id cũ.
5. profile_store.py --store … verify <id> — sha256 phải khớp manifest.
1. Tick các dòng cần bỏ (hoặc dùng nút trong ngăn kéo cho một dòng).
2. Bấm Xoá hẳn (A1 (GPM)→A5 (WebProfile)), đọc danh sách gom theo WP.
3. Giữ nguyên hai ô Xoá VĨNH VIỄN và force ở trạng thái tắt, trừ khi có lý do rõ ràng.
4. Xác nhận. Theo dõi hộp tiến trình: mỗi WP một bước, ✓/✗ kèm số xoá n/m.
5. Đọc toast cuối: Đã xóa n/m — bỏ qua: … liệt kê profile bị hoãn và lý do (lock còn hạn, worker chết…).
1. Bấm nút chổi, đợi bắt mạch xong (khoảng 6 giây).
2. Giữ mặc định: Dọn MySQL bật, Chỉ đụng con dấu UID bật, chốt tuổi 24 giờ, các ô force tắt.
3. Xem trước, đọc từng thẻ. Mục nào lạ thì dừng lại điều tra.
4. Không có gì ⇒ nút Thực thi tự ẩn, bấm Đóng. Có việc ⇒ Thực thi dọn dẹp, đọc toast đã xóa n mục.
# từ n8n / script (qua B37) curl -s 'http://127.0.0.1:20370/uid/next?count=1'
# trên worker (công cụ) python profile_store.py --store \\172.16.20.201\lotus_profiles uid peek # xem số kế tiếp, không cấp python profile_store.py --store \\172.16.20.201\lotus_profiles uid next --json |
# |
Nhìn vào |
Bình thường |
Bất thường ⇒ làm gì |
1 |
Đèn NAS |
xanh |
đỏ ⇒ mục 20.1 |
2 |
cập nhật kho: … trước |
vài phút / vài giờ khi có workflow chạy |
nhiều ngày ⇒ không có push nào: worker tắt hoặc workflow dừng (tình trạng ngày 18/09) |
3 |
Ô Khoá treo |
0 |
> 0 ⇒ quy trình 16.2 |
4 |
Ô Cách ly |
0 |
> 0 ⇒ đọc lý do, quy trình 16.4 |
5 |
Chip Thiếu file |
không có |
có ⇒ Dọn dẹp (xem trước) hoặc push lại |
6 |
Thống kê Ngày |
có cột hôm nay |
trống cả tuần ⇒ kiểm worker và n8n |
Lần chạy — = số lần push, không phải số lần mở. Workflow mở mà lỗi trước khi push thì không được đếm.
Cỡ — là kích thước .tar trên đĩa lúc đọc, không phải dung lượng khi xả ra.
Cache — thực/hạn mức: cache là phần con của profile nên thực tế Cache ≤ cỡ .tar. Toàn 0/50 = push không truyền actual_cache_mb (mục 8.1).
Lease còn… — tính ngay lúc B36 đọc, làm mới 4 s/lần; về 0 thì chuyển Khoá treo.
Giờ — cột Chạy gần nhất là tương đối; giờ tuyệt đối trong ngăn kéo là UTC (cộng 7 giờ ra giờ Việt Nam).
B=http://127.0.0.1:20360/api curl -s $B/health # {ok, mounted, store, server_time} curl -s $B/summary | jq .summary # đếm theo trạng thái curl -s "$B/profiles?status=stale-lock" | jq '.profiles[].id' curl -s "$B/profiles?wp=WP3&q=UID_0000" | jq .count curl -s $B/locks | jq '.stale_lock[] | {id, host: .stale_lock.host}' curl -s "$B/runs?limit=20" | jq '.runs[-1]' curl -s $B/workers | jq '.workers[] | {work_pool, alive, rtt_ms}' |
Đầu ra thật của /api/summary lúc biên soạn:
{"ok": true, "mounted": true, "updated": "2026-08-15T02:49:24Z", "summary": {"total": 2, "running": 0, "idle": 2, "quarantined": 0, "stale_lock": 0, "missing": 0, "size_bytes": 10383360}, "work_pools": {"WP3": {"total": 2, "idle": 2, ...}}} |
MẸO · CẢNH BÁO TỰ ĐỘNG GỢI Ý Một cron trên HP1 gọi /api/locks mỗi 10 phút và gửi cảnh báo khi stale_lock khác rỗng, hoặc khi manifest.updated cũ hơn 24 giờ trong ngày làm việc — đó là hai dấu hiệu sớm nhất của worker chết. |
Thao tác ghi thẳng lên kho, cần mount RW. Sao lưu trước, lọc các dòng demo (id bắt đầu p_WP), ghi đè:
cp /mnt/lotus_profiles/_index/runs.jsonl /mnt/lotus_profiles/_index/runs.jsonl.bak-$(date +%Y%m%d) grep -v '"id": "p_WP' /mnt/lotus_profiles/_index/runs.jsonl.bak-$(date +%Y%m%d) \ > /mnt/lotus_profiles/_index/runs.jsonl wc -l /mnt/lotus_profiles/_index/runs.jsonl # còn 56 dòng thật (18/09/2026) |
LƯU Ý · KIỂM ĐỊNH DẠNG KHOÁ TRƯỚC KHI LỌC Mẫu grep phải khớp đúng cách ghi ("id": "…" có khoảng trắng sau dấu hai chấm như dòng thật ở mục 4.3). Chạy grep -c với mẫu đó trước; số dòng bị loại phải xấp xỉ số sự kiện demo. |
Đã đếm ngày 18/09/2026: đúng 776 dòng có id p_WP… (máy WP1_PC1, WP1_PC2, WP2_PC3, WP2_PC4, WP3_PC5, WP3_PC6 — những tên máy không có thật), còn 56 dòng thật đều là UID_… từ WP3-PC1. Việc lọc chưa thực hiện: nó ghi đè dữ liệu dùng chung trên NAS nên chờ người phụ trách duyệt. Khi làm, có thể chạy ngay trong B37 (đã mount RW tại /nas) thay vì mount tay.
PHẦN VI
AN TOÀN, SAO LƯU & RỦI RO
Thành phần |
Nằm ở |
Mất thì |
Dữ liệu profile |
profiles/<id>.tar trên NAS2 |
không có bản thứ hai ngoài A2 trên worker (nếu còn) — cần snapshot NAS |
Bản đã cách ly |
quarantine/ |
như trên |
Bản đã Xoá hẳn (mềm) |
_deleted/<id>.<ts>.purge.tar |
chép ngược về profiles/<id>.tar, rồi push lại manifest bằng push |
Hàng MySQL đã xoá |
_deleted/mysql/<id>.<ts>.json |
INSERT lại theo nội dung JSON |
Sổ cái |
_index/manifest.json (+ .bak) |
dùng manifest.json.bak; hoặc push lại từng profile |
Sổ lịch sử |
_index/runs.jsonl |
chỉ ảnh hưởng thống kê |
Mã B36/B37 |
repo git riêng trong từng thư mục + bind-mount |
git của thư mục đó; image dựng lại được |
Cấu hình |
docker-compose.yml, .env, Caddyfile |
.env không có trong git — tự sao lưu riêng |
NGUY HIỂM · KHO CHÍNH CHƯA CÓ BẢN SAO THỨ HAI ĐƯỢC XÁC NHẬN Tài liệu nguồn không ghi nhận lịch snapshot hay replication nào cho share lotus_profiles trên NAS2. Nếu chưa có: bật Snapshot Replication của DSM cho share này (theo giờ, giữ vài ngày). Đây là lớp duy nhất cứu được khi chính NAS2 hỏng hoặc có người xoá tay trên share. |
Tới chiều 18/09/2026, khối Caddy profiles.lotus1104.synology.me { reverse_proxy lotus_profile_dashboard:8080 } không có basic_auth, trong khi cổng 443 của WP1 forward vào Caddy (đã ghi nhận khi mở B43 ra Internet). Bất kỳ ai biết tên miền đều xem được danh sách profile, tên máy, và — kể từ khi có B37 — gọi được POST /api/command/purge với hard: true.
Tối cùng ngày, khối site được thêm basic_auth (mẫu ở mục 6.6). Đã kiểm: không kèm tài khoản trả 401; có tài khoản trả 200 cho cả trang lẫn /api/*; route n8n và cổng LAN 20360 không bị ảnh hưởng. Tài khoản nằm ở .env cạnh compose (B36_WEB_USER, B36_WEB_PASS); Caddyfile chỉ giữ băm bcrypt.
Hình 18.1. Trang công khai sau khi đăng nhập bằng tài khoản basic_auth (chụp 18/09/2026).
Việc |
Cách làm |
Đổi mật khẩu |
sửa B36_WEB_PASS trong .env; tạo băm mới bằng docker exec lotus_caddy caddy hash-password --plaintext '<mật khẩu>'; thay băm trong khối site; docker restart lotus_caddy |
Script gọi API qua tên miền |
thêm -u "$B36_WEB_USER:$B36_WEB_PASS" vào curl; trong LAN thì gọi thẳng http://172.16.10.220:20360 (không qua Caddy, không hỏi mật khẩu) |
Siết thêm (tuỳ chọn) |
chỉ cho LAN/VPN: @ngoai not remote_ip 172.16.0.0/16 114.114.114.0/24 + respond @ngoai 403; hoặc chặn riêng /api/command/* từ ngoài |
LƯU Ý · CỔNG 20360 KHÔNG CÓ MẬT KHẨU Đăng nhập chỉ đặt ở Caddy. Cổng 20360 của HP1 vẫn mở trong LAN/VPN không mật khẩu — chấp nhận được chừng nào cổng này không được forward ra Internet trên router. |
Tách đọc/ghi — B36 dùng WP2_GPM_Profile_RO + cờ ro; chỉ B37 và worker có RW.
Mật khẩu NAS/MQTT — chỉ ở .env (HP1) và Windows Credential Manager (worker).
MySQL của B37 — tới 18/09/2026 khối B37 ghi thẳng B37_MYSQL_PASS trong compose; nay là B37_MYSQL_PASS=${B37_MYSQL_PASS}, giá trị ở .env, và B37 đã được tạo lại (up -d) để nhận. Ba khối khác trong compose (MYSQL_PASSWORD của lotus_mysql và hai dịch vụ Zabbix) vẫn ghi thẳng — chưa đổi vì phải tạo lại cả CSDL đang chạy.
Swagger — /docs công khai cùng trang: nhóm command chạy thật (mục 15.3).
Lưới |
Ở đâu |
Chống |
Cách ly thay vì xoá |
delete_nas, purge mặc định |
xoá nhầm bản gốc |
Worker trước, kho sau |
purge |
mất bản NAS khi worker chết giữa chừng |
Hoãn khi worker chết |
purge + bắt mạch 6 s |
kho sạch mà A1/A2 còn rác |
Con dấu UID |
managed_only của Dọn dẹp |
xoá profile người dùng tự tạo |
Ba điều kiện janitor + lớp unsafe |
dọn A2 |
xoá thư mục có data chưa cứu |
Chốt tuổi 24 giờ |
max_age_h |
xoá phiên vừa đóng mà push còn thử lại |
Xem trước bắt buộc |
Dọn dẹp bước 1 |
thực thi mù |
Lease + từ chối push chéo |
profile_store.py |
hai máy ghi đè nhau |
Upload nguyên tử |
incoming/ → os.replace |
người đọc thấy gói dở |
Kịch bản |
Dấu hiệu trên B36 |
Xử lý |
Worker mất điện giữa phiên |
Khoá treo sau ≤ 1 TTL |
16.2; dữ liệu phiên dở không được push — NAS giữ bản cũ |
NAS2 tắt / VPN WP1–WP2 đứt |
đèn NAS đỏ, dải báo lỗi |
20.1; worker cũng không push/pull được — workflow sẽ lỗi |
B37 tắt |
mọi nút báo B37 không phản hồi |
docker compose up -d lotus_profile_broker; bảng vẫn xem được |
B36 tắt |
trang không mở |
không ảnh hưởng worker/kho; bật lại là xong |
Hai máy cùng ghi (clobber) |
không thấy trực tiếp — số Lần chạy nhảy lạ |
nguyên nhân thường là gỡ khoá khi máy kia còn sống. Tìm bản tốt trong A2 của máy kia, push lại |
n8n ghi đè bản vá workflow |
profile mới mang tên LOTUS_<ip> thay vì UID |
Phụ lục B; hàng rào nay ở worker |
Người lạ bấm nút trên trang công khai |
profile biến mất khỏi bảng |
18.2; lấy lại từ _deleted/ nếu chưa bật hard |
Share bị xoá tay trên DSM |
mọi thứ trống |
chỉ cứu được bằng snapshot NAS (18.1) |
NGUY HIỂM · THAO TÁC KHÔNG HOÀN TÁC ĐƯỢC Ba thứ duy nhất trên giao diện có thể làm mất dữ liệu vĩnh viễn: ▸ Xoá hẳn với ô Xoá VĨNH VIỄN bật. ▸ Dọn dẹp với ô Xoá cả thư mục A3 (NAS) không biết id (force) bật. ▸ Gỡ khoá treo khi máy giữ vẫn đang chạy (dẫn tới ghi đè). |
PHẦN VII
KHẮC PHỤC SỰ CỐ
docker exec lotus_profile_dashboard ls /nas/_index/manifest.json docker volume inspect dcp_production_nas2_profiles_ro ping -c2 172.16.20.201 # NAS2 qua VPN WP1-WP2 |
Nguyên nhân |
Dấu hiệu |
Sửa |
NAS2 tắt / VPN đứt |
ping không tới |
khôi phục mạng; B36 tự thấy lại khi SMB lành |
Sai tài khoản RO trong .env |
ls /nas báo Permission denied |
sửa .env rồi xoá và tạo lại volume: docker compose down lotus_profile_dashboard, docker volume rm dcp_production_nas2_profiles_ro, docker compose up -d lotus_profile_dashboard |
Share đổi tên / chưa init |
/nas trống hoặc thiếu _index |
profile_store.py init bằng tài khoản RW |
Mount CIFS ôi (sau khi NAS khởi động lại) |
ls treo lâu hoặc Host is down |
docker compose restart lotus_profile_dashboard; không được thì tạo lại volume như dòng 2 |
GHI CHÚ · VÌ SAO ĐỔI .ENV MÀ VOLUME VẪN DÙNG MẬT KHẨU CŨ Volume Docker ghi lại tuỳ chọn mount lúc tạo. Đổi .env không làm volume đổi theo; phải xoá volume (dữ liệu nằm trên NAS nên không mất gì) rồi để compose tạo lại. |
Thông báo (toast) |
Nghĩa |
Làm gì |
B37 không phản hồi: … |
B36 không gọi được B37 (HTTP 502) |
docker ps xem lotus_profile_broker; curl :20370/health |
worker không phản hồi (timeout) |
B37 gửi MQTT nhưng không ai trả lời |
Process Manager trên máy WP đó tắt, sai topic, hoặc hai instance tranh nhau (Phụ lục C, D) |
worker_offline trong phản hồi |
bắt mạch 6 s thất bại, lệnh nặng không được gửi |
bật Process Manager rồi thử lại |
worker báo NG / Unsupported function … |
worker nhận nhưng không có hàm đó |
worker chạy code cũ — deploy lại (Phụ lục C) |
Chưa biết workpool → không gửi được lệnh cần worker |
profile có WP ? |
đặt LOTUS_WORK_POOL hoặc đặt tên máy có chữ WPn; Gỡ khoá và Xoá khỏi kho vẫn dùng được |
Lỗi mạng khi gửi lệnh |
trình duyệt không tới được B36 |
kiểm mạng/Caddy |
Trình duyệt hỏi tên/mật khẩu; script nhận 401 |
đi qua tên miền công khai (basic_auth ở Caddy) |
nhập tài khoản trong .env (B36_WEB_USER/PASS); script thêm curl -u, hoặc gọi thẳng :20360 trong LAN |
Triệu chứng |
Nguyên nhân |
Cách xử lý |
Tạo profile trong GPM mà bảng không có |
tạo tay không đi qua worker ⇒ không push |
tạo bằng workflow (create_profile) hoặc push tay |
Profile hiện WP? |
manifest work_pool rỗng và tên máy không khớp mẫu |
đặt LOTUS_WORK_POOL trên worker; hoặc cập nhật profile_store.py (bản 18/09 tự suy từ tên máy WPn) |
Thời lượng 0s, Cache 0/50 MB |
node Push không truyền duration, actual_cache_mb |
mục 8.1 |
Xoá hẳn xong dòng vẫn hiện thành Cách ly |
bug cũ trước 14/08 (purge bỏ quên quarantine/) |
đã sửa: purge dọn cả quarantine/ sang _deleted/ (Phụ lục E) |
Một profile Đang chạy rất lâu |
phiên dài có heartbeat — bình thường |
chỉ lo khi workflow thật sự kẹt (16.3) |
Biểu đồ có cột lớn tháng 7–8 dù ít profile |
776 sự kiện demo còn trong runs.jsonl |
mục 17.4 |
cập nhật kho nhiều ngày trước |
không có push nào |
worker tắt hoặc workflow dừng; kiểm B34 và n8n |
Triệu chứng |
Nguyên nhân |
Cách xử lý |
Thư mục A2 vẫn còn sau khi dọn |
mới hơn chốt tuổi 24 giờ (lớp young) |
đặt ô tuổi = 0 nếu chắc chắn |
Thư mục ghi unsafe |
dữ liệu chưa được push an toàn |
không bao giờ tự xoá — push trước, rồi dọn |
Entry GPM chỉ báo cáo |
tên không mang con dấu UID |
xoá tay trong GPM nếu chắc chắn |
Hàng MySQL mồ côi còn email |
email ↔ IP là dữ liệu nghiệp vụ |
bật Xóa cả hàng MySQL CÒN email nếu chắc |
WP không được tick sẵn |
B37 chưa có topic, hoặc worker không trả lời 6 s |
mục 7.6 / bật Process Manager |
Không có nút Thực thi |
kế hoạch rỗng |
đúng thiết kế |
Triệu chứng |
Nguyên nhân thật đã gặp |
Cách xử lý |
pull đúng dữ liệu nhưng Chrome mở thư mục rỗng |
xả vào <base>/<id> trong khi GPM mở <base>/<mã>-<ngày> |
đã sửa: pull hỏi GPM profile_path; đường lui là --into |
push/pull rc≠0 dù đã upload xong |
stdout cp1252 ném UnicodeEncodeError khi in ký tự ✔ |
đã sửa trong profile_store.py (ép stdout UTF-8) |
Lệnh mở profile báo Browser debug request failed … refused |
truyền remote_port khi test tay |
đừng truyền remote_port cho open_profile khi thử bằng tay |
Workflow cũ tạo profile tên LOTUS_<ip> |
bản vá n8n bị một lần Save ghi đè |
Phụ lục B; hàng rào nay ở worker |
PHẦN VIII
THỰC HÀNH
Sáu bài, từ chỉ-đọc tới có ghi. Bài 1–3 làm được ngay trên hệ thật; bài 4–6 cần một profile thử nghiệm và một worker đang chạy.
Mục tiêu: Đối chiếu con số trên giao diện với API.
1. Mở trang, ghi lại năm ô tổng hợp và dòng cập nhật kho.
2. curl -s :20360/api/summary — so từng số.
3. Mở ngăn kéo một profile, so sha256 (16 ký tự) với manifest.json qua /api/profiles/<id>.
Đạt khi: mọi số trùng khớp; giải thích được vì sao Thời lượng bằng 0.
Mục tiêu: Thành thạo thanh lọc.
1. Lọc theo WP3, rồi theo Sẵn sàng; bấm ô Đang chạy để thấy bảng rỗng.
2. Tìm theo tên máy WP3-PC1.
3. Sắp theo Chạy gần nhất giảm dần; tạm dừng làm mới; đổi nền sáng.
Đạt khi: chụp màn hình từng bước, bộ đếm (n) đúng số dòng.
Mục tiêu: Hiểu bắt mạch và kế hoạch mà không xoá gì.
1. Bấm nút chổi, chờ bắt mạch; ghi lại trạng thái từng WP.
2. Xem trước với mặc định. Ghi số trong dòng tổng.
3. Đóng — không bấm Thực thi.
Đạt khi: giải thích được vì sao WP bị bỏ tick và vì sao không có nút Thực thi (nếu kế hoạch rỗng).
Mục tiêu: Thấy pull/push/lease hiện lên B36.
1. Tạo profile bằng workflow (tên để trống ⇒ nhận UID_…).
2. Chạy Pull (lease bật) — trên B36 dòng chuyển Đang chạy, cột Đang chạy ở đếm lùi.
3. Close rồi Push có điền duration và actual_cache_mb — dòng về Sẵn sàng, Lần chạy tăng.
Đạt khi: biểu đồ Ngày có cột hôm nay; Thời lượng khác 0.
Mục tiêu: Luyện quy trình 16.2 an toàn.
1. Với profile thử nghiệm: Pull (lease, TTL 60 s) rồi tắt Process Manager bằng tay.
2. Chờ tới khi dòng chuyển Khoá treo; kiểm B34 thấy máy offline.
3. Gỡ khoá treo; dòng về Sẵn sàng. Bật lại Process Manager.
Đạt khi: không có hai máy nào ghi cùng lúc; lease status sau cùng báo không có lock.
Mục tiêu: Hiểu cách ly, _deleted/ và sao lưu MySQL.
1. Xoá khỏi kho → Cách ly profile thử nghiệm; đọc bảng Cách ly.
2. Xoá hẳn (không bật VĨNH VIỄN); dòng biến mất.
3. Trên NAS (mount RO), tìm _deleted/<id>.<ts>.purge.tar và _deleted/mysql/<id>.<ts>.json.
Đạt khi: chỉ ra được nơi lấy lại cả dữ liệu lẫn hàng MySQL.
PHẦN IX
PHỤ LỤC
Trước khi biên soạn, hệ được kiểm từ đầu tới cuối để trả lời câu hỏi: trang còn kết nối được NAS không, và profile đẩy lên/kéo về thế nào? Trình tự và kết quả:
# |
Kiểm |
Lệnh |
Kết quả |
1 |
Container B36 |
docker ps --filter name=lotus_profile_dashboard |
Up 13 days |
2 |
Trang công khai |
curl -w '%{http_code}' https://profiles…/ |
200 · 0,74 s |
3 |
Mạng tới NAS2 |
ping 172.16.20.201 |
0 % mất gói, ~6,5 ms |
4 |
Đọc kho qua B36 |
ls -la /nas/profiles trong container |
2 file .tar, cùng 5 191 680 byte |
5 |
Chống bộ đệm |
mount mới hoàn toàn bằng tài khoản RO, cache=none |
tên, cỡ, sha256 trùng khớp mount của B36 |
6 |
Toàn vẹn |
sha256sum so manifest.json |
khớp cả hai |
7 |
Nội dung gói |
tar tf |
455 mục — cấu trúc profile Chrome |
8 |
Khoá / upload dở |
ls _locks incoming quarantine |
đều trống |
9 |
Lịch sử pull/push |
/api/state của B34 |
118 sự kiện pull/push, tất cả từ WP3/PC_1; 33 lỗi đều ngày 10/08 |
10 |
Worker |
/api/workers; ping 172.16.30.101 |
lúc kiểm: không phản hồi MQTT trong 6 s; máy vẫn bật (SSH/RDP mở). Tối 18/09 đã bật lại Process Manager: alive: true, rtt_ms: 640 |
LƯU Ý · KẾT LUẬN Trang và NAS ổn; dữ liệu nguyên vẹn. Nhưng không có push/pull nào từ 15/08 vì Process Manager trên WP3-PC1 không chạy. Đã bật lại tối 18/09/2026 qua SSH (tác vụ lên lịch /it vào phiên RDP 2 của meomay22): hai tiến trình Python ở phiên 2, kết nối 128.199.200.17:1883 ESTABLISHED, B34 thấy WP3 / PC_1 online. Việc còn lại: chạy một vòng pull → mở → push để xác nhận cả tuyến. |
Bối cảnh (14/08/2026). ID cũ là UUID của GPM — vô nghĩa, không tra được, không có số thứ tự. Hệ chuyển sang UID_xxxxxx_GPM_Profile, và node Parse IP + Proxy của workflow Lv1RQyThF9U9cT8Q được vá để để trống tên (trước đó tự đặt LOTUS_<ip>).
Sự cố. Vá qua API lúc 14:44, chạy đúng hai lần; tới 16:45 một cú Save trong trình soạn thảo n8n ghi đè lại bản cũ — bốn profile ra đời tên LOTUS_<ip>, .tar và MySQL mang UUID, không một dòng lỗi. Bấm Test workflow là n8n tự lưu trước khi chạy, nên một tab mở từ trước hay một lần Ctrl+Z là đủ xoá bản vá.
Truy vết. Bảng workflow_history trong database.sqlite của volume n8n (cột authors, createdAt, nodes) và workflowData trong từng execution cho thấy đúng mốc đổi.
ĐÚNG THIẾT KẾ · BÀI HỌC Đừng coi n8n là chỗ đặt hàng rào. Hàng rào nay nằm ở worker: create_profile bỏ qua mọi tên caller gửi và luôn xin UID (trừ tên đã đúng chuẩn hoặc keep_name=true); xin UID lỗi thì không tạo. Đã thử thật: gửi LOTUS_103_249_99_99 → ra UID_000005_GPM_Profile. |
Bẫy đi kèm. Khi đổi hệ ID, phép so "A3 có biết profile này không" phải đối chiếu cả UUID lẫn tên-mang-con-dấu. Chỉ so UUID thì mọi profile đời mới thành "mồ côi" và Dọn dẹp đề nghị xoá sạch A1. Có test hồi quy riêng (verify/test_profile_identity.py).
Triệu chứng (10/08/2026). Node C12 Pull/Push trả Unsupported function push_profile in group ManagerAction — 33 lượt lỗi còn ghi trong Fleet Monitor. File trên đĩa worker đã có hàm mới (findstr _handle_push_profile thấy).
Nguyên nhân thật. Hai Process Manager cùng chạy và cùng subscribe một topic. Instance cũ vẫn giữ code cũ trong RAM và trả lời trước. Lần khác, hàm nghiệp vụ đã ở file base (GPM_multi_remote_browser_manager.py) nhưng lớp định tuyến (GPM_multi_remote_browser_process_manager.py, cái GUI thật sự import) chưa nối — yêu cầu bị chặn trước khi tới base.
ĐÚNG THIẾT KẾ · QUY TRÌNH DEPLOY CODE GITIGNORED SANG WORKER so sha256 ba file → scp → xoá __pycache__ → khởi động lại đúng một instance → probe từ HP1 bằng mqtt_send.py. Hết Unsupported, trả OK mới tính là xong. .bat nay có khoá một instance (PowerShell giết PM cũ trước khi mở). |
Topic thật của worker WP3-PC1 là WP3-PC1 (xác minh 13/08/2026 trong config đang chạy GPM_multi_remote_browser_process_manager_gui_config.json > values.subscribe_topics). WP2_PC176 là topic cũ thời điều tra C12 tháng 7 — vẫn còn trong vài tài liệu, trong mặc định của docs/tools/mqtt_send.py và của chính mã Process Manager (SUBSCRIBE_TOPICS = ["WP2_PC176"]).
LƯU Ý · TIMEOUT KHÔNG CÓ NGHĨA MÁY CHẾT Kiểm topic trước, rồi kiểm số instance. B37 chỉ gửi tới topic trong B37_WP_TOPICS. |
Khởi động lại Process Manager từ xa (đã kiểm 13/08): taskkill /PID <pid> /T /F rồi tạo scheduled task chạy .bat với /it (GUI Tkinter cần phiên tương tác) — cần người dùng đang đăng nhập.
Bug (14/08/2026). Bản đầu của purge chỉ nhìn profiles/ và đẩy bản mềm sang quarantine/. Hậu quả: profile đang Cách ly bị xoá mỗi cái sổ, file vẫn nằm trong quarantine/ và B36 dựng lại dòng từ thư mục — người dùng bấm xoá mà thấy không mất; còn profile vừa purge lại hiện thành Cách ly.
ĐÚNG THIẾT KẾ · QUY TẮC RÚT RA quarantine/ là thứ người dùng thấy; _deleted/ là thứ họ đã bảo bỏ đi. Đừng trộn hai vùng. |
Nợ |
Vì sao để nợ |
Máy trắng / A1 bị xoá: GPM create sinh ID mới, không nhận ID cũ |
99 % ca không cần; cần remap ID ở tầng NAS — đại phẫu |
config.json chưa mang danh tính GPM (tên/proxy/core/version) |
nhánh tạo lại A1 hiện dựng từ {} |
Fencing token trong _locks |
chỉ thêm khi thấy clobber TOCTOU thật |
Janitor --apply + lịch chạy |
đang DRY-RUN; cần đọc profile_janitor.log vài ngày trước khi bật |
Topic WP1/WP2 chưa khai cho B37 |
chưa xác minh topic thật |
Cột cuối ghi tình trạng sau đợt sửa tối 18/09/2026. Mã B36 nằm ở kho git riêng githublotus/B36_Profile_Dashboard (commit 5a5d6cc); bản sửa profile_store.py ở repo chính (commit e21af2b).
# |
Lỗi |
Bằng chứng |
Cách sửa · tình trạng |
F1 |
Lệnh CLI trong ngăn kéo sai cú pháp |
chạy thử trên kho tạm: unrecognized arguments: --dir --host H, --dest, --host H |
--host lên trước lệnh con; pull <id> <dest>; push <id> <dir> trong app.js và /help. Đã sửa, bảy lệnh chạy thử đều rc=0 |
F2 |
Thanh chọn-nhiều luôn hiện một dải trống |
CSS .bulk-bar{display:flex} đè hidden |
thêm .bulk-bar[hidden]{display:none!important} vào style.css. Đã sửa |
F3 |
README.md B36 và /help còn ghi "chỉ đọc", "tất cả API chỉ đọc" |
có /api/command/* từ 13/08 |
ghi rõ ranh giới B36 đọc / B37 ghi trong README, /help, chân trang, mô tả OpenAPI. Đã sửa |
F4 |
runs.jsonl còn 776 sự kiện demo |
đếm trực tiếp trên kho |
lọc theo mục 17.4. Chưa làm — ghi đè dữ liệu dùng chung, chờ duyệt |
F5 |
Lịch sử ghi wp rỗng, dur 0, cache 0 |
dòng thật cuối trong runs.jsonl |
wp: đã sửa — profile_store.py tự suy từ tên máy (cần git pull trên worker). dur/cache: node Push phải truyền duration, actual_cache_mb — chưa đổi |
F6 |
TTL mặc định lệch nhau: 1800 / 300 / 180 s |
profile_store.py, .bat, GUI |
thống nhất 300 s (khớp placeholder node C12). Giữ nguyên — đổi mặc định ảnh hưởng mọi workflow đang chạy |
F7 |
Trang công khai không xác thực nhưng có nút ghi |
Caddyfile dòng 70; WP1 forward 443 |
basic_auth ở Caddy. Đã sửa — 401 khi thiếu tài khoản (mục 18.2) |
F8 |
Mật khẩu MySQL viết thẳng trong compose |
khối lotus_profile_broker |
B37_MYSQL_PASS=${B37_MYSQL_PASS} + .env, tạo lại B37. Đã sửa |
F9 |
Chú giải biểu đồ thiếu màu xám (?) |
renderLegend() chỉ vẽ WP1–WP3 |
thêm mục WP? (chưa biết). Đã sửa |
Để chụp tab NAS, mã GUI của Process Manager được chạy trên màn hình X của B35 cùng một file cấu hình riêng trỏ MQTT về 127.0.0.1:1 (không tới đâu). Nhưng __init.py của APP_5 tự chdir về gốc repo, và GUI tìm cấu hình theo thư mục hiện hành — nên nó nạp file GPM_multi_remote_browser_process_manager_gui_config.json thật ở gốc repo, tự kết nối broker 128.199.200.17:1883, subscribe topic WP2_PC176 và gửi heartbeat lên Fleet Monitor khoảng mười giây mỗi lần chạy.
Hậu quả |
Kiểm chứng |
Tình trạng |
Một máy lạ WP3 / NUC (hostname NOVNC_CHROME) xuất hiện trên B34 |
/api/state → pools |
đã gỡ ngày 18/09/2026 bằng POST :20340/api/clear_pc (work_pool: WP3, pc_name: NUC) |
Subscribe topic cũ WP2_PC176 ~10 s |
log GUI |
không nhận/trả lệnh nào; worker WP3 nghe WP3-PC1 |
5 file logs/faulthandler/*.log ở gốc repo |
git status |
đã xoá; git status sạch |
File config thật ở gốc repo |
mtime vẫn 26/07/2026 |
không bị ghi đè |
NGUY HIỂM · BÀI HỌC: CHẠY GUI APP_5 NGOÀI WORKER Đặt config cạnh kịch bản là không đủ — __init.py đổi thư mục làm việc. Muốn chạy cô lập phải chặn mạng của tiến trình (ví dụ docker run --network none) hoặc chạy trên bản sao repo có config riêng ở gốc. Và GUI luôn tự kết nối 300 ms sau khi mở. |
Địa chỉ: http://127.0.0.1:20370 (host) hoặc http://114.114.114.37:8080 (mạng Docker). Chỉ LAN/VPN.
Phương thức |
Đường dẫn |
Việc |
GET |
/health |
locks_active, wp_topics, trạng thái MySQL |
GET |
/lease/{id} · /locks |
đọc lock của một profile / mọi lock |
POST |
/command/restore |
MQTT restore_profile → worker |
POST |
/command/kill_chrome |
MQTT close_profile → worker |
POST |
/command/release_lock |
xoá _locks/<id>.lock (B37 tự làm) |
POST |
/command/delete_nas |
chuyển sang quarantine/ (cần confirm: true) |
POST |
/command/cleanup |
dọn theo chuẩn A3; apply:false = xem trước |
POST |
/command/purge |
xoá hẳn 1..n profile ở mọi nơi |
POST |
/command/ping |
bắt mạch một WP |
GET |
/workers |
bắt mạch song song mọi WP (mặc định 6 s) |
GET |
/mysql/rows |
sổ A4 + phân loại theo A3 |
GET |
/uid/next?count=N |
cấp tên UID_… (1–100) |
GET |
/profile/{id}/verify |
cổng "đã an toàn trên NAS" (sha256) |
Định dạng lệnh MQTT (great-lotus-pyauto-v1): action là object {key, group, function}, inputs là dict có kiểu {name: {type, value}}, phản hồi trên RES_<topic> mang lại meta.requestId. Sai dạng thì worker trả NG: Missing object action.
Biến |
Mặc định |
Ý nghĩa |
LOTUS_PROFILE_NAS_SYNC |
0 |
bật hook tự pull trước khi mở / push sau khi đóng |
LOTUS_PROFILE_STORE |
\\172.16.20.201\lotus_profiles |
gốc kho |
LOTUS_GPM_PROFILE_DIR |
(trống) |
thư mục gốc GPM giữ folder profile |
LOTUS_GPM_PROFILE_GLOB |
{id} |
mẫu tên folder con; {id}-* nếu có hậu tố ngày |
LOTUS_PROFILE_HOST |
hostname |
tên máy cho lease và lịch sử |
LOTUS_PROFILE_LEASE_TTL |
xem mục 2.3 |
hạn lease (giây) |
LOTUS_PROFILE_LEASE_RENEW_SEC |
TTL/3 |
nhịp gia hạn (sàn 10 s khi đặt bằng biến) |
LOTUS_WORK_POOL |
(trống ⇒ giữ giá trị cũ, rồi suy WPn từ tên máy) |
WP ghi vào manifest và lịch sử |
LOTUS_JANITOR_DELETE_UNKNOWN |
0 |
janitor được xoá thư mục không map được id |
Thuật ngữ |
Nghĩa trong sách |
A1…A5 |
năm nơi của một profile: GPM · C_Temp · NAS · MySQL · WebProfile (mục 1.3) |
AIMD |
Additive-Increase / Multiplicative-Decrease — cách profile_store.py tự chỉnh trần cache |
Cách ly (quarantine) |
tách bản lỗi khỏi kho chính, vẫn giữ để điều tra |
Con dấu UID |
tên UID_xxxxxx_GPM_Profile — dấu hiệu profile do hệ quản lý |
Heartbeat |
luồng nền gia hạn lease mỗi TTL/3 giây khi profile đang mở |
Khoá treo (stale-lock) |
lease đã hết hạn nhưng file lock còn |
Lease |
khoá phiên có hạn: _locks/<id>.lock |
Mồ côi |
bản ghi/khoá/hàng/thư mục không còn liên kết với profile nào trên NAS |
Same-host reclaim |
máy tự đòi lại lease của chính nó ngay khi khởi động lại |
Snapshot (API) |
ảnh chụp trạng thái kho do /api/snapshot trả về |
Topic |
kênh MQTT của một worker; phản hồi trên RES_<topic> |
Upload nguyên tử |
ghi incoming/…tmp rồi os.replace sang profiles/ |
Workpool (WP) |
một địa điểm/mạng: WP1, WP2, WP3 |
Loại |
Nguồn |
Mã B36 |
B36_Profile_Dashboard/app/server.py, store_reader.py, static/index.html, app.js, style.css, help.html, manual.html |
Mã B37 |
B37_Profile_Broker/README.md, app/server.py |
Công cụ kho |
docs/tools/profile_store.py (trợ giúp từng lệnh con được chạy thật) |
Worker |
Library/B05_GPM_Login_Manager/profile_nas_sync.py; GUI APP_5 (mục NAS) |
n8n |
GreatLotus.node.ts của C12 (ACTION_SPECS, ACTION_SUMMARIES) |
Hạ tầng |
docker-compose.yml, .env (chỉ tên khoá), B16_Caddy/Caddyfile, docs/network/ |
Kinh nghiệm |
sổ ghi nhớ vận hành 05/08 → 18/09/2026 |
Số liệu sống |
API B36/B37/B34, docker, ping, mount RO độc lập — 18/09/2026 |
Ảnh chụp bằng Chrome thật trên B35 (1600×900) qua Selenium, toạ độ phần tử lấy bằng getBoundingClientRect() rồi đánh số tự động; mọi lệnh ghi bị chặn ngay trong trình duyệt trừ Dọn dẹp ở chế độ xem trước. Sơ đồ vẽ bằng matplotlib. Sách dựng bằng python-docx (doc_kit), PDF bằng LibreOffice, bản HTML đăng qua web tài liệu HDID. Mã dựng: tmp/b36_gt/ (cap*.py, run_annotate.py, diagrams.py, c*.py, build.py).