Skip to content

Ứng dụng Desktop (Desktop App) ​

Govard Desktop là ứng dụng giao diện (GUI) viết bằng Wails 3, tái sử dụng cùng một core engine với phiên bản CLI. Ứng dụng chạy trên GTK 4 và WebKitGTK 6.0 (có sẵn từ Ubuntu 24.04+ và Debian 13+); Ubuntu 22.04 và Debian 12 chỉ cài được CLI.


Các chế độ khởi chạy (Launch Modes) ​

bash
govard desktop              # Khởi chạy ứng dụng desktop đã được build
govard desktop --dev        # Vite dev server kèm build lại Go
govard desktop --background # Khởi động ẩn, tái sử dụng instance khi mở lại
Chế độMô tả
govard desktopKhởi chạy tiêu chuẩn — sử dụng binary đã build
govard desktop --devChế độ phát triển — live Go backend, hỗ trợ hot reload frontend
govard desktop --backgroundTiến trình chạy ngầm — giữ ứng dụng hoạt động ngay cả khi đóng cửa sổ

Giao diện hiện tại ​

Ứng dụng Desktop tập trung vào các thao tác quản lý cốt lõi:

Tính năngMô tả
Bảng điều khiển môi trườngStart/stop/open/delete — bao gồm việc phát hiện các dự án Docker mồ côi
Workspace dự ánDanh sách các môi trường, các thao tác nhanh, quy trình onboard
Thao tác nhanhPHPMyAdmin, bật/tắt Xdebug, kiểm tra sức khỏe, Mailpit, DB client
Tab RemotesQuy trình thêm/kiểm tra/mở/lập kế hoạch đồng bộ cho môi trường remote
Tab DeployTrạng thái/releases/plan/check chỉ đọc, kèm rollback và sandbox rehearse có cổng preview
Tab DataThông tin DB đã che, query có kiểm soát, dump/import, snapshot (xem bên dưới)
Giám sát tài nguyênCPU, RAM, mạng, cảnh báo OOM
Nhật ký (Logs)Lọc logs theo dịch vụ, mức độ nghiêm trọng, tìm kiếm text, stream trực tiếp
Shell LauncherChọn container dịch vụ, user kết nối và loại shell tương ứng
Tool RunnerChạy các lệnh tool theo preset của framework, có stream output (xem bên dưới)
Thông báo hệ thốngHiển thị cảnh báo khi các thao tác thành công hoặc thất bại
Bảng Cài đặtThay đổi theme giao diện, cấu hình proxy, trình duyệt ưa thích, DB client

Các thao tác khởi động/dừng/tải môi trường và quản lý dịch vụ toàn cục trên Desktop đều gọi trực tiếp tới tầng lệnh CLI của Govard (govard up, govard env ..., govard svc ...), giúp hành vi của ứng dụng Desktop luôn đồng bộ với các cập nhật mới nhất của CLI.

Các thao tác dài được chờ lâu hơn: up, bootstrap, restart và pull chờ tối đa 15 phút (10 phút cho global-services start) thay vì thất bại sau hai phút. Thông báo timeout nêu rõ thời lượng; khi thao tác có thể đã để lại container dở dang, thông báo sẽ nói rõ và chỉ tới chức năng phát hiện dự án mồ côi trên dashboard. Mỗi project chỉ chạy một thao tác thay đổi môi trường tại một thời điểm — bấm Start hai lần hay Start rồi Stop dồn dập sẽ báo thao tác đang chạy thay vì khởi động trùng lặp, và khóa chỉ được thả khi lệnh backend thực sự kết thúc kể cả khi bộ đếm dự phòng của UI đã hết trước.

Các panel log trực tiếp giữ 2.000 dòng gần nhất và tạm dừng polling khi cửa sổ bị ẩn, rồi tải dữ liệu mới khi quay lại. Nếu một panel gặp lỗi, nó hiển thị thông báo kèm nút thử lại thay vì trắng trơn.


Trung tâm điều hành (Operation Center) ​

Tab Operations là nơi hiển thị các thao tác dài. Tab gồm hai danh sách, cả hai đều do OperationService phục vụ qua các route ListOps, ListHistory, CancelOp và RetryOp trong bridge.js, ánh xạ tới các method Go cùng tên (internal/desktop/operation_service.go). Bản thân tab là React island OperationCenter với các helper thuần trong desktop/frontend/modules/operation-center.js.

Danh sáchNguồnThứ tự
Đang chạy (Running)ListOpsCũ nhất trước, mỗi op có nút Cancel
Lịch sử (History)ListHistoryMới nhất trước; ListHistory(limit) giới hạn số bản ghi đọc, limit <= 0 nghĩa là đọc tất cả

Island tải lại cả hai danh sách khi nhận sự kiện backend sync:progress và operations:notification (đăng ký qua services/events.js; hai hằng số SYNC_PROGRESS_EVENT và OPERATIONS_NOTIFICATION_EVENT nằm trong modules/operation-center.js).

Đồng thời: một sync mỗi project ​

  • Mỗi project chỉ chạy một sync nền tại một thời điểm. RunRemoteSync giữ project qua TryRegisterSyncingProject trong một lần khóa mutex duy nhất, và chính lần giữ đó là chốt chặn, nên hai lần khởi động đồng thời không thể cùng vượt qua. Lần khởi động thứ hai bị từ chối với cannot start sync for "X" while sync from "Y" is in progress (hoặc another sync is already in progress khi không ghi nhận remote nào). Khóa dùng ID project chuẩn hóa, nên một lần Start gọi bằng domain không thể lọt qua một sync đã khởi động bằng đường dẫn.
  • Trong khi sync đang chạy, các thao tác thay đổi môi trường trên cùng project (start, stop, restart, pull, toggle, delete) đều bị từ chối, vì sync đang sở hữu các container.
  • Registry từ chối id op trùng lặp (RegisterOp và RegisterOpWithArgs trả về false và giữ nguyên entry cũ), nên bản trùng không bao giờ đẩy được hàm cancel ra và làm mồ côi context của nó.
  • Một RetryOp thứ hai cho bản ghi đã có retry đang chạy cũng bị từ chối; hãy đợi lần chạy hiện tại kết thúc rồi mới retry.

