fix: 修正 HWPOD 控制台状态与有界布局

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
root
2026-07-21 13:23:55 +02:00
parent d1020aaab8
commit ccf10af5bd
9 changed files with 97 additions and 62 deletions
@@ -351,6 +351,24 @@ test("R2 visual constraints keep industrial controls bounded and component-owned
assert.match(read(relativePath), /ViewModeSwitch/u, `${relativePath} should use the shared view switch`);
});
test("HWPOD topology keeps connection state separate and owns bounded scrolling", () => {
const routes = read("src/router/index.ts");
for (const path of ["/hwpods/devices/:hwpodId?", "/hwpods/nodes/:nodeId?", "/hwpods/onboarding/:nodeId?"]) {
const route = routes.split(`{ path: "${path}"`)[1]?.split("},")[0] ?? "";
assert.match(route, /boundedLayout: true/u, `${path} should use bounded AppShell layout`);
}
const view = read("src/views/admin/HwpodGroupsView.vue");
assert.match(view, /\.hwpod-console \{[^}]*height: 100%;[^}]*overflow: hidden;/u);
assert.match(view, /\.hwpod-workspace \{[^}]*flex: 1 1 auto;/u);
assert.doesNotMatch(view, /\.hwpod-workspace \{[^}]*height: clamp\(/u);
const deviceWorkspace = read("src/components/hwpod/HwpodDevicesWorkspace.vue");
assert.match(deviceWorkspace, /capabilityCoverage\.matched/u);
assert.match(deviceWorkspace, /capabilityCoverage\.declared/u);
assert.doesNotMatch(deviceWorkspace, /actualCapabilities\.length\s*\}\}\/\{\{\s*row\.declaredCapabilities\.length/u);
});
test("R6 Sub2API source comparison is recorded in parent/child issue scope", () => {
const issueBody = readIssueBodyFixture();
assert.match(issueBody, /Wei-Shaw\/sub2api@v0\.1\.136=a2f76e4/u);
@@ -14,12 +14,8 @@ const props = withDefaults(
const emit = defineEmits<{ select: [id: string] }>();
const statusLabel = computed(() => ({
available: "可用",
busy: "执行中",
online: "在线",
offline: "节点离线",
"capability-mismatch": "能力不匹配",
"workspace-blocked": "工作区阻塞",
unprobed: "待探测"
}[props.device.status] ?? props.device.status));
function linkState(kind: "target" | "workspace" | "debug" | "io"): string {
@@ -80,7 +76,7 @@ function linkState(kind: "target" | "workspace" | "debug" | "io"): string {
<p v-if="device.blockers[0]" class="pcb-blocker"><strong>{{ device.blockers[0].code }}</strong>{{ device.blockers[0].summary }}</p>
<footer>
<span>{{ device.declaredCapabilities.length }} 项声明能力</span>
<span>能力覆盖 {{ device.capabilityCoverage.matched }}/{{ device.capabilityCoverage.declared }} · 上报 {{ device.capabilityCoverage.actual }}</span>
<span v-if="device.operation">{{ device.operation.status }} · {{ device.operation.opCount }} ops</span>
<button
type="button"
@@ -95,17 +91,13 @@ function linkState(kind: "target" | "workspace" | "debug" | "io"): string {
.pcb-card { position: relative; min-width: 0; border: 1px solid #cbd2cf; border-top: 3px solid #2e6b67; border-radius: 8px; background: #fbfaf6; box-shadow: 0 12px 28px rgb(20 34 32 / 8%); overflow: hidden; transition: border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease; }
.pcb-card[data-selected="true"] { border-color: #1f7469; border-top-color: #145f57; box-shadow: 0 0 0 3px rgb(31 116 105 / 18%), 0 16px 34px rgb(20 55 48 / 14%); transform: translateY(-1px); }
.pcb-card[data-selected="true"] footer button { border-color: #145f57; background: #dff2ed; color: #145f57; }
.pcb-card[data-status="busy"] { border-top-color: #b86d1d; }
.pcb-card[data-status="offline"], .pcb-card[data-status="workspace-blocked"] { border-top-color: #8d453b; }
.pcb-card[data-status="capability-mismatch"] { border-top-color: #8a6b16; }
.pcb-card[data-status="offline"] { border-top-color: #8d453b; }
.pcb-card__header { display: flex; justify-content: space-between; gap: 16px; align-items: flex-start; padding: 18px 20px 12px; }
.pcb-card__header h2 { margin: 3px 0 0; color: #152421; font-size: 1.08rem; letter-spacing: 0; overflow-wrap: anywhere; }
.pcb-card__eyebrow { color: #687572; font: 600 .72rem/1.2 ui-monospace, SFMono-Regular, Consolas, monospace; text-transform: uppercase; }
.pcb-status { display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto; padding: 5px 9px; border: 1px solid #b7c6c1; border-radius: 999px; color: #244943; background: #eef7f2; font-size: .76rem; font-weight: 700; }
.pcb-status i { width: 7px; height: 7px; border-radius: 50%; background: currentcolor; }
.pcb-status[data-status="busy"] { color: #8b4c0a; background: #fff4df; border-color: #e6bd80; }
.pcb-status[data-status="offline"], .pcb-status[data-status="workspace-blocked"] { color: #7b332c; background: #fff0ed; border-color: #ddb2ac; }
.pcb-status[data-status="capability-mismatch"] { color: #6f560b; background: #fff8d8; border-color: #d8c376; }
.pcb-status[data-status="offline"] { color: #7b332c; background: #fff0ed; border-color: #ddb2ac; }
.pcb-map { display: block; width: calc(100% - 28px); max-height: 220px; margin: 0 14px; }
.pcb-board { fill: #142c28; stroke: #31514a; stroke-width: 2; }
.pcb-grid-line { fill: none; stroke: rgb(137 184 168 / 9%); stroke-width: 1; }
@@ -11,7 +11,7 @@ import HwpodDeviceCard from "./HwpodDeviceCard.vue";
defineProps<{ devices: HwpodDeviceTopologyItem[]; selectedId: string | null; view: "cards" | "list"; density: "compact" | "comfortable"; sort: string }>();
const emit = defineEmits<{ select: [device: HwpodDeviceTopologyItem]; sort: [key: string] }>();
const columns: DataGridColumn[] = [{ key: "name", label: "HWPOD", sortable: true, width: "190px" }, { key: "nodeName", label: "Node", width: "170px" }, { key: "status", label: "状态", sortable: true, width: "100px" }, { key: "elements", label: "目标 / 工作区", width: "230px" }, { key: "debugIo", label: "调试 / IO", width: "210px" }, { key: "capabilities", label: "能力", width: "140px" }, { key: "blocker", label: "主要 blocker", width: "220px" }, { key: "operation", label: "用户操作", width: "160px" }];
const statusLabel = (status: string): string => ({ online: "在线", offline: "离线", busy: "忙碌", available: "可用", error: "异常", mismatch: "能力不匹配", "capability-mismatch": "能力不匹配" }[status] || status || "unknown");
const statusLabel = (status: string): string => ({ online: "在线", offline: "离线" }[status] || status || "unknown");
</script>
<template>
@@ -49,7 +49,7 @@ const statusLabel = (status: string): string => ({ online: "在线", offline: "
<template #cell-status="{ row }"><span class="status-chip" :data-status="row.status">{{ statusLabel(row.status) }}</span></template>
<template #cell-elements="{ row }"><span class="grid-stack"><strong>{{ row.elements.target.label }}</strong><small>{{ row.elements.workspace.label }}</small></span></template>
<template #cell-debugIo="{ row }"><span class="grid-stack"><strong>{{ row.elements.debugProbe.label }}</strong><small>{{ row.elements.ioProbe.label }}</small></span></template>
<template #cell-capabilities="{ row }"><span class="grid-stack"><strong>{{ row.actualCapabilities.length }}/{{ row.declaredCapabilities.length }}</strong><small v-if="row.missingCapabilities.length"> {{ row.missingCapabilities.join(', ') }}</small></span></template>
<template #cell-capabilities="{ row }"><span class="grid-stack"><strong>覆盖 {{ row.capabilityCoverage.matched }}/{{ row.capabilityCoverage.declared }}</strong><small>上报 {{ row.capabilityCoverage.actual }} · {{ row.capabilityCoverage.missing }} · 额外 {{ row.capabilityCoverage.extra }}</small></span></template>
<template #cell-blocker="{ row }"><span class="grid-stack"><strong>{{ row.blockers[0]?.code || '无' }}</strong><small>{{ row.blockers[0]?.summary || '当前可操作' }}</small></span></template>
<template #cell-operation="{ row }"><span class="grid-stack"><strong>{{ row.operation?.status || '无' }}</strong><small>{{ row.operation ? `${row.operation.opCount} ops` : '尚无用户操作' }}</small></span></template>
</DataGrid>
@@ -11,7 +11,7 @@ import HwpodReadinessRail from "./HwpodReadinessRail.vue";
defineProps<{ nodes: HwpodNodeTopologyItem[]; selectedId: string | null; view: "cards" | "list"; density: "compact" | "comfortable"; sort: string }>();
const emit = defineEmits<{ select: [node: HwpodNodeTopologyItem]; sort: [key: string] }>();
const columns: DataGridColumn[] = [{ key: "name", label: "Node", sortable: true, width: "210px" }, { key: "status", label: "连接", sortable: true, width: "100px" }, { key: "deviceCount", label: "HWPOD", align: "right", width: "80px" }, { key: "inFlight", label: "In-flight", width: "100px" }, { key: "capabilities", label: "能力", width: "130px" }, { key: "mounted", label: "挂载 HWPOD", width: "220px" }, { key: "readiness", label: "接入阶段", width: "180px" }, { key: "lastSeen", label: "最后心跳", width: "150px" }, { key: "blocker", label: "Blocker", width: "220px" }];
const statusLabel = (status: string): string => ({ online: "在线", offline: "离线", busy: "忙碌", available: "可用", error: "异常", mismatch: "能力不匹配", "capability-mismatch": "能力不匹配" }[status] || status || "unknown");
const statusLabel = (status: string): string => ({ online: "在线", offline: "离线" }[status] || status || "unknown");
function formatTime(value: string | null): string { if (!value) return "-"; const date = new Date(value); return Number.isNaN(date.getTime()) ? value : date.toLocaleString(); }
</script>
@@ -46,7 +46,7 @@ function formatTime(value: string | null): string { if (!value) return "-"; cons
<section class="node-gauges">
<div><small>HWPOD</small><strong>{{ node.deviceCount }}</strong></div>
<div><small>in-flight</small><strong>{{ node.inFlight.count }}/{{ node.inFlight.max }}</strong></div>
<div><small>capabilities</small><strong>{{ node.actualCapabilities.length }}/{{ node.declaredCapabilities.length }}</strong></div>
<div><small>capability coverage</small><strong>{{ node.capabilityCoverage.matched }}/{{ node.capabilityCoverage.declared }}</strong></div>
</section>
<section class="mounted-hwpods" aria-label="挂载 HWPOD">
<header><strong>挂载 HWPOD</strong><span>{{ node.hwpods.length }}</span></header>
@@ -85,7 +85,7 @@ function formatTime(value: string | null): string { if (!value) return "-"; cons
<template #cell-status="{ row }"><span class="status-chip" :data-status="row.status">{{ statusLabel(row.status) }}</span></template>
<template #cell-deviceCount="{ row }"><strong>{{ row.deviceCount }}</strong></template>
<template #cell-inFlight="{ row }"><strong>{{ row.inFlight.count }}/{{ row.inFlight.max }}</strong></template>
<template #cell-capabilities="{ row }"><span class="grid-stack"><strong>{{ row.actualCapabilities.length }}/{{ row.declaredCapabilities.length }}</strong><small v-if="row.missingCapabilities.length"> {{ row.missingCapabilities.join(', ') }}</small></span></template>
<template #cell-capabilities="{ row }"><span class="grid-stack"><strong>覆盖 {{ row.capabilityCoverage.matched }}/{{ row.capabilityCoverage.declared }}</strong><small>上报 {{ row.capabilityCoverage.actual }} · {{ row.capabilityCoverage.missing }} · 额外 {{ row.capabilityCoverage.extra }}</small></span></template>
<template #cell-mounted="{ row }"><span class="grid-stack"><strong>{{ row.hwpods.map((item) => item.name).join(', ') || '无' }}</strong><small>{{ row.hwpods.map((item) => item.hwpodId).join(', ') || '未绑定 spec' }}</small></span></template>
<template #cell-readiness="{ row }"><span class="grid-stack"><strong>{{ row.readiness.filter((stage: HwpodNodeReadinessStage) => stage.status === 'ready').length }}/{{ row.readiness.length }} ready</strong><small>{{ row.readiness.find((stage: HwpodNodeReadinessStage) => stage.status !== 'ready')?.label || '全部阶段 ready' }}</small></span></template>
<template #cell-lastSeen="{ row }">{{ formatTime(row.lastSeenAt) }}</template>
+3 -3
View File
@@ -49,9 +49,9 @@ const routes: RouteRecordRaw[] = [
{ path: "/admin/billing", name: "AdminBilling", component: () => import("@/views/admin/BillingView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.billing", title: "兑换与订阅", section: "admin" } },
{ path: "/admin/hwpod-groups", redirect: "/hwpods/devices" },
{ path: "/hwpods", redirect: "/hwpods/devices" },
{ path: "/hwpods/devices/:hwpodId?", name: "HwpodDevices", component: () => import("@/views/admin/HwpodGroupsView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.hwpodGroups", navParent: "HwpodGroups", title: "HWPOD 设备资源", section: "admin" } },
{ path: "/hwpods/nodes/:nodeId?", name: "HwpodNodes", component: () => import("@/views/admin/HwpodGroupsView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.hwpodGroups", navParent: "HwpodGroups", title: "HWPOD Node", section: "admin" } },
{ path: "/hwpods/onboarding/:nodeId?", name: "HwpodOnboarding", component: () => import("@/views/admin/HwpodGroupsView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.hwpodGroups", navParent: "HwpodGroups", title: "HWPOD 节点接入", section: "admin" } },
{ path: "/hwpods/devices/:hwpodId?", name: "HwpodDevices", component: () => import("@/views/admin/HwpodGroupsView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.hwpodGroups", navParent: "HwpodGroups", title: "HWPOD 设备资源", section: "admin", boundedLayout: true } },
{ path: "/hwpods/nodes/:nodeId?", name: "HwpodNodes", component: () => import("@/views/admin/HwpodGroupsView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.hwpodGroups", navParent: "HwpodGroups", title: "HWPOD Node", section: "admin", boundedLayout: true } },
{ path: "/hwpods/onboarding/:nodeId?", name: "HwpodOnboarding", component: () => import("@/views/admin/HwpodGroupsView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.hwpodGroups", navParent: "HwpodGroups", title: "HWPOD 节点接入", section: "admin", boundedLayout: true } },
{ path: "/admin/provider-profiles", name: "ProviderProfiles", component: () => import("@/views/admin/ProviderProfilesView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.providerProfiles", title: "Provider Profiles", section: "admin" } },
{ path: "/admin/provider-profiles/:profile", name: "ProviderProfileDetail", component: () => import("@/views/admin/ProviderProfilesView.vue"), meta: { requiresAuth: true, requiresAdmin: true, navId: "admin.providerProfiles", navParent: "ProviderProfiles", title: "Provider Profile 详情", section: "admin" } },
{ path: "/skills", name: "Skills", component: () => import("@/views/SkillsView.vue"), meta: { requiresAuth: true, navId: "system.skills", title: "技能包", section: "system" } },
+14 -4
View File
@@ -389,9 +389,17 @@ export type AgentChatResultResponse = AgentChatResponse & {
export interface HwpodSpecsResponse { specs?: Array<Record<string, unknown>>; [key: string]: unknown }
export interface HwpodNodeOpsResponse { ok?: boolean; status?: string; events?: TraceEvent[]; groups?: Array<Record<string, unknown>>; [key: string]: unknown }
export type HwpodResourceStatus = "available" | "busy" | "offline" | "workspace-blocked" | "capability-mismatch" | "unprobed" | "online" | "unconfigured" | string;
export type HwpodConnectionStatus = "online" | "offline";
export type HwpodReadinessStatus = "ready" | "blocked" | "unknown" | string;
export interface HwpodCapabilityCoverage {
matched: number;
declared: number;
actual: number;
missing: number;
extra: number;
}
export interface HwpodBlocker {
code: string;
summary: string;
@@ -431,7 +439,7 @@ export interface HwpodDeviceTopologyItem {
name: string;
nodeId: string;
nodeName: string;
status: HwpodResourceStatus;
status: HwpodConnectionStatus;
online: boolean;
available: boolean;
busy: boolean;
@@ -439,6 +447,7 @@ export interface HwpodDeviceTopologyItem {
declaredCapabilities: string[];
actualCapabilities: string[];
missingCapabilities: string[];
capabilityCoverage: HwpodCapabilityCoverage;
elements: HwpodElements;
availability: { ok: boolean | null; status: string; checkedAt: string | null; blocker: HwpodBlocker | null };
operation: HwpodOperationSummary | null;
@@ -461,7 +470,7 @@ export interface HwpodNodeTopologyItem {
resourceType: "hwpod-node";
nodeId: string;
name: string;
status: HwpodResourceStatus;
status: HwpodConnectionStatus;
online: boolean;
platform: string;
version: string | null;
@@ -470,10 +479,11 @@ export interface HwpodNodeTopologyItem {
lastSeenAt: string | null;
ageMs: number | null;
deviceCount: number;
hwpods: Array<{ hwpodId: string; name: string; status: HwpodResourceStatus; busy: boolean; capabilityMismatch: boolean }>;
hwpods: Array<{ hwpodId: string; name: string; status: HwpodConnectionStatus; busy: boolean; capabilityMismatch: boolean }>;
declaredCapabilities: string[];
actualCapabilities: string[];
missingCapabilities: string[];
capabilityCoverage: HwpodCapabilityCoverage;
capabilityMismatch: boolean;
inFlight: { count: number; max: number; busy: boolean };
operation: HwpodOperationSummary | null;
@@ -70,18 +70,11 @@ const statusItems = computed(() => [
const statusFilterOptions = computed(() => tab.value === "nodes"
? [
{ value: "online", label: "在线" },
{ value: "busy", label: "执行中" },
{ value: "offline", label: "离线" },
{ value: "capability-mismatch", label: "能力不匹配" },
{ value: "unconfigured", label: "无 Spec" }
{ value: "offline", label: "离线" }
]
: [
{ value: "available", label: "可用" },
{ value: "busy", label: "执行中" },
{ value: "offline", label: "离线" },
{ value: "capability-mismatch", label: "能力不匹配" },
{ value: "workspace-blocked", label: "工作区阻塞" },
{ value: "unprobed", label: "待探测" }
{ value: "online", label: "在线" },
{ value: "offline", label: "离线" }
]);
const collectionPage = computed(() => tab.value === "devices" ? hwpod.deviceTopology?.page : tab.value === "nodes" ? hwpod.nodeTopology?.page : null);
@@ -195,7 +188,7 @@ async function copySha(): Promise<void> {
}
function statusLabel(status: string): string {
return ({ available: "可用", busy: "执行中", offline: "离线", "capability-mismatch": "能力不匹配", "workspace-blocked": "工作区阻塞", unprobed: "待探测", online: "在线", unconfigured: "无 Spec" }[status] ?? status);
return ({ online: "在线", offline: "离线" }[status] ?? status);
}
function formatTime(value: string | null): string {
@@ -310,20 +303,24 @@ function text(value: unknown): string {
</template>
<style scoped>
.hwpod-console { display: grid; min-width: 0; min-height: 100%; align-content: start; gap: 12px; padding: 8px; color: var(--console-graphite-900); }
.hwpod-console { display: grid; height: 100%; min-width: 0; min-height: 0; grid-template-rows: auto auto minmax(0, 1fr); align-content: stretch; gap: 12px; padding: 8px; color: var(--console-graphite-900); overflow: hidden; }
.hwpod-console :deep(.console-async-boundary) { display: flex; min-height: 0; flex-direction: column; overflow: hidden; }
.hwpod-console :deep(.page-command-filters) { flex-wrap: nowrap; align-items: center; gap: 6px; padding-bottom: 1px; overflow-x: auto; overscroll-behavior-inline: contain; }
.console-tabs { display: flex; gap: 2px; width: fit-content; max-width: 100%; padding: 3px; border: 1px solid var(--console-border-strong); border-radius: var(--console-radius-sm); background: var(--console-border); overflow-x: auto; }
.console-tabs button { min-height: 32px; border: 0; border-radius: 5px; background: var(--console-surface-raised); color: var(--console-graphite-600); padding: 0 12px; font-size: 12px; font-weight: 750; white-space: nowrap; cursor: pointer; }
.console-tabs button[data-active="true"] { color: #fff; background: var(--console-graphite-900); }
.console-search { display: flex; min-width: min(420px, 100%); align-items: center; gap: 7px; }
.console-search { display: flex; width: min(340px, 100%); min-width: min(340px, 100%); align-items: center; gap: 6px; }
.console-search label, .status-filter { color: var(--console-graphite-600); font-family: var(--console-font-mono); font-size: 10px; font-weight: 760; letter-spacing: 0; text-transform: uppercase; }
.console-search input { min-width: 0; flex: 1; border: 1px solid var(--console-border-strong); border-radius: var(--console-radius-sm); background: var(--console-surface-raised); padding: 8px 10px; }
.console-search button { border: 1px solid var(--console-cyan-700); border-radius: var(--console-radius-sm); background: var(--console-cyan-100); color: var(--console-cyan-700); padding: 7px 10px; font-weight: 750; cursor: pointer; }
.console-search input { height: 32px; min-width: 0; flex: 1; border: 1px solid var(--console-border-strong); border-radius: var(--console-radius-sm); background: var(--console-surface-raised); padding: 0 9px; font-size: 11px; }
.console-search button { height: 32px; border: 1px solid var(--console-cyan-700); border-radius: var(--console-radius-sm); background: var(--console-cyan-100); color: var(--console-cyan-700); padding: 0 9px; font-size: 11px; font-weight: 750; cursor: pointer; }
.status-filter { display: flex; align-items: center; gap: 7px; }
.status-filter select { border: 1px solid var(--console-border-strong); border-radius: var(--console-radius-sm); background: var(--console-surface-raised); padding: 7px 9px; color: var(--console-graphite-800); }
.probe-button { border: 1px solid var(--console-amber-500); border-radius: var(--console-radius-sm); background: var(--console-amber-100); color: var(--console-amber-700); padding: 7px 10px; font-size: 11px; font-weight: 750; cursor: pointer; }
.status-filter select { height: 32px; border: 1px solid var(--console-border-strong); border-radius: var(--console-radius-sm); background: var(--console-surface-raised); padding: 0 8px; color: var(--console-graphite-800); font-size: 11px; }
.probe-button { height: 32px; border: 1px solid var(--console-amber-500); border-radius: var(--console-radius-sm); background: var(--console-amber-100); color: var(--console-amber-700); padding: 0 9px; font-size: 11px; font-weight: 750; cursor: pointer; white-space: nowrap; }
.hwpod-console :deep(.view-mode-switch) { height: 32px; flex: 0 0 auto; }
.hwpod-console :deep(.view-mode-switch button) { height: 28px; min-height: 28px; padding-block: 0; font-size: 11px; }
.spec-warning { display: flex; gap: 8px 20px; align-items: center; margin-bottom: 10px; padding: 10px 13px; border: 1px solid #dfc47d; border-left: 4px solid #aa7a18; border-radius: 7px; background: #fff8df; color: #6b5317; font-size: .78rem; }
.stale-warning { display: flex; gap: 8px 20px; align-items: center; margin-bottom: 10px; padding: 10px 13px; border: 1px solid #e4a8a2; border-left: 4px solid var(--console-red-700); border-radius: var(--console-radius-sm); background: var(--console-red-100); color: var(--console-red-700); font-size: .78rem; }
.hwpod-workspace { height: clamp(420px, calc(100dvh - 300px), 760px); }
.hwpod-workspace { min-height: 0; flex: 1 1 auto; }
.hwpod-workspace :deep(.bounded-workspace-main) { padding: 12px; }
.hwpod-workspace :deep(.resource-stage) { display: grid; height: 100%; min-width: 0; min-height: 0; grid-template-rows: minmax(0, 1fr); }
.hwpod-workspace :deep(.grid-stack) { display: grid; min-width: 0; gap: 3px; }
@@ -346,6 +343,6 @@ dd { margin: 3px 0 0; overflow-wrap: anywhere; }
.collection-pagination div { display: flex; gap: 6px; }
.collection-pagination button { border: 1px solid var(--console-border-strong); border-radius: var(--console-radius-sm); background: var(--console-surface-raised); color: var(--console-graphite-800); padding: 6px 10px; font-weight: 700; cursor: pointer; }
.collection-pagination button:disabled { opacity: .45; cursor: not-allowed; }
@media (max-width: 680px) { .hwpod-console { padding: 2px; } .console-tabs { width: 100%; } .console-tabs button { flex: 1 0 auto; } .console-search { min-width: 100%; } .status-filter { width: 100%; justify-content: space-between; } .spec-warning, .stale-warning { align-items: flex-start; flex-direction: column; } .hwpod-workspace { height: clamp(520px, calc(100dvh - 260px), 680px); } .hwpod-workspace :deep(.bounded-workspace-main) { padding: 8px; } }
@media (max-width: 680px) { .hwpod-console { padding: 2px; } .console-tabs { width: 100%; } .console-tabs button { flex: 1 0 auto; } .console-search { width: 100%; min-width: 100%; } .status-filter { flex: 0 0 auto; } .spec-warning, .stale-warning { align-items: flex-start; flex-direction: column; } .hwpod-workspace :deep(.bounded-workspace-main) { padding: 8px; } }
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; transition: none !important; animation: none !important; } }
</style>