Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ versioning theo [SemVer](https://semver.org). **8sync rule:** mỗi PR cập nh

## [Unreleased]

## [0.110.69] — 2026-10-22
### Fixed
- **Guard tắt ngay trình duyệt của phòng ăn CPU khi máy quá ngưỡng** (owner: "cpu chạm 45% phải kill … fix triệt để"): một Edge headless của phòng QA vẽ WebGL bằng CPU, ăn 40% máy, CPU lên 94% dù ngưỡng 45%, mà guard chỉ hạ ưu tiên. Giờ khi CPU ≥ ngưỡng, trình duyệt nào do phòng mở ăn ≥ 15% máy bị tắt cả cây (phòng vẫn sống, lần chụp sau thử lại); trình duyệt của anh không bao giờ bị đụng. Test +1.
- **Dọn Chromium của `8sync shot` không qua WMI nữa**: luật mồ côi của v0.110.67/68 (#216, #217: wmic + taskkill) giờ đọc danh sách tiến trình native (8sync chủ đã chết mới dọn) thay cho WMI (`Get-CimInstance`/`wmic` liệt kê mọi tiến trình mỗi lần chụp); HQ không còn chạy PowerShell/WMI mỗi phút, việc dọn nằm trong `8sync ccc auto revive`. Đã chạy thật trên Windows: hai lần chụp cách 0,1 s đều ra ảnh, Chromium mồ côi bị dọn. Test +1.
- **"⚡ Làm ngay" trên card backlog có người nhận ngay** (owner 2026-10-22, 3d-game: "bấm khẩn cấp gấp đợi lâu kinh khủng không ai làm"): card backlog/blocked chỉ gọi lead/ba; lead đang trong một goal dài, không có ba, `rd` ngồi rảnh, card P0 nằm im. Giờ nút gọi thêm `rd` (R&D) làm thay lead: viết AC + lệnh check, tách card con, `auto move <id> ready` ngay lượt này; lead vẫn nhận tin. Card ở cột khác không đổi. Test +1.
- **Máy bớt nặng tận gốc: guard không còn gọi WMI** (owner: "fix tận gốc … ram, cpu"): mỗi nhịp guard của mỗi dự án chạy `Get-CimInstance Win32_Process` kèm command line qua PowerShell; trên máy owner WmiPrvSE phình tới 12,7 GB RAM và 22% CPU. Giờ 8sync liệt kê process bằng Win32 API trực tiếp (ToolHelp, GetProcessTimes, bộ nhớ, NtQueryInformationProcess cho command line): khoảng 0,1 giây, không qua WMI. WMI chỉ còn là đường dự phòng khi cách mới trả rỗng. Test Windows thật +1.
- **Terminal phòng qua web VPS không còn bỏ cuộc sau 3 giây** (owner: "Fail connect terminal rất nhiều"): trang hiện "đang nối" và thử lại giãn dần (0,6 → 5 giây) suốt một phút; lỗi từ chối vẫn báo ngay.
- **Câu hỏi 5 lựa chọn trong terminal không còn bị cắt còn 3**: terminal xin ít nhất 22 dòng và không thu nhỏ khi bàn phím điện thoại mở. Test +2.
- **Dự án ghim hoặc trong scope mà 0 phòng vẫn có nút "Mở team"** (owner, ide: "mất nút mở lại team"): Văn phòng của dự án đang chọn hiện nút mở team dù dự án chỉ được ghim; `POST /api/team` nhận cả dự án ghim và dự án trong scope/team của auto. Test +1.
### Changed
- **Văn phòng 3D đẹp và dễ đọc hơn** (owner: "qúa xấu fix gấp đi"): phòng có tường, cửa sổ, sàn gỗ, bàn sáng màu, bóng mềm dưới bàn ghế; áo theo vai trò (lead xanh dương, dev xanh ngọc, qa cam, review đỏ, rd ô liu, ui tím); trạng thái hiện ở vòng sáng dưới ghế, màn hình và chấm trên bảng tên; bảng tên nhỏ gọn, đọc rõ ở cả nền sáng và tối, không che người. Test +1.
- **Nút tăng tốc ngay ở Tổng quan từng dự án**: "Ship card approved" và "Thao tác team" (Ship tốc độ cao, Sấm Sét, mở lại, dọn…) ở đầu mỗi khối dự án, đúng dự án đó.

## [0.110.68] — 2026-10-22
### Fixed
- **v0.110.67 bị Windows Defender đỏ mừ trực tiếp** (chuỗi `powershell -Command … Stop-Process` nhúng trong binary → heuristic dropper: mọi máy Windows `8sync up` lên .67 bị quarantine mất exe — máy owner chết lệnh `8sync` hoàn toàn tối 22/10). Sweep zombie giờ dùng WMIC CSV + `taskkill` theo pid mồ côi (cha đã chết) — binary mới qua Defender (hash + chạy OK), logic tách hàm thuần `orphan_pids` + unit test: shot sống không bị đụng, mồ côi chết.
Expand Down
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/cli", "crates/ccc-core"]

[workspace.package]
version = "0.110.68"
version = "0.110.69"
edition = "2021"
license = "PolyForm-Noncommercial-1.0.0"
authors = ["8sync-dev <[email protected]>"]
Expand Down
13 changes: 10 additions & 3 deletions apps/hq-ui/app/api/team/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { withMissing, type ProjectRow } from "@/lib/server/db";
import { bad, json, open, readBody } from "@/lib/server/http";
import { projectOf } from "@/lib/server/office";
import { lastAutoStatus } from "@/lib/server/auto";
import { projectOf, readPins } from "@/lib/server/office";
import { mergedSessions } from "@/lib/server/proc";
import { runBlocked } from "@/lib/server/power";
import { liveProjects } from "@/lib/server/targets";
Expand Down Expand Up @@ -30,8 +31,14 @@ export async function POST(req: Request) {
const added = withMissing(o.db.prepare("SELECT id, path, name, added_at FROM projects").all() as ProjectRow[])
.filter((p) => !p.missing)
.map((p) => p.path);
const want = root.toLowerCase();
const known = [...live, ...added].find((r) => r.toLowerCase() === want);
// Also a pinned project and one in auto's scope or team roots: the sidebar shows them with 0 rooms,
// and "Mở team" there must work (owner 2026-10-22: ide pinned, team gone, no button).
const st = lastAutoStatus();
const listed = (v: unknown) => (Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : []);
const extraRoots = [...readPins().map((p) => p.root), ...listed(st?.repos), ...listed(st?.team_roots)].map((r) => r.replace(/^\\\\\?\\/, ""));
const norm = (r: string) => r.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
const want = norm(root);
const known = [...live, ...added, ...extraRoots].find((r) => norm(r) === want);
if (!known) return bad("thư mục này không có trong HQ: thêm nó ở mục Projects trước", 409);
const r = await runTeam(known, action, typeof task === "string" ? task : null);
return json(r, r.ok ? 200 : r.busy ? 409 : 502);
Expand Down
19 changes: 12 additions & 7 deletions apps/hq-ui/components/hq/live-terminal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,13 @@ import { FILES_MAX, fileError, toBase64 } from "@/lib/attach";
import { isLocalHost } from "@/lib/hosts";
import type { ActivityEvent } from "@/lib/server/activity";
import { activityScreen } from "@/lib/term-activity";
import { afterStream } from "@/lib/term-stream";
import { afterStream, giveUp, keyboardOpen, retryDelay, termRows } from "@/lib/term-stream";

/** Off loopback: no byte (not even the opening ping) this long = the proxy holds the stream back, so poll. */
const FIRST_BYTE_MS = 4000;
/** Polling: gap while the terminal prints, and while it is quiet. */
const POLL_BUSY_MS = 150;
const POLL_IDLE_MS = 600;
/** Polling: polls in a row that may fail (tunnel hiccup) before the terminal says so. */
const POLL_TRIES = 5;
/** Read-only view (an omp session open in another terminal): how often its Nhật ký is read again. */
const READ_ONLY_MS = 2000;

Expand Down Expand Up @@ -78,7 +76,7 @@ function fit(el: HTMLElement, font: string): { cols: number; rows: number } {
const cw = probe.getBoundingClientRect().width / 50 || 8;
const ch = probe.getBoundingClientRect().height || 16;
probe.remove();
return { cols: Math.max(20, Math.floor((el.clientWidth - 16) / cw)), rows: Math.max(8, Math.floor((el.clientHeight - 8) / ch)) };
return { cols: Math.max(20, Math.floor((el.clientWidth - 16) / cw)), rows: termRows(Math.floor((el.clientHeight - 8) / ch), null, false) };
}

/** Decode SSE `data:` lines of `event: out` (base64 bytes) as they arrive. */
Expand Down Expand Up @@ -254,7 +252,9 @@ export function LiveTerminal({ url, kind = "room", token, onEnded }: { url: stri
});
let last = size;
ro = new ResizeObserver(() => {
const s = fit(el, font);
const f = fit(el, font);
// A phone keyboard makes the box short: keep the rows (the box scrolls) so a question's options stay whole.
const s = { cols: f.cols, rows: termRows(f.rows, last.rows, keyboardOpen(window.visualViewport?.height, window.innerHeight)) };
if (s.cols === last.cols && s.rows === last.rows) return;
last = s;
sizeRef.current = s;
Expand Down Expand Up @@ -329,6 +329,7 @@ export function LiveTerminal({ url, kind = "room", token, onEnded }: { url: stri
let gen = 0;
let since = -1;
let fails = 0;
let failingSince = 0;
while (!ac.signal.aborted) {
let p: PollJson;
try {
Expand All @@ -339,8 +340,12 @@ export function LiveTerminal({ url, kind = "room", token, onEnded }: { url: stri
p = body;
fails = 0;
} catch (e) {
if (ac.signal.aborted || e instanceof PollRefused || ++fails >= POLL_TRIES) throw e;
await new Promise((r) => setTimeout(r, POLL_IDLE_MS));
if (ac.signal.aborted || e instanceof PollRefused) throw e;
// The tunnel is down for a moment (phone asleep, Wi-Fi to 4G): say so and keep trying for a minute.
if (fails++ === 0) failingSince = Date.now();
if (giveUp(failingSince, Date.now())) throw e;
setState("connecting");
await new Promise((r) => setTimeout(r, retryDelay(fails)));
continue;
}
setState("live");
Expand Down
Loading
Loading