Hủy (Cancel) ​

  • CancelOp(id) trên một op đang chạy bị theo dõi sẽ gỡ entry, ghi thêm một bản ghi terminal cancelled, và kích hoạt hàm cancel đã lưu đúng một lần (sau khi đã thả khóa, để hàm cancel có gọi ngược vào registry cũng không deadlock). Một cancel đua với một finish chỉ ghi đúng một trạng thái terminal. Id không tồn tại hoặc đã kết thúc trả về false, và UI coi đó là thành công không cần báo (<id> already finished) chứ không phải lỗi.
  • Giới hạn, nói rõ ràng: các sync nền (RemoteService.RunRemoteSync) và các lệnh CLI môi trường một lần hiện chưa phải op được registry theo dõi, nên chúng không xuất hiện trong Running và trung tâm không thể hủy chúng. Chúng chạy tới khi xong và báo cáo qua các sự kiện sync:failed / sync:completed cùng notification thao tác bên dưới. Các sync mở trong terminal của hệ điều hành (RunRemoteSyncInTerminal) chạy hoàn toàn ngoài ứng dụng. Luồng log trực tiếp là cơ chế riêng: StopLogStream và StopGlobalServiceLogStream dừng luồng và kill tiến trình docker logs bên dưới.

Lưu lịch sử và khôi phục sau crash ​

  • Các bản ghi terminal được lưu dạng JSONL trong <govard-home>/desktop/operation-history.jsonl (tôn trọng GOVARD_HOME_DIR), mỗi dòng một trạng thái terminal: completed, failed, cancelled, interrupted. Tham số args được tẩy secret khi ghi (secret thành placeholder ***); message được làm sạch và cắt ở 2.000 ký tự. Dòng hỏng hoặc dòng trống bị bỏ qua và không bao giờ chiếm ngân sách.
  • Trần: OperationHistoryCap là 200 bản ghi hợp lệ. Ghi vượt trần sẽ loại các bản ghi cũ nhất, giữ nguyên các bản ghi mới nhất. Các lần ghi lại toàn bộ xuất bản nguyên tử (file tạm rồi rename), nên crash giữa chừng không bao giờ để lại file lịch sử cắt dở.
  • Khôi phục sau crash: mỗi op đã đăng ký được nhân bản vào sidecar operation-inflight.json cho tới khi đạt trạng thái terminal. RecoverInterruptedOps chạy khi khởi động và đánh dấu các entry mồ côi thành interrupted với message Interrupted by app restart, khử trùng theo cặp (id op, thời điểm start) để bản ghi terminal đã kịp ghi trước crash không bao giờ bị trùng. Sidecar không đọc được sẽ bị cách ly sang một bên (operation-inflight.json.corrupt-<timestamp>) để điều tra: không bao giờ lặng lẽ xóa, cũng không bao giờ bịa bản ghi từ đó.

Chạy lại (Retry) ​

  • Nút Retry chỉ xuất hiện trên các bản ghi thất bại. RetryOp phát lại args đã lưu qua đúng đường service gốc dưới một id op mới; bản ghi terminal cũ không bao giờ bị sửa, nên ngữ nghĩa exactly-once của finish và cancel được giữ nguyên.
  • Mọi từ chối đều fail-closed kèm lỗi có hướng xử lý: id không tồn tại hoặc id không có bản ghi terminal; bản ghi đã kết thúc ở completed, cancelled hoặc interrupted; args đã lưu mang placeholder tẩy *** (hãy chạy lại với credential mới thay vì phát lại, vì placeholder không còn xác thực được); loại op chưa có handler phát lại đã đăng ký (RegisterOpRetryHandler); đã có một retry đang chạy cho cùng bản ghi.
  • Các entry bị gián đoạn không bao giờ phát lại. Dòng lịch sử hiển thị gợi ý chạy mới thay thế: Interrupted by an app restart. Start a fresh run; the stored args are kept for reference.
  • Các handler phát lại cho các họ thao tác WS-A/B/C sẽ đi kèm các follow-up đó. Cho tới khi một loại đăng ký handler của mình, RetryOp vẫn fail-closed (no replay path) và nút Retry hướng dẫn chạy lại thao tác từ view gốc thay vì phát lại tại đây.

Thông báo (Notifications) ​

  • Các thao tác terminal phát operations:notification (EventOperationsNotification), được thăm dò mỗi 2 giây với giới hạn đọc 256 sự kiện. Phân phối là at-least-once: watcher phát trước rồi mới lưu cursor sau, nên crash xen giữa hai bước sẽ gửi lại (trùng lặp hiển thị) thay vì lặng lẽ mất notification.
  • Cài mới sẽ ẩn lịch sử log có sẵn từ trước. Khởi động lại tiếp tục từ cursor đã lưu (operation-notify-cursor trong cùng thư mục state) và gửi bù khoảng trống downtime ngay lần poll đầu. Nếu cursor đã mất (log xoay vòng, hoặc cửa sổ đọc đã trượt qua trong lúc app tắt), toàn bộ cửa sổ sẽ được gửi thay vì bị bỏ.
  • Toast thất bại mang các action trong catalog (retry, open-logs, copy qua OpActionRetry, OpActionOpenLogs, OpActionCopy). Retry gắn với đúng id bản ghi terminal nguồn (retryId): op id của sự kiện phải trỏ tới một bản ghi lịch sử tồn tại và đã kết thúc ở failed, và đúng id đó là thứ RetryOp phát lại. Không khớp thì không có nút Retry, và một retry không có id gắn kèm sẽ fail-closed dạng record unavailable thay vì quét tìm bản thay thế có thể phát lại nhầm lần chạy. Notification thành công không mang action nào.

Sức khỏe: Doctor và Audit toàn vẹn ​

Thanh sức khỏe ở đầu dashboard chạy hai kiểm tra chỉ đọc cho dự án đang chọn: RunDoctor (chẩn đoán máy và dự án, kèm nút sửa từng mục) và RunIntegrityAudit (audit toàn vẹn dự án không cần container). Cả hai đều đi qua các route HealthService trong desktop/frontend/services/bridge.js (runDoctor(project), runIntegrityAudit(project)); tiến trình audit phát trên sự kiện audit:progress. Các lượt chạy dài có thể hủy (CancelDoctorRun, CancelAudit); hủy audit sẽ loại bỏ kết quả đến muộn và banner giữ nguyên kết quả hoàn tất gần nhất.

