fix: 修复工作区状态构建明细浮层

This commit is contained in:
Code Queue Review
2026-05-24 09:00:51 +00:00
parent 8321603a63
commit f65dc3f083
6 changed files with 243 additions and 15 deletions
@@ -799,6 +799,7 @@ test("layout smoke verifies desktop and mobile default workbench geometry withou
"layout-narrow-desktop-default",
"layout-mobile-default",
"layout-feedback-352-resource-explorer-removed",
"layout-feedback-437-live-build-overlay",
"layout-left-sidebar-collapse",
"layout-gate-desktop",
"layout-gate-narrow-desktop",
@@ -820,6 +821,7 @@ test("layout smoke verifies desktop and mobile default workbench geometry withou
assert.match(report.safety.statement, /does not send Code Agent chat, call M3 IO/u);
assert.equal(report.checks.find((check) => check.id === "layout-issue-287-future-hardware-status-tabs")?.status, "pass");
assert.equal(report.checks.find((check) => check.id === "layout-feedback-352-resource-explorer-removed")?.status, "pass");
assert.equal(report.checks.find((check) => check.id === "layout-feedback-437-live-build-overlay")?.status, "pass");
assert.equal(report.checks.find((check) => check.id === "layout-issue-288-future-single-table-gate")?.status, "skip");
const desktopDefault = report.checks.find((check) => check.id === "layout-desktop-default")?.observations;
@@ -843,6 +845,11 @@ test("layout smoke verifies desktop and mobile default workbench geometry withou
assert.equal(desktopDefault.wiring.metadataInDetails, true);
assert.equal(desktopDefault.wiring.horizontalScroll.panelScrollWidth <= desktopDefault.wiring.horizontalScroll.panelClientWidth + 2, true);
assert.equal(desktopDefault.noHorizontalOverflow.right, true);
assert.equal(desktopDefault.liveBuildLayout.overlayPositioned, true);
assert.equal(desktopDefault.liveBuildLayout.popoverVisible, true);
assert.equal(desktopDefault.liveBuildLayout.popoverViewportContained, true);
assert.equal(desktopDefault.liveBuildLayout.stableGeometry, true);
assert.equal(desktopDefault.liveBuildLayout.closedByButton, true);
assert.equal(Object.hasOwn(desktopDefault.boxes.shell, "text"), false);
assert.equal(Object.hasOwn(desktopDefault.semanticOverlapChecks[0].boxes["#m3-control-form"], "text"), false);
assert.equal(desktopDefault.failures.length, 0);
@@ -863,6 +870,10 @@ test("layout smoke verifies desktop and mobile default workbench geometry withou
assert.equal(mobileDefault.wiring.metadataInSummary, true);
assert.equal(mobileDefault.wiring.metadataInDetails, true);
assert.equal(mobileDefault.noHorizontalOverflow.right, true);
assert.equal(mobileDefault.liveBuildLayout.overlayPositioned, true);
assert.equal(mobileDefault.liveBuildLayout.popoverVisible, true);
assert.equal(mobileDefault.liveBuildLayout.popoverViewportContained, true);
assert.equal(mobileDefault.liveBuildLayout.stableGeometry, true);
const leftCollapse = report.checks.find((check) => check.id === "layout-left-sidebar-collapse")?.observations;
assert.equal(leftCollapse.status, "pass");
@@ -906,5 +917,6 @@ test("repo-owned web checks expose source build and DEV live layout smoke gates"
assert.match(cloudWebCheckSource, /runDevCloudWorkbenchLayoutSmoke/u);
assert.match(cloudWebCheckSource, /tmp\/dev-cloud-workbench-layout-web-check\.json/u);
assert.match(cloudWebCheckSource, /layout-feedback-352-resource-explorer-removed/u);
assert.match(cloudWebCheckSource, /layout-feedback-437-live-build-overlay/u);
assert.match(cloudWebCheckSource, /compactLayoutSmokeFailures/u);
});
+86 -7
View File
@@ -1140,6 +1140,12 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
summary: "#352 default workbench has no aside#resource-explorer, #explorer-resize, resource tree/capability copy, or reclaimed left panel across desktop and mobile.",
observations: summarizeResourceExplorerRemovalCoverage(viewportResults)
},
{
id: "layout-feedback-437-live-build-overlay",
status: allViewportCoverage(viewportResults, "liveBuildOverlayStable") ? "pass" : "blocked",
summary: "#437 workbench status build details open as an overlay without changing the topbar, workspace, or conversation geometry across desktop and mobile.",
observations: summarizeCoverageGap(viewportResults, "liveBuildOverlay")
},
{
id: "layout-left-sidebar-collapse",
status: Object.values(viewportResults).every((result) => result.leftCollapse?.pass) ? "pass" : "blocked",
@@ -1271,7 +1277,7 @@ export async function runDevCloudWorkbenchLayoutSmoke(args = {}) {
summary: useLiveUrl
? "Live browser layout smoke verifies workbench layout, hit targets, overflow, /gate usability, and the two-column wiring panel only; it is not M3 hardware acceptance."
: "Static local browser layout smoke verifies workbench layout, hit targets, overflow, /gate usability, and the two-column wiring panel only; deployment still requires DEV live verification.",
refs: ["pikasTech/HWLAB#273", "pikasTech/HWLAB#276", "pikasTech/HWLAB#278", "pikasTech/HWLAB#287", "pikasTech/HWLAB#288", "pikasTech/HWLAB#352", "pikasTech/HWLAB#99", "pikasTech/HWLAB#227"],
refs: ["pikasTech/HWLAB#273", "pikasTech/HWLAB#276", "pikasTech/HWLAB#278", "pikasTech/HWLAB#287", "pikasTech/HWLAB#288", "pikasTech/HWLAB#352", "pikasTech/HWLAB#437", "pikasTech/HWLAB#239", "pikasTech/HWLAB#99", "pikasTech/HWLAB#227"],
viewports: layoutViewports.map(({ id, width, height }) => ({ id, width, height })),
checks,
blockers,
@@ -3171,22 +3177,26 @@ async function inspectLiveDom(url, options = {}) {
liveBuildListVisibleWhenClosed: visible("#live-build-list"),
liveBuildDetailWhenOpen: (() => {
const details = document.querySelector("#live-build-summary");
const popover = document.querySelector("#live-build-popover");
const list = document.querySelector("#live-build-list");
if (!details || !list) {
if (!details || !popover || !list) {
return {
visible: false,
rows: 0,
scrollContained: false,
overlay: false,
text: "",
overflowX: 0
};
}
details.open = true;
const popoverStyle = getComputedStyle(popover);
const rows = [...list.querySelectorAll(".live-build-row")];
return {
visible: visible("#live-build-list"),
rows: rows.length,
scrollContained: list.clientHeight <= Math.ceil(window.innerHeight * 0.42) + 4,
scrollContained: list.clientHeight <= Math.ceil(window.innerHeight * 0.58) + 4,
overlay: popoverStyle.position === "absolute",
text: list.textContent?.replace(/\s+/gu, " ").trim() ?? "",
overflowX: Math.max(0, ...rows.map((row) => row.scrollWidth - row.clientWidth), list.scrollWidth - list.clientWidth)
};
@@ -7830,36 +7840,103 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
const inspectLiveBuildLayout = async () => {
const details = document.querySelector("#live-build-summary");
const label = document.querySelector("#live-build-latest");
const toggle = document.querySelector("#live-build-toggle");
const popover = document.querySelector("#live-build-popover");
const list = document.querySelector("#live-build-list");
if (!details || !label || !list) {
const topbar = document.querySelector(".topbar");
const workspace = document.querySelector('[data-view="workspace"]');
const conversation = document.querySelector(".conversation-column");
const boxSnapshot = () => ({
topbar: boxForElement(topbar),
workspace: boxForElement(workspace),
conversation: boxForElement(conversation)
});
if (!details || !label || !toggle || !popover || !list || !topbar || !workspace || !conversation) {
return { ok: false, exists: false, summaryOverflowX: null, listOverflowX: null, maxRowOverflowX: null, rows: 0 };
}
const wasOpen = details.open;
const labelBox = boxForElement(label);
const detailsBox = boxForElement(details);
const labelStyle = getComputedStyle(label);
const summaryOverflowX = Math.max(0, label.scrollWidth - label.clientWidth);
const summaryContained =
Boolean(labelBox && detailsBox) &&
labelBox.left >= detailsBox.left - 1 &&
labelBox.right <= detailsBox.right + 1 &&
labelStyle.overflow === "hidden" &&
labelStyle.textOverflow === "ellipsis" &&
labelStyle.whiteSpace === "nowrap";
details.open = false;
await new Promise((resolve) => requestAnimationFrame(resolve));
const before = boxSnapshot();
details.open = true;
await new Promise((resolve) => requestAnimationFrame(resolve));
const rows = [...list.querySelectorAll(".live-build-row")];
const rowOverflows = rows.map((row) => Math.max(0, row.scrollWidth - row.clientWidth));
const maxRowOverflowX = Math.max(0, ...rowOverflows);
const listOverflowX = Math.max(0, list.scrollWidth - list.clientWidth);
const scrollContained = list.clientHeight <= Math.ceil(window.innerHeight * 0.42) + 4;
const popoverBox = boxForElement(popover);
const popoverStyle = getComputedStyle(popover);
const toggleExpanded = toggle.getAttribute("aria-expanded") === "true";
const popoverVisible = popoverBox && popoverBox.width > 0 && popoverBox.height > 0 && popoverStyle.display !== "none";
const popoverViewportContained =
Boolean(popoverBox) &&
popoverBox.left >= -1 &&
popoverBox.right <= window.innerWidth + 1 &&
popoverBox.top >= -1 &&
popoverBox.bottom <= window.innerHeight + 1;
const overlayPositioned = popoverStyle.position === "absolute";
const scrollContained = list.clientHeight <= Math.ceil(window.innerHeight * 0.58) + 4;
const text = list.textContent?.replace(/\s+/gu, " ").trim() ?? "";
const expectedRows = rows.length >= 4;
const expectedText = /构建时间不可用/u.test(text) && /外部镜像或非 HWLAB 构建产物/u.test(text);
const afterOpen = boxSnapshot();
const stableGeometry = ["topbar", "workspace", "conversation"].every((key) => {
const a = before[key];
const b = afterOpen[key];
return a && b && Math.abs(a.top - b.top) <= 1 && Math.abs(a.height - b.height) <= 1;
});
document.querySelector("#live-build-close")?.click();
await new Promise((resolve) => requestAnimationFrame(resolve));
const closedByButton = details.open === false && toggle.getAttribute("aria-expanded") === "false";
details.open = wasOpen;
await new Promise((resolve) => requestAnimationFrame(resolve));
return {
ok: summaryOverflowX <= 1 && listOverflowX <= 1 && maxRowOverflowX <= 1 && scrollContained && expectedRows && expectedText,
ok:
summaryContained &&
listOverflowX <= 1 &&
maxRowOverflowX <= 1 &&
scrollContained &&
expectedRows &&
expectedText &&
overlayPositioned &&
popoverVisible &&
popoverViewportContained &&
toggleExpanded &&
stableGeometry &&
closedByButton,
exists: true,
openStateRestored: details.open === wasOpen,
summaryContained,
summaryOverflowX,
labelBox,
detailsBox,
listOverflowX,
maxRowOverflowX,
rowOverflows,
rows: rows.length,
expectedRows,
expectedText,
overlayPositioned,
popoverVisible,
popoverViewportContained,
toggleExpanded,
scrollContained,
stableGeometry,
closedByButton,
before,
afterOpen,
popoverBox,
textSample: text.slice(0, 240)
};
};
@@ -8231,6 +8308,7 @@ async function inspectLayoutState(page, { mode, viewport, compareTo }) {
rightResizeHandle,
sidePanelScroll,
sidePanelOverflowUsable,
liveBuildOverlayStable: liveBuildLayout.ok,
hardwareTabsCenterHit,
wiring: {
longTableOk: wiringLongTableOk,
@@ -8640,7 +8718,8 @@ function summarizeCoverageGap(viewportResults, issueKey) {
issue288: "issue288SingleTableReady",
issue287: "issue287HardwareTabsReady",
hardwareTabs: "hardwareTabsCenterHit",
wiring: "wiringReadableTwoColumn"
wiring: "wiringReadableTwoColumn",
liveBuildOverlay: "liveBuildOverlayStable"
}[issueKey] ?? "issue287HardwareTabsReady";
return {
status: allViewportCoverage(viewportResults, fieldName) ? "pass" : "skip",
+33
View File
@@ -104,6 +104,9 @@ const el = {
liveStatus: byId("live-status"),
liveDetail: byId("live-detail"),
liveBuildSummary: byId("live-build-summary"),
liveBuildToggle: byId("live-build-toggle"),
liveBuildPopover: byId("live-build-popover"),
liveBuildClose: byId("live-build-close"),
liveBuildLatest: byId("live-build-latest"),
liveBuildList: byId("live-build-list"),
agentChatStatus: byId("agent-chat-status"),
@@ -196,6 +199,7 @@ initSideTabs();
initHardwareTabs();
initQuickActions();
initCommandBar();
initLiveBuildOverlay();
initWorkbenchLogout(el.logoutButton);
initM3Control();
initGateControls();
@@ -228,6 +232,35 @@ function resolveTimeoutMs(name, fallback, { min, max }) {
return Math.min(Math.max(Math.trunc(configured), min), max);
}
function initLiveBuildOverlay() {
syncLiveBuildOverlayState();
el.liveBuildSummary.addEventListener("toggle", syncLiveBuildOverlayState);
el.liveBuildClose.addEventListener("click", () => closeLiveBuildOverlay({ restoreFocus: true }));
document.addEventListener("keydown", (event) => {
if (event.key === "Escape" && el.liveBuildSummary.open) {
event.preventDefault();
closeLiveBuildOverlay({ restoreFocus: true });
}
});
document.addEventListener("click", (event) => {
if (!el.liveBuildSummary.open || el.liveBuildSummary.contains(event.target)) return;
closeLiveBuildOverlay();
});
}
function syncLiveBuildOverlayState() {
const expanded = el.liveBuildSummary.open === true;
el.liveBuildToggle.setAttribute("aria-expanded", expanded ? "true" : "false");
el.liveBuildPopover.setAttribute("aria-hidden", expanded ? "false" : "true");
}
function closeLiveBuildOverlay(options = {}) {
if (!el.liveBuildSummary.open) return;
el.liveBuildSummary.open = false;
syncLiveBuildOverlayState();
if (options.restoreFocus === true) el.liveBuildToggle.focus();
}
function initRoutes() {
window.addEventListener("hashchange", () => showView(routeFromLocation()));
for (const button of document.querySelectorAll("[data-route]")) {
+8 -2
View File
@@ -72,10 +72,16 @@
<strong id="live-status" class="tone-pending">等待验证</strong>
<small id="live-detail">正在验证 hwlab-cloud-api /health/live、/v1、/v1/agent/chat 与 /v1/m3/io;未完成前不标为通过。</small>
<details class="live-build-summary" id="live-build-summary">
<summary>
<summary id="live-build-toggle" aria-controls="live-build-popover" aria-expanded="false">
<span class="live-build-summary-label" id="live-build-latest">最新镜像构建时间:等待实时元数据</span>
</summary>
<div class="live-build-list" id="live-build-list" aria-label="live HWLAB 微服务构建时间明细"></div>
<div class="live-build-popover" id="live-build-popover" role="dialog" aria-modal="false" aria-label="live HWLAB 微服务构建时间明细">
<div class="live-build-popover-head">
<strong>构建版本明细</strong>
<button class="live-build-close" id="live-build-close" type="button" aria-label="关闭构建版本明细">关闭</button>
</div>
<div class="live-build-list" id="live-build-list" tabindex="0" aria-label="live HWLAB 微服务构建时间明细"></div>
</div>
</details>
</div>
<button class="logout-button" id="logout-button" type="button">退出</button>
+17 -2
View File
@@ -278,6 +278,11 @@ assert.equal(
"pass",
"#352 resource explorer removal must remain covered by desktop and 390x844 layout smoke"
);
assert.equal(
layoutSmoke.checks.find((check) => check.id === "layout-feedback-437-live-build-overlay")?.status,
"pass",
"#437 live build details overlay must not move the topbar, workspace, or conversation geometry"
);
assert.equal(
layoutSmoke.checks.find((check) => check.id === "layout-left-sidebar-collapse")?.status,
"pass",
@@ -293,6 +298,7 @@ for (const layoutCheckId of [
"layout-narrow-desktop-default",
"layout-mobile-default",
"layout-feedback-352-resource-explorer-removed",
"layout-feedback-437-live-build-overlay",
"layout-left-sidebar-collapse",
"layout-right-sidebar-resize-desktop",
"layout-right-sidebar-resize-narrow-desktop",
@@ -934,7 +940,13 @@ assert.match(app, /fetchJson\("\/v1\/diagnostics\/gate"/);
assert.match(app, /fetchJson\("\/v1\/live-builds"\)/);
assert.match(html, /id="live-build-latest"/);
assert.match(html, /id="live-build-list"/);
assert.match(html, /id="live-build-toggle"[^>]*aria-controls="live-build-popover"[^>]*aria-expanded="false"/);
assert.match(html, /id="live-build-popover"[^>]*role="dialog"[^>]*aria-modal="false"/);
assert.match(html, /id="live-build-close"/);
assert.match(styles, /\.live-build-summary\s*{/);
assert.match(app, /initLiveBuildOverlay/);
assert.match(app, /closeLiveBuildOverlay/);
assert.match(app, /aria-expanded/);
assert.match(app, /formatBeijingTime/);
assert.match(app, /Asia\/Shanghai/);
assert.match(app, /最新镜像构建时间/);
@@ -946,9 +958,12 @@ assert.match(app, /liveBuildDesiredStateText/);
assert.match(app, /外部镜像或非 HWLAB 构建产物/);
assert.doesNotMatch(app, /artifact report|artifact-report/);
assert.match(app, /live health/);
assert.match(styles, /\.live-build-summary-label\s*{[^}]*min-width:\s*0;[^}]*max-width:\s*100%;[^}]*overflow-wrap:\s*anywhere;/s);
assert.match(styles, /\.live-build-list\s*{[^}]*max-height:\s*min\(280px,\s*42dvh\);[^}]*min-width:\s*0;[^}]*overflow:\s*auto;/s);
assert.match(styles, /\.live-build-summary-label\s*{[^}]*min-width:\s*0;[^}]*max-width:\s*100%;[^}]*overflow:\s*hidden;[^}]*text-overflow:\s*ellipsis;[^}]*white-space:\s*nowrap;/s);
assert.match(styles, /\.live-build-popover\s*{[^}]*position:\s*absolute;[^}]*max-height:\s*min\(420px,\s*58dvh\);[^}]*overflow:\s*hidden;/s);
assert.match(styles, /\.live-build-summary\[open\]\s+\.live-build-popover\s*{[^}]*display:\s*grid;/s);
assert.match(styles, /\.live-build-list\s*{[^}]*min-width:\s*0;[^}]*max-height:\s*min\(352px,\s*calc\(58dvh - 54px\)\);[^}]*overflow:\s*auto;/s);
assert.match(styles, /\.live-build-row-head\s*{[^}]*grid-template-columns:\s*minmax\(0,\s*0\.8fr\)\s*minmax\(0,\s*1\.2fr\);/s);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.live-build-popover\s*{[\s\S]*?width:\s*100%;/);
assert.match(styles, /@media \(max-width: 860px\)[\s\S]*?\.live-build-row-head\s*{[\s\S]*?grid-template-columns:\s*1fr;/);
assert.match(styles, /\.live-build-meta span\s*{[^}]*overflow-wrap:\s*anywhere;/s);
assert.match(app, /fetchJson\("\/health\/live"\)/);
+87 -4
View File
@@ -504,6 +504,7 @@ h3 {
}
.probe-card {
position: relative;
min-width: 0;
display: grid;
align-content: center;
@@ -526,6 +527,7 @@ h3 {
}
.live-build-summary {
position: relative;
min-width: 0;
max-width: 100%;
margin-top: 4px;
@@ -543,19 +545,88 @@ h3 {
font-size: 10px;
line-height: 1.35;
cursor: pointer;
overflow-wrap: anywhere;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
list-style-position: inside;
}
.live-build-summary summary::marker {
color: var(--accent-2);
}
.live-build-summary-label {
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
overflow-wrap: anywhere;
}
.live-build-list {
max-height: min(280px, 42dvh);
.live-build-popover {
position: absolute;
z-index: 30;
top: calc(100% + 8px);
right: 0;
width: min(100%, 560px);
max-width: 100%;
max-height: min(420px, 58dvh);
display: none;
grid-template-rows: auto minmax(0, 1fr);
gap: 8px;
padding: 10px;
background: rgba(20, 22, 20, 0.99);
border: 1px solid var(--line-strong);
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.42);
overflow: hidden;
}
.live-build-summary[open] .live-build-popover {
display: grid;
}
.live-build-popover-head {
min-width: 0;
margin-top: 7px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.live-build-popover-head strong {
min-width: 0;
color: var(--text);
font-family: var(--mono);
font-size: 11px;
line-height: 1.2;
text-transform: none;
overflow-wrap: anywhere;
}
.live-build-close {
flex: 0 0 auto;
min-height: 26px;
padding: 3px 8px;
border: 1px solid var(--line);
background: var(--surface-2);
color: var(--muted);
font-family: var(--mono);
font-size: 10px;
font-weight: 760;
cursor: pointer;
}
.live-build-close:hover,
.live-build-close:focus-visible {
border-color: var(--line-strong);
color: var(--text);
outline: 0;
}
.live-build-list {
min-width: 0;
max-height: min(352px, calc(58dvh - 54px));
display: grid;
gap: 6px;
overflow: auto;
@@ -2261,6 +2332,18 @@ tbody tr:last-child td {
gap: 2px;
}
.live-build-popover {
right: 0;
left: auto;
width: 100%;
max-width: 100%;
max-height: min(420px, 56dvh);
}
.live-build-list {
max-height: min(352px, calc(56dvh - 54px));
}
.live-build-time {
text-align: left;
}