Lượt chạy doctor bị giới hạn theo thư mục gốc của dự án đang chọn: các mục kiểm tra gắn với dự án đọc file của đúng dự án đó, và báo cáo được gom theo đúng dự án. Dự án có tên nhưng không phân giải được thành thư mục sẽ bị từ chối kèm hướng dẫn chọn dự án, thay vì audit nhầm thư mục làm việc hiện tại. Khi chưa chọn dự án, lượt chạy ở mức máy (thư mục làm việc) thuộc scope rỗng.

Nút sửa của Doctor có ý nghĩa gì ​

Mỗi mục kiểm tra của doctor thuộc một trong ba loại. Fix sửa ngay trong một lượt bấm. Preview fix mở kế hoạch chạy thử (dry-run) và phải được xác nhận rõ ràng trước khi thay đổi bất cứ thứ gì. Các mục không có cách sửa an toàn không hiện nút sửa; hãy làm theo gợi ý và lệnh CLI được đề xuất. Mục kiểm tra nào chưa từng được allowlist phân loại sẽ bị từ chối thay vì được gắn nút.

ID mục kiểm traNútCách sửa làm gì
host.govard.homeFixSửa quyền các thư mục runtime của Govard
host.compose.spamFixDọn các file compose tồn quá 7 ngày
host.govard.registryFixSửa đường dẫn registry dự án (gặp thư mục nằm ở vị trí file thì từ chối, không bao giờ tự xóa)
host.search.index_blockPreview fixGỡ nghẽn chỉ mục tìm kiếm bằng một lệnh ghi vào container search đang chạy
project.runtime.imagesPreview fixTải các image Docker runtime còn thiếu (tốn mạng)
project.config.auditPreview fixChuyển các key cấu hình cũ sang khối services trong .govard.yml
project.config.driftPreview fixĐồng bộ .govard.yml với framework profile đã phát hiện
project.profile.syncPreview fixBù các thiếu hụt từ framework profile, giữ nguyên mọi thiết lập người dùng đã khai báo
docker.daemonKhông cóKhông sửa trên desktop; làm theo gợi ý
docker.composeKhông cóKhông sửa trên desktop; làm theo gợi ý
host.port.80Không cóKhông sửa trên desktop; làm theo gợi ý
host.port.443Không cóKhông sửa trên desktop; làm theo gợi ý
host.disk.scratchKhông cóKhông sửa trên desktop; làm theo gợi ý
host.network.outboundKhông cóKhông sửa trên desktop; làm theo gợi ý
host.ssh.agentKhông cóKhông sửa trên desktop; làm theo gợi ý
host.deps.dockerKhông cóKhông sửa trên desktop; làm theo gợi ý
host.deps.remoteKhông cóKhông sửa trên desktop; làm theo gợi ý

Các cách sửa chạm tới dự án (host.search.index_block và mọi mục project.*) yêu cầu đang chọn một dự án trong ứng dụng desktop; các cách sửa toàn cục của máy có thể chạy mà không cần chọn dự án. Sau mỗi lần sửa, panel tự chạy lại doctor trong nền, nên banner chỉ tắt khi một lượt chạy mới sạch hoàn toàn.

Cách sửa rủi ro: xem trước, xác nhận, hết hạn ​

PreviewDoctorFix trả về kế hoạch chạy thử (tóm tắt kèm danh sách hành động) mà không thay đổi gì; ApplyDoctorFix kèm xác nhận rõ ràng mới áp dụng. Hiểu đơn giản: một bản xem trước chỉ hiệu lực trong thời gian ngắn (khoảng 15 phút) và dùng được một lần. Nếu file dự án hay trạng thái container đổi sau lúc xem trước, lệnh áp dụng bị từ chối và panel yêu cầu xem trước lại, lúc đó hãy xác nhận trên kế hoạch mới. Đóng bản xem trước hay chuyển dự án sẽ hủy bản xem trước đang chờ.

Phạm vi audit toàn vẹn ​

Desktop chỉ chạy đúng mục integrity không cần container, cùng phân tích mà govard audit run --checks integrity thực hiện. Hai dòng lint và profiler vẫn hiển thị trong kết quả ở trạng thái Enabled=false kèm lý do và hướng tới CLI (govard audit run --checks lint, govard audit run --checks profiler); chúng không bao giờ chạy từ desktop và không bao giờ bị ẩn. GetAuditResult đọc một lượt chạy đã lưu, GetAuditDiff tóm tắt chênh lệch finding giữa hai lượt chạy đã lưu, còn LastAuditResult điền banner từ lượt chạy hoàn tất gần nhất mà không chạy gì thêm. Panel hỏi RunningAuditOp khi mở, nên nút hủy luôn có mặt kể cả khi lượt chạy đã bắt đầu từ trước lúc mở panel.

Banner luôn suy ra từ kết quả mới nhất của đúng dự án đang chọn. Chuyển dự án sẽ tính lại banner của dự án đó và hiển thị trạng thái đang tải hoặc trống trong lúc chờ, không bao giờ hiện banner của dự án khác.

BannerÝ nghĩa
Doctor, đỏ (error)Báo cáo doctor mới nhất có ít nhất một mục lỗi. Banner nêu số vấn đề và số mục sửa được bên dưới
Doctor, vàng (warning)Chỉ có cảnh báo, không có lỗi
Không có banner doctorChưa chạy lần nào, hoặc lượt chạy mới nhất đạt toàn bộ. Chỉ lượt chạy sạch mới tắt được banner cũ
Audit, vàng (warning)Kết quả toàn vẹn mới nhất còn finding (hoặc trạng thái chưa đạt). Banner nêu số finding; panel liệt kê từng finding cùng hai dòng lint và profiler bị tắt
Không có banner auditDự án này chưa audit lần nào, hoặc lượt chạy mới nhất sạch. Các lượt bị hủy hay lỗi không thay thế kết quả hoàn tất gần nhất

Tool Runner ​

Bảng Tool Runner chạy các lệnh tool của framework từ danh sách preset do backend sở hữu. Chọn project, chọn preset, điền tham số rồi bấm Run. Output được stream vào panel (giữ 2.000 dòng gần nhất, cùng giới hạn với các panel log). Dropdown Recent runs nạp lại preset cùng tham số của một lần chạy trước (các giá trị đã scrub hiển thị *** sẽ không được nạp lại). Khi chưa chọn project, bảng hiển thị Select a project to run tools. và không cho chạy.

Giao diện nằm trong React island desktop/frontend/islands/ToolPalette.tsx với các helper thuần trong desktop/frontend/modules/tool-palette.js. Nó gọi Go chỉ qua services/bridge.js:

Wrapper frontendRoute backend
listToolPresets(project)ToolService.ListToolPresets
runTool(project, presetId, args)ToolService.RunTool
cancelToolRun(opId)ToolService.CancelToolRun
getToolHistory(project)ToolService.GetToolHistory

Tiến trình chạy được stream qua tool:output; kết quả đến qua tool:completed hoặc tool:failed.

Preset ​

Preset được định nghĩa trong internal/desktop/tool_runner.go. Bảng liệt kê các preset theo framework của project đang chọn cộng thêm nhóm generic; framework lạ hoặc rỗng chỉ nhận nhóm generic.

ID presetChạy lệnhTham sốÁp dụng cho
magento-cache-flushbin/magento cache:flushtype (tùy chọn, ^[a-z_]+$)Họ Magento
magento-cache-cleanbin/magento cache:cleantype (tùy chọn, ^[a-z_]+$)Họ Magento
magento-setup-di-compilebin/magento setup:di:compilekhông cóHọ Magento
magento-indexer-reindexbin/magento indexer:reindexindexer (tùy chọn, ^[a-z_]+$)Họ Magento
laravel-migratephp artisan migratedatabase (flag tùy chọn), force (flag tùy chọn, true/false/1/0)Laravel
laravel-cache-clearphp artisan cache:clearkhông cóLaravel
laravel-config-clearphp artisan config:clearkhông cóLaravel
composer-installcomposer installno-dev (flag tùy chọn, true/false/1/0)Mọi project
npm-installnpm installkhông cóMọi project
npm-run-buildnpm run buildkhông cóMọi project

Họ Magento gồm magento1, magento2, mageos và openmage (kể cả các alias framework đã đăng ký dẫn xuất từ Magento).

Các preset chậm (magento-setup-di-compile, magento-indexer-reindex, laravel-migrate, composer-install, npm-install, npm-run-build) chạy với trần 15 phút; các lệnh xóa cache nhanh giữ trần mặc định 2 phút. Timeout báo rõ thời lượng, và entry history ghi lần chạy là failed (timeout) hoặc cancelled (hủy).

Đảm bảo không shell ​

Bảng chỉ gửi ID preset kèm map tham số. Argv thực thi do backend sở hữu: ID preset lạ bị từ chối trước khi có process nào tồn tại, và tham số được kiểm tra theo ArgSpec của preset (tên lạ, thiếu tham số bắt buộc, giá trị dài quá 256 ký tự và giá trị ngoài pattern của arg đều bị loại; arg không có pattern riêng dùng ^[A-Za-z0-9_@+.,:/=-]+$, loại mọi khoảng trắng và ký tự đặc biệt của shell mà không có ngoại lệ quoting). Tham số hợp lệ được ráp vào argv của preset theo thứ tự ArgSpec (flag thành --name hoặc --name=value), rồi chạy trực tiếp qua exec.CommandContext: không shell, không sh -c, không nối chuỗi, trong mọi trường hợp.

Đảm bảo hủy (cancel) ​

Mỗi project chỉ chạy một tool tại một thời điểm. Lần Run thứ hai khi đang có lần chạy active bị từ chối với lỗi already-running, và khóa được thả khi chạy xong hoặc khi hủy. CancelToolRun kill process đang chạy qua op registry và thả khóa; hủy một ID không tồn tại hoặc đã xong là no-op vô hại trả về false, và giá trị trả về đồng bộ chính là xác nhận cho phía gọi hủy.

Sự kiện terminal mang theo generation của lần chạy (OpID, Gen trong ToolResultPayload). Bảng chỉ áp dụng receipt tool:completed hoặc tool:failed khi generation của nó vẫn còn hiện hành (IsCurrentToolGeneration ở backend, applyToolTerminalEvent trong modules/tool-palette.js), và một lần chạy đã kết thúc không bao giờ lật lại thành đang chạy. Các batch tool:output từ lần chạy đã hủy, đã timeout hoặc đã bị thay thế đều bị bỏ, nên rerun nhanh không bao giờ trộn output cũ; còn receipt terminal luôn đến để history và trạng thái được chốt.

Lịch sử và scrub secret ​

Mỗi lần chạy được duyệt đều ghi vào file history JSONL riêng từng project trong thư mục tool-history/ thuộc Govard home, giữ tối đa 50 lần chạy gần nhất (entry cũ nhất bị loại trước). Entry lúc duyệt mang trạng thái unknown rồi được nâng thành success, failed hoặc cancelled trước khi sự kiện terminal phát ra, nên lần chạy mất terminal (crash) đọc là unknown, không bao giờ là success. Lỗi ghi history không bao giờ làm hỏng lần chạy.

Tham số được scrub ngay khi ghi, nên secret thô không bao giờ chạm tới file: giá trị dưới tên key secret (password, passwd, passphrase, secret, token, apikey, accesskey, privatekey, pwd, kể cả khớp hậu tố như db-password hay apiKey) thành ***, và text mang dạng secret (cặp KEY=value, URL có credential dạng scheme://user:pass@, block private-key PEM) cũng bị che tương tự. Có một giới hạn: giá trị secret trần đặt dưới tên key không phải secret thì không phân biệt được với giá trị vô hại và không bị phát hiện, nên chỉ tên key và dạng giá trị mới quyết định việc scrub. File được ghi nguyên tử (atomic) với quyền hạn chế.

Tab Data ​

Tab Data quản lý database của project đang chọn và các snapshot của nó: bảng thông tin kết nối đã che, query tự do có kiểm soát, dump/import với tiến trình streaming, và liệt kê/tạo/export snapshot cùng khôi phục qua cổng preview.

Query chỉ đọc theo mặc định, ghi phải opt-in rõ ràng ​

DBService.RunDBQuery(project, sql, allowWrite, confirmToken) phân loại mọi câu lệnh trước khi chạm vào bất cứ thứ gì:

  • Các từ mở đầu chỉ đọc là SELECT, WITH, SHOW, DESCRIBE, DESC, EXPLAIN. Mọi từ khác đi theo đường ghi.
  • Từ mở đầu trông như đọc là chưa đủ: toàn bộ câu lệnh còn bị quét tìm các từ thay đổi dữ liệu (DELETE, UPDATE, INSERT, INTO, UNION và các từ tương tự, các lệnh khóa đọc như FOR UPDATE), nên một lệnh ghi giấu trong thân query vẫn đi theo đường ghi.
  • Input nhiều câu lệnh luôn bị từ chối. Việc từ chối diễn ra trước khi resolve project, nên câu lệnh bị từ chối không chạm vào gì cả.

Một lệnh ghi cần đồng thời hai thứ: allowWrite=true cộng token xác nhận của từng project từ DBService.GetDBWriteConfirmToken(project). Luồng UI là: huy hiệu mode coi mọi thứ chưa chắc chắn là "write", checkbox ghi mở hộp thoại xác nhận, hộp thoại lấy token rồi trả lại cho RunDBQuery. Định dạng token là confirm-write:<tên-project-viết-thường> và do phía Go sở hữu; island chỉ mang token đi. Token chống bấm nhầm, không chống phá hoại có chủ ý: bản thân desktop đã giữ credentials.

Khôi phục chỉ qua preview cộng xác nhận, không có đường tắt ​

SnapshotService.PreviewSnapshotRestore(project, name, dbOnly, mediaOnly) không ghi gì cả. Nó báo chính xác những gì lệnh apply sẽ thay thế (willReplaceDb, willReplaceMedia, kèm dòng summary cho người đọc) và trả về một previewToken dùng một lần. Nút xác nhận giữ disabled cho tới khi preview này hiển thị.

SnapshotService.ApplySnapshotRestore(project, name, previewToken, confirm, dbOnly, mediaOnly) đòi token cộng confirm=true. Thiếu một trong hai, lệnh khôi phục không bao giờ chạy. Token gắn với project, snapshot và scope, hết hạn sau 15 phút, và bị tiêu thụ bởi một lần apply hợp lệ, nên phát lại không bao giờ tới được engine. Lần apply bị từ chối không tiêu thụ gì, nên xác nhận lại sau đó vẫn được. Scope mặc định là full, hoặc dbOnly / mediaOnly từ nút radio scope.

Cam kết che dữ liệu nhạy cảm ​

  • DBService.GetDBInfo trả về struct không có trường password theo thiết kế. hasPassword chỉ báo có cấu hình password hay không, và displayUrl hiển thị password dưới dạng ****.
  • Output của query đi qua sanitizeUIText, giới hạn 20.000 ký tự, đồng thời loại bỏ escape ANSI và byte điều khiển.
  • Frontend chỉ hiển thị thông tin DB qua allowlist pickDisplayDBInfo (project, engine, host, port, database, user, hasPassword, displayUrl): mọi khóa lạ, kể cả password dưới bất kỳ tên nào, đều bị loại trước khi chạm tới DOM.
  • Cố tình không có lịch sử query: không có gì gõ vào ô query được lưu lại, nên không có secret nào kẹt lại trong đó.

Ngữ nghĩa của cancel ​

DBService.CancelDBOp(opId) và SnapshotService.CancelSnapshotOp(opId) dừng theo dõi của desktop trước: định danh op bị xóa để các sự kiện muộn từ goroutine đang tắt không bao giờ tới UI, và người dùng nhận một sự kiện kết thúc db:failed / snapshot:failed. Những gì diễn ra bên dưới thì khác nhau:

  • Dump/import chạy qua tiến trình docker khởi động cùng context của op, nên cancel kích hoạt context đó và kill tiến trình. Dump bị hủy còn xóa file .partial của nó, và file đã có sẵn tại đích vẫn nguyên vẹn.
  • Khôi phục snapshot gọi engine, mà engine không nhận context một khi import đã chạy. Cancel dừng op desktop và các sự kiện của nó trong khi import bên dưới có thể vẫn chạy tới cùng, giống như ngắt CLI giữa chừng. Thông báo nói đúng như vậy: "<Kind> tracking stopped by user; the underlying operation may run to completion" (ví dụ "Snapshot restore tracking stopped by user; ..."). Nó không bao giờ báo đã hủy điều chưa được hủy.

Kiểm tra đĩa trước khi chạy ​

  • DumpDB ước lượng kích thước database, rồi thăm dò dung lượng trống tại thư mục cha gần nhất còn tồn tại của đích trước khi tạo bất kỳ thư mục nào hay khởi động tiến trình nào. Lần từ chối không để lại file, thư mục hay tiến trình nào.
  • ImportDB kiểm tra file đồng bộ trước khi đăng ký op: file phải là file thường không rỗng (không phải thư mục), mang header dump được nhận diện (mysqldump/MariaDB hoặc PostgreSQL, gzip được xử lý trong suốt), và engine phải khớp database của project. File bị từ chối không bao giờ mở writer tới database.
  • ExportSnapshot từ chối đích là thư mục hoặc có thư mục cha không tồn tại. Xác nhận ghi đè file đã có là việc của UI: các hộp thoại lưu trả về { path, exists } để island hỏi trước khi gọi dump hay export.

Route và sự kiện ​

Các route của bridge trong desktop/frontend/services/bridge.js trỏ tới bindings đã sinh (đã đối chiếu với DBService, SnapshotService và data_dialog.go):

RouteGo service.method
GetDBInfoDBService.GetDBInfo
RunDBQueryDBService.RunDBQuery
DumpDBDBService.DumpDB
ImportDBDBService.ImportDB
CancelDBOpDBService.CancelDBOp
PickDumpDestinationDBService.PickDumpDestination
PickImportSourceDBService.PickImportSource
GetDBWriteConfirmTokenDBService.GetDBWriteConfirmToken
ListSnapshotsSnapshotService.ListSnapshots
CreateSnapshotSnapshotService.CreateSnapshot
PreviewSnapshotRestoreSnapshotService.PreviewSnapshotRestore
ApplySnapshotRestoreSnapshotService.ApplySnapshotRestore
ExportSnapshotSnapshotService.ExportSnapshot
CancelSnapshotOpSnapshotService.CancelSnapshotOp
PickExportDestinationSnapshotService.PickExportDestination

Các sự kiện backend trong internal/desktop/events.go:

Sự kiệnMục đích
db:progressTiến trình dump/import có cấu trúc (opId, generation, số byte, phần trăm)
db:completedDump/import hoàn tất
db:failedDump/import thất bại hoặc bị hủy
snapshot:completedTạo/khôi phục/export snapshot hoàn tất
snapshot:failedOp snapshot thất bại hoặc bị hủy

Các op snapshot chỉ phát sự kiện kết thúc, không có sự kiện tiến trình. Lựa chọn trống trong hộp thoại (lưu hay mở) mà không có lỗi nghĩa là người dùng đã hủy, và island gửi đúng đường dẫn đã chọn tới dump/import/export, không bao giờ tự bịa đường dẫn. Hộp thoại dump mặc định là <project>-dump.sql; hộp thoại export mặc định là <name>.tar.gz, khớp với CLI.


Trạng thái môi trường và an toàn đồng bộ ​

Môi trường chỉ chạy một phần báo degraded (màu hổ phách) thay vì xanh: một số container chạy trong khi các container khác đã dừng hoặc crash. Project degraded vẫn là project đang chạy — nút Stop vẫn bật và thao tác chính vẫn là Restart, không bao giờ là Start.

Các thao tác thay đổi môi trường (start, stop, restart, pull, delete, toggle) bị từ chối khi một lượt sync đang đổ vào cùng project, kèm tên remote đang giữ nó. Bộ chặn phân giải tên, domain và đường dẫn qua registry project, nên gọi project bằng cách nào cũng bị chặn như nhau. Sync nền bị giới hạn bởi sự im lặng, không phải thời gian tuyệt đối: sync còn stream output là sync đang chạy, sync im lặng quá 15 phút thất bại với timeout SyncFailed và nhả project thay vì khóa tới khi khởi động lại app.

Nút Start trên Desktop luôn recreate container (up kèm --force-recreate và --remove-orphans), nên service bị tắt hay image đổi không bao giờ bị áp dụng dở dang. Dữ liệu nằm ở volume nên recreate an toàn.

Mặc định preset sync: preset files và media bật nén mặc định (rsync -z), khớp CLI và preset full. Lựa chọn đã đổi trong phiên hiện tại vẫn thắng. Nút "Delete Missing Files" của preset full chuyển --delete tới bootstrap, nên file không có trên remote bị xóa local — dấu tick trước đây bị bỏ qua nay xóa dữ liệu thật, vì vậy nút này mang cảnh báo.

Onboarding phát hiện checkout DDEV và truyền --migrate-from ddev cho init, khớp đường Warden hiện có. Giá trị host:port giữ port trong khi host được thêm hậu tố .test (myapp:8080 thành myapp.test:8080).


Phím tắt (Keyboard Shortcuts) ​

Phím tắtThao tác
Ctrl+, / Cmd+,Mở Cài đặt
EscĐóng Cài đặt

Các thao tác Remote trên Desktop ​

Thao tácHành vi
Mở Database (Remote)Gọi lệnh govard open db -e <remote> --client
Mở terminal SSH (Remote)Ưu tiên các terminal gốc của Linux, fallback về giao thức ssh://
Mở SFTP (Remote)Ưu tiên ứng dụng FileZilla, fallback về giao thức sftp://
Mở URL admin (Remote)OpenRemoteURL dựng https://<url hoặc host>/<admin path> từ admin path của framework

Đối với phương thức cấu hình auth.method: ssh-agent, ứng dụng Desktop tái sử dụng SSH_AUTH_SOCK và thăm dò socket tại /run/user/<uid>/keyring/ssh trên môi trường Linux.

Mọi thao tác đều resolve remote trước khi dựng bất cứ thứ gì, và một remote không resolve được cả url lẫn host sẽ bị từ chối kèm lỗi thay vì được mở. URL admin cùng các launcher SFTP/SSH đều sẽ fallback về chuỗi localhost, tức là mở chính máy của lập trình viên trong khi vẫn báo rằng remote đã được mở.

Remote synthetic sandbox được resolve từ container mà govard sandbox up đã tạo, không bao giờ từ block remotes.sandbox của project, nên một block không khai báo host không thể khiến thao tác nào trỏ về localhost. Nó xuất hiện trong bảng remote bất cứ khi nào container đó trả lời, và không phụ thuộc project có cấu hình block hay không. Đó là điều kiện kích hoạt mà govard remote list cũng dùng, chứ không phải cùng một quy tắc về dòng: bảng của app bỏ qua sandbox mà nó không resolve được, trong khi remote list luôn in một dòng và đặt trạng thái vào cột HOST. Khi block đã được cấu hình mà không có container đang chạy, dòng đó bị bỏ khỏi bảng và lý do trả về dưới dạng warning trong bảng; một project không có block và cũng không có container thì không có dòng nào và không có warning nào. Xem thêm Cấu hình remote sandbox.

Mở Cơ sở dữ liệu local ​

  • Xác định host và port docker được publish trước.
  • Fallback về PHPMyAdmin nếu trình kết nối DB client được cấu hình bị thất bại.

Tab Deploy ​

Tab Deploy tái sử dụng cùng các đường engine với CLI, chia làm hai mốc theo mức độ ảnh hưởng.

M1: hiển thị chỉ đọc ​

Chế độ xem trong panelLệnh CLI tương đương
Statusgovard deploy status [remote]
Releasesgovard deploy releases [remote]
Plangovard deploy plan [remote]
Checkgovard deploy check [remote]

Bốn lệnh gọi này không bao giờ thay đổi trạng thái target, với một ngoại lệ được công bố: trên target dùng chiến lược symlink, lệnh check thực hiện một lần ghi probe tạm thời để kiểm tra khả năng swap nguyên tử, rồi xóa nó. Panel luôn hiển thị ghi chú mỗi khi check báo probe đã chạy, để check không bao giờ bị trình bày như thuần quan sát. Việc tải dữ liệu bị chặn sau picker remote: khi chưa chọn remote, không lệnh gọi nào rời khỏi panel.

M2: thao tác có cổng bảo vệ ​

Thao tác trong panelLệnh CLI tương đương
Xem trước + áp dụng rollbackgovard deploy rollback [remote]
Xem trước + khởi động sandboxgovard sandbox up
Xem trước + gỡ sandboxgovard sandbox down
Thông tin kết nối + mở shellgovard sandbox ssh

Mọi thay đổi đều chạy đúng hợp đồng preview-plus-confirm:

  1. Preview cho thấy chính xác điều apply sẽ làm (release và chiến lược của rollback, hoặc container của sandbox) và cấp một token dùng một lần gắn với đúng thao tác và tham số đó.
  2. Apply tiêu thụ token và còn đòi confirm tường minh: nút apply trong panel luôn bị vô hiệu cho tới khi preview đã hiện VÀ hộp kiểm ("Tôi đã đọc preview ...") được tick, nên không một cú nhấp đơn nào có thể đổi trạng thái target.

Token hết hạn sau 15 phút; token lạ, đã dùng, sai chủ hoặc hết hạn đều bị từ chối trước mọi lệnh gọi engine. Preview không còn khớp target (có deploy mới xen vào, hoặc container sandbox đã đổi) bị từ chối là stale, và người dùng xem preview lại. Rollback kèm --with-db chỉ có trên CLI: khôi phục database mang tính hủy dữ liệu và giữ confirm riêng ở đó. Sandbox rehearse khởi đầu trống; seeding chỉ có trên CLI.

Tính trung thực khi cancel ​

Dừng một lệnh đang chạy giữa chừng sẽ báo rõ engine đã để lại gì, không bao giờ chỉ nói "cancelled" trống. Rollback in-place nêu record release có được khôi phục không và cửa sổ maintenance có được đóng không; rollback symlink nêu có release nào đã được kích hoạt trước khi dừng không; khởi động hoặc gỡ sandbox nêu trạng thái container đọc lại sau đó (vẫn chạy, đã dừng, không còn gì, hoặc không xác định được). Panel hiển thị lệnh bị cancel đúng là cancelled kèm các ghi chú đó, không bao giờ như một lỗi, và không gợi nút thử lại cho nó.


Cấu hình ưu tiên của Desktop (Desktop Preferences) ​

Các cài đặt cấu hình ưu tiên được lưu trữ tại file:

~/.govard/desktop-preferences.json

Các cấu hình ưu tiên hiện được ghi nhớ:

  • Theme giao diện (light/dark)
  • Proxy target
  • Trình duyệt ưa thích
  • Trình kết nối database client ưa thích

Chế độ phát triển (Dev Mode) ​

Yêu cầu: Go, Node.js 24+, pnpm, và trên Linux cần libgtk-4-dev cùng libwebkitgtk-6.0-dev. Giao diện desktop được Vite đóng gói vào desktop/frontend/dist và được nhúng thẳng vào binary Go.

bash
make frontend                      # Vite build vào desktop/frontend/dist
DISPLAY=:1 govard desktop --dev    # Vite HMR + build lại backend Go
make bindings                      # sinh lại JS bindings từ các Go service

govard desktop --dev chạy Vite tại http://localhost:5173 rồi chạy ứng dụng với FRONTEND_DEVSERVER_URL trỏ vào đó; Wails chỉ proxy dev server khi build không có tag production. Không còn bước Wails CLI và không còn wails.json.

make frontend là bắt buộc: bản build desktop mà bỏ qua bước này sẽ nhúng dist/ rỗng và chỉ hiện cửa sổ trắng. Mọi đường build desktop tự động (CI, goreleaser, scripts/build-macos-pkg.sh, install.sh --source) đều chạy bước này trước.

Mở trực tiếp Vite server (http://localhost:5173) hoặc thư mục dist/ sẽ hiển thị đúng khung giao diện đó với dữ liệu mock kèm cảnh báo "Desktop bridge not available", đủ để kiểm tra style và layout; muốn có dữ liệu dự án thực tế thì phải mở trong cửa sổ ứng dụng.


Chế độ Preview và Behaviour Test ​

Chế độ preview chạy chính main.js trong Chrome thường, mọi lời gọi binding Go đều được trả lời từ fixture JSON, nên việc làm giao diện và các kịch bản tự động không cần backend Go hay cửa sổ ứng dụng.

bash
pnpm --dir desktop/frontend dev    # Vite tại http://localhost:5173
# sau đó mở http://localhost:5173/preview.html

preview.html là bản sao của index.html, chỉ khác ở script bootstrap của preview và container của demo island. Mọi thay đổi markup trong index.html phải được chép sang preview.html; TestPreviewHTMLMirrorsIndexHTML sẽ báo lỗi khi hai file lệch nhau. Không có gì trong desktop/frontend/preview/ đi vào bản build production.

Trang cung cấp một bề mặt điều khiển duy nhất, window.__govardPreview:

Thành phầnMục đích
reset()Xóa các fixture đã cài và danh sách lời gọi đã ghi
installFixtures(name)Nạp desktop/frontend/preview/fixtures/<name>.json
pushEvent(name, data)Phát một sự kiện backend tới các subscription của ứng dụng
getCalls()Trả về các lời gọi binding mà ứng dụng đã thực hiện

Một file fixture là mảng JSON gồm các phần tử { "service", "method", "args", "result" }; lời gọi lỗi dùng "error" (chuỗi thông báo) thay cho "result". Giá trị dùng đúng tên trường JSON mà các Go service trả về (ví dụ cpuUsage, không phải CPUUsage). Một lời gọi khớp với phần tử có cùng service, method và tham số; nếu không có thì lấy phần tử đầu tiên cùng service và method, cuối cùng là giá trị mặc định sinh sẵn trong preview/route-defaults.generated.js.

Chế độ ghi (record mode) thu fixture thật từ ứng dụng đang chạy:

bash
GOVARD_PREVIEW_RECORD=1 go run ./cmd/govard desktop --dev

Mọi lời gọi binding vẫn đi tới backend thật và đồng thời được ghi thêm vào preview/fixtures/<mode>.json, trong đó <mode> là chế độ sidebar hiện tại. Hãy đổi tên file theo module mà nó thuộc về trước khi commit.

Behaviour test điều khiển preview.html qua raw CDP trong Chrome headless, mỗi kịch bản dùng một Vite server tạm riêng:

bash
make test-frontend-behaviour                                     # tự tìm google-chrome hoặc chromium
make test-frontend-behaviour CHROME_BIN=/opt/google/chrome/chrome

Các kịch bản nằm trong tests/frontend/behaviour/*.behaviour.test.mjs và CI chạy chúng sau make test-frontend.

Các React island được mount qua mountIsland(containerId, element) trong desktop/frontend/islands/mount.js. Hàm trả về { unmount() }, hoặc null khi không có container, để main.js vẫn khởi động được trên trang không có nó.


Cấu trúc thư mục Frontend ​

FileMục đích
desktop/frontend/index.htmlĐiểm vào HTML chính
desktop/frontend/main.jsKhởi tạo, lắng nghe sự kiện, quản lý tab và state
desktop/frontend/services/bridge.jsCầu nối gọi RPC tới Go backend của Wails; là module duy nhất được phép gọi Go
desktop/frontend/services/events.jsĐăng ký sự kiện từ backend; là module duy nhất được phép dùng @wailsio/runtime
desktop/frontend/bindings/Sinh tự động từ các Go service bằng make bindings; được commit, không sửa tay
desktop/frontend/state/store.jsState UI dùng chung (dự án đang chọn, bộ lọc)
desktop/frontend/modules/Các module tính năng (dashboard, logs, remotes, v.v.)
desktop/frontend/ui/toast.jsHệ thống hiển thị thông báo toast
desktop/frontend/utils/dom.jsCác helper xử lý DOM dùng chung

Hành vi ở chế độ test (Test Mode) ​

Cách thức truy cậpTrạng thái BackendDữ liệu hiển thị
Cửa sổ ứng dụngBindings hoạt độngDữ liệu dự án thực tế
Vite dev (localhost:5173) hoặc thư mục dist/Bindings không khả dụngDữ liệu mock fallback + toast cảnh báo

Một test Go (tests/desktop_frontend_bridge_guard_test.go) sẽ fail nếu bất kỳ file frontend nào ngoài services/bridge.js và services/events.js chạm vào window.go, window.runtime, desktopBridge.runtime, @wailsio/runtime hoặc bindings/. Hai module này được type bằng JSDoc kèm // @ts-check, nên pnpm typecheck kiểm tra đúng những shape chúng khai báo; thêm một test Go thứ hai (tests/desktop_bindings_contract_test.go) fail khi một route trong bridge.js không còn khớp với bindings đã sinh, nhờ đó việc đổi tên method phía Go bị CI chặn thay vì để người dùng phát hiện.

Đóng cửa sổ ​

Đóng cửa sổ sẽ giữ Govard chạy trong tray khi bật Run in background (mặc định) và máy có tray host; nếu không thì ứng dụng thoát, để không bao giờ rơi vào tình trạng cửa sổ bị ẩn mà không có cách mở lại. GNOME nguyên bản không có tray host: cần bật extension AppIndicator, nếu không thì đóng cửa sổ là thoát. Lệnh govard desktop doctor cho biết máy đang ở trường hợp nào, và trang Settings sẽ hiển thị cảnh báo khi không có tray. Menu tray cho phép hiện/ẩn cửa sổ, liệt kê dự án (dự án đang chạy lên trước), start/stop, mở trên trình duyệt và thoát.

Hỗ trợ nền tảng ​

Nền tảngỨng dụng desktopGhi chú
Linux, Ubuntu 24.04+ / Debian 13+CóGTK 4 và WebKitGTK 6.0
Linux, Ubuntu 22.04 / Debian 12KhôngChỉ cài CLI; không có WebKitGTK 6.0
macOSChưaGói chỉ có CLI; govard self-update giữ nguyên binary desktop đang cài

Danh tính launcher trên Linux ​

Gói deb cài launcher với tên io.github.ddtcorex.govard.desktop, và ứng dụng desktop công bố đúng chuỗi đó làm GTK application id (DesktopApplicationID trong internal/desktop/launch.go). Trên Wayland, GNOME gom cửa sổ vào launcher theo id này: nó tra file desktop "<application id>.desktop" và không dùng StartupWMClass cho surface Wayland. Cửa sổ có id không khớp launcher nào sẽ thành window-backed app, nên dock hiện thêm một icon thứ hai với tên khác bên cạnh icon đã ghim. Hằng số, tên file trong packaging/linux/, StartupWMClass và mục GoReleaser phải luôn khớp nhau — tests/linux_desktop_entry_test.go sẽ fail nếu chúng lệch.


Ghi chú về Kiến trúc (Architecture Notes) ​

Ứng dụng Desktop được thiết kế tập trung tối đa vào các quy trình thao tác vận hành:

  • Điểm vào desktop: cmd/govard-desktop
  • Wails bindings: internal/desktop
  • Khung giao diện frontend: desktop/frontend/index.html
  • Khởi tạo & Lắng nghe sự kiện: desktop/frontend/main.js
  • Cầu nối gọi backend: desktop/frontend/services/bridge.js
  • Quản lý State: desktop/frontend/state/store.js
  • Module tính năng: desktop/frontend/modules/

Để hiểu sâu hơn về kiến trúc hệ thống, xem thêm tài liệu Kiến trúc.


SSL và Tên miền | Kiến trúc

Phát hành theo giấy phép MIT.