diff --git a/CHANGELOG.md b/CHANGELOG.md index 728757c..5f1c68f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ > 开发中的用户文案可先写入 `## Unreleased`;该区不会被前端解析,正式发布时再替换为数字版本和日期。 > 工程口径的完整记录见 `PROGRESS.md` / git log。 +## Unreleased + +- 管理后台总览按“当前运行”和“运营资源”重新组织,执行容量改为精简摘要并可从右侧详情面板查看队列、容器、用户占用与宿主资源,异常状态和近期指标也更容易识别。 + ## 0.70.0 — 2026-09-02 - 容器长任务会在整机繁忙时安全排队,后台任务可查看或取消排队状态;管理员可在“容器状态”查看实时容量,并通过“容器依赖”分析用户临时安装的 Python 依赖及占用。 diff --git a/PROGRESS.md b/PROGRESS.md index deee5e9..55d9684 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -20,6 +20,8 @@ --- ## 已完成关键能力 +- **09-02 / Unreleased / 管理后台运行总览分层**:Admin 八张摘要卡按当前运行与运营资源重排,主指标统一为近期或实时口径,累计与低频明细降级;执行容量新增可自动刷新的右侧详情抽屉,集中展示前后台队列、容器生命周期、单用户占用与宿主资源,并移除物理占用与逻辑配额口径不一致的存储进度条。无 API、schema、migration 或运行方式变化。 + - **09-02 / 0.70.0 / 管理与更新日志界面收敛**:Admin 顶部“容器状态”直接承载前后台执行、排队、回收、宿主资源与单用户占用,移除重复的独立容量区块,并统一采用“容器依赖”“专业软件节点”用户文案;用户版更新日志只保留可感知变化,公开接口新增向后兼容的 offset 分页元数据,前端每页加载 5 个版本并按需继续加载。 - **09-01 / 0.70.0 / Sandbox 执行容量与临时依赖观测**:默认单容器资源提升到 4 GiB/2 CPU、pids 1024、shm 512 MiB、tmp 1 GiB,普通容器无活动 10 分钟回收;新增跨蓝绿/多 Web 实例共享的整机 6、后台 4、单用户 2 重型槽位,前台可取消排队且排队不计 timeout,活动计数防长命令被 reaper 误删,内存压力只暂停新放行。Docker 后台 proc 无槽时持久化 queued、约 30 秒恢复调度,运行中仍由 Docker 托管,终态目录继续保留 7 天。镜像构建基础 Python 包清单,容器删除前可信扫描 `/tmp/.local` 的 `.dist-info` 并以单会话表幂等记录非空差异;Admin 顶部“容器状态”集中展示实时容量,“容器依赖”提供 7/30 天/全部依赖会话统计。新增 0038 migration,未连接或迁移生产数据库、未启动真实专业软件。 diff --git a/tests/test_static_vendor.py b/tests/test_static_vendor.py index 13bf1e4..3ab1148 100644 --- a/tests/test_static_vendor.py +++ b/tests/test_static_vendor.py @@ -103,14 +103,14 @@ class StaticVendorTests(unittest.TestCase): self.assertIn('id="ops-summary" class="ops-summary"', admin_js) self.assertIn(".ops-summary {", html) labels = ( - "容器状态", - "任务状态", + "任务运行", + "执行容量", + "专业软件节点", + "工具健康", "活跃用户", "Token 与缓存", - "专业软件节点", - "存储", "成本", - "工具健康", + "存储", ) positions = [] for label in labels: @@ -122,8 +122,8 @@ class StaticVendorTests(unittest.TestCase): self.assertNotIn("function renderUsersAndUsage", admin_js) self.assertIn('["s-overview", "总览"]', admin_js) self.assertIn('["s-usage", "用量趋势"]', admin_js) - self.assertIn('unit: "活动任务 / 在线节点"', admin_js) - self.assertIn("tokenSummaryHTML(tokens7d, totalTokens)", admin_js) + self.assertIn('unit: softwareNodesLoaded ? "在线 / 总数"', admin_js) + self.assertIn('value: fmtTokens(tokens7d), unit: "近 7 天"', admin_js) self.assertIn("storageTotals.physical_bytes", admin_js) self.assertIn("repeat(auto-fit, minmax(190px, 1fr))", html) self.assertIn('apiGet("/v1/admin/storage/users?page=0&page_size=1")', admin_js) @@ -133,7 +133,12 @@ class StaticVendorTests(unittest.TestCase): self.assertNotIn("function renderSandboxCapacity", admin_js) self.assertIn('["s-sandbox-packages", "容器依赖"]', admin_js) self.assertIn('sandboxCapacityData = await apiGet("/v1/admin/sandbox/capacity")', admin_js) - self.assertIn("containerStatusHTML(capacity)", admin_js) + self.assertNotIn("containerStatusHTML(capacity)", admin_js) + self.assertIn('data-summary-action="${escapeHtml(action)}"', admin_js) + self.assertIn("function renderCapacityDrawer()", admin_js) + self.assertIn("function openCapacityDrawer()", admin_js) + self.assertIn('id="capacity-drawer" class="capacity-drawer"', html) + self.assertIn(".capacity-drawer-panel {", html) def test_changelog_loads_incremental_pages(self) -> None: source = (JS_DIR / "changelog.js").read_text(encoding="utf-8") diff --git a/web/static/admin.html b/web/static/admin.html index 14d837b..b3807c6 100644 --- a/web/static/admin.html +++ b/web/static/admin.html @@ -94,14 +94,25 @@ /* 首屏运营摘要:第一排业务运行,第二排基础设施与质量。 */ .ops-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 10px; } .summary-card { - min-width: 0; min-height: 156px; display: flex; flex-direction: column; padding: 13px 14px 11px; + min-width: 0; min-height: 142px; display: flex; flex-direction: column; padding: 13px 14px 11px; color: inherit; text-decoration: none; background: var(--panel); border: 1px solid var(--border); - border-radius: var(--r-lg); box-shadow: var(--shadow-soft); transition: .15s ease; + border-radius: var(--r-lg); box-shadow: var(--shadow-soft); transition: .15s ease; text-align: left; } - .summary-card[href]:hover { border-color: #c9ced2; box-shadow: 0 3px 10px rgba(18,24,30,.06); transform: translateY(-1px); } + button.summary-card { width: 100%; appearance: none; font: inherit; cursor: pointer; } + .summary-card[href]:hover, button.summary-card:hover { border-color: #c9ced2; box-shadow: 0 3px 10px rgba(18,24,30,.06); transform: translateY(-1px); } + .summary-card[href]:focus-visible, button.summary-card:focus-visible { outline: 2px solid var(--accent-ring); outline-offset: 2px; border-color: var(--accent); } .summary-card .label-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; } .summary-card .label { min-width: 0; color: #626b73; font-size: 13px; font-weight: 650; } + .summary-card-actions { min-width: 0; display: flex; align-items: center; justify-content: flex-end; gap: 6px; } .summary-card .jump { flex: 0 0 auto; color: #929aa1; font-size: 14px; } + .summary-detail { color: #899198; font-size: 10px; white-space: nowrap; } + .summary-badge { + display: inline-flex; align-items: center; min-height: 20px; padding: 2px 7px; border-radius: 999px; + color: #606970; background: #f0f2f3; font-size: 10px; font-weight: 700; white-space: nowrap; + } + .summary-badge.ok { color: var(--ok); background: #e8f5e9; } + .summary-badge.warn { color: var(--warn); background: #fff0d8; } + .summary-badge.danger { color: var(--danger); background: var(--accent-soft); } .summary-card .summary-value { margin-top: 6px; color: var(--text-strong); font: 680 28px/1.15 var(--mono); letter-spacing: -.7px; } .summary-card .summary-value .unit { color: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: 0; white-space: nowrap; } .summary-card .summary-sub { margin-top: 5px; overflow: hidden; color: var(--muted); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; } @@ -116,20 +127,6 @@ .summary-card.danger { border-color: #edc1bd; background: var(--accent-soft); } .summary-card.danger .summary-value { color: var(--danger); } .summary-card.danger .summary-meter > i { background: var(--danger); } - .summary-dual { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 7px; } - .summary-dual > div { min-width: 0; padding-right: 8px; border-right: 1px solid var(--border-soft); } - .summary-dual > div:last-child { padding-right: 0; border-right: 0; } - .summary-dual span { display: block; color: var(--muted); font-size: 10px; font-weight: 600; } - .summary-dual strong { display: block; overflow: hidden; margin-top: 1px; color: var(--text-strong); font: 680 23px/1.2 var(--mono); letter-spacing: -.6px; text-overflow: ellipsis; white-space: nowrap; } - .summary-task-group { display: grid; grid-template-columns: 27px minmax(0, 1fr); gap: 4px; margin-top: 5px; } - .summary-group-label { padding-top: 2px; color: #8b939a; font-size: 9px; font-weight: 700; } - .summary-task-group > div { display: flex; flex-wrap: wrap; gap: 3px; min-width: 0; } - .summary-status { padding: 1px 5px; border-radius: 999px; background: #f0f2f3; color: #4d555c; font: 10px/1.4 var(--mono); white-space: nowrap; } - .summary-status b { font-weight: 750; } - .summary-status.run { color: #1565c0; background: #e7f3ff; } - .summary-status.err { color: var(--danger); background: var(--accent-soft); } - .summary-status.ok { color: var(--ok); background: #e8f5e9; } - .task-summary .summary-value { font-size: 25px; } .card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 7px; } .card-head h2 { margin: 0; } .ctrl { display: flex; gap: 6px; } @@ -232,6 +229,60 @@ #app-dialog button { padding: 5px 12px; border: 1px solid var(--border); border-radius: var(--r-md); background: #fff; cursor: pointer; } #app-dialog button.primary { color: #fff; border-color: var(--accent); background: var(--accent); } + /* 实时执行容量使用右侧抽屉,避免在摘要卡内堆叠监控明细。 */ + html.drawer-open, body.drawer-open { overflow: hidden; } + .capacity-drawer { + display: none; position: fixed; inset: 0; z-index: 140; justify-content: flex-end; + background: rgba(18,24,30,.28); backdrop-filter: blur(1px); + } + .capacity-drawer.show { display: flex; } + .capacity-drawer-panel { + width: min(460px, calc(100vw - 32px)); height: 100%; display: flex; flex-direction: column; + background: var(--panel); border-left: 1px solid var(--border); box-shadow: -12px 0 34px rgba(18,24,30,.14); + } + .capacity-drawer-head { + min-height: 58px; display: flex; align-items: center; gap: 10px; padding: 12px 16px; + border-bottom: 1px solid var(--border); background: rgba(255,255,255,.96); + } + .capacity-drawer-head > div { min-width: 0; flex: 1; } + .capacity-drawer-head h2 { margin: 0; color: var(--text-strong); font-size: 15px; } + .capacity-drawer-head p { margin: 2px 0 0; color: var(--muted); font-size: 10px; } + .capacity-drawer-close { + width: 30px; height: 30px; flex: 0 0 auto; padding: 0; border: 1px solid var(--border); + border-radius: var(--r-md); color: var(--muted); background: #fff; font-size: 20px; line-height: 1; cursor: pointer; + } + .capacity-drawer-close:hover { color: var(--text-strong); background: var(--panel-soft); } + .capacity-drawer-body { min-height: 0; padding: 16px; overflow-y: auto; overscroll-behavior: contain; } + .capacity-alert { + display: grid; gap: 3px; margin-bottom: 12px; padding: 10px 12px; border: 1px solid #edc1bd; + border-radius: var(--r-lg); color: var(--danger); background: var(--accent-soft); + } + .capacity-alert span { font-size: 11px; } + .capacity-hero { display: grid; grid-template-columns: 1.3fr 1fr 1fr; gap: 8px; } + .capacity-hero > div { min-width: 0; padding: 11px; border: 1px solid var(--border-soft); border-radius: var(--r-lg); background: var(--panel-soft); } + .capacity-hero strong { display: block; overflow: hidden; color: var(--text-strong); font: 700 21px/1.2 var(--mono); text-overflow: ellipsis; } + .capacity-hero span { display: block; margin-top: 3px; color: var(--muted); font-size: 10px; } + .capacity-detail-section { margin-top: 18px; } + .capacity-detail-section h3 { margin: 0 0 7px; color: #5f6870; font-size: 11px; font-weight: 700; } + .capacity-detail-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 7px; } + .capacity-detail-item { min-width: 0; padding: 9px 10px; border: 1px solid var(--border-soft); border-radius: var(--r-md); } + .capacity-detail-item > span, .capacity-detail-item small { display: block; color: var(--muted); font-size: 10px; } + .capacity-detail-item strong { display: block; margin: 2px 0; color: var(--text-strong); font: 700 18px/1.2 var(--mono); } + .capacity-detail-item.warn { border-color: #efd8b7; background: #fffaf2; } + .capacity-detail-item.warn strong { color: var(--warn); } + .capacity-user-list { overflow: hidden; border: 1px solid var(--border-soft); border-radius: var(--r-md); } + .capacity-user-list > div, .capacity-resource-list > div { + display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 36px; + padding: 7px 10px; border-bottom: 1px solid var(--border-soft); + } + .capacity-user-list > div:last-child, .capacity-resource-list > div:last-child { border-bottom: 0; } + .capacity-user-list code { overflow: hidden; color: var(--text); text-overflow: ellipsis; } + .capacity-user-list span, .capacity-resource-list span { color: var(--muted); font-size: 11px; } + .capacity-resource-list { border: 1px solid var(--border-soft); border-radius: var(--r-md); } + .capacity-resource-list strong { color: var(--text-strong); font: 650 12px/1.3 var(--mono); text-align: right; } + .capacity-empty { padding: 24px 12px; color: var(--muted); text-align: center; } + .capacity-empty.compact { padding: 14px 10px; border: 1px dashed var(--border); border-radius: var(--r-md); font-size: 11px; } + #s-windows-node button, #node-enrollment-modal button { font-size: 12px; padding: 5px 11px; border: 1px solid var(--border); border-radius: var(--r-md); background: #fff; color: var(--text); cursor: pointer; @@ -365,7 +416,7 @@ .page-intro { padding: 12px 14px; } .page-intro h1 { font-size: 17px; } .ops-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } - .summary-card { min-height: 150px; } + .summary-card { min-height: 140px; } .summary-card .summary-value { font-size: 26px; } .anchor { scroll-margin-top: 150px; } .card { padding: 12px; } @@ -373,7 +424,13 @@ @media (max-width: 480px) { .ops-summary { grid-template-columns: 1fr; } - .summary-card { min-height: 146px; } + .summary-card { min-height: 138px; } + .capacity-drawer { align-items: flex-end; } + .capacity-drawer-panel { width: 100%; height: min(90vh, 760px); border: 0; border-radius: 14px 14px 0 0; } + .capacity-hero { grid-template-columns: repeat(3, minmax(0, 1fr)); } + .capacity-hero > div { padding: 9px 8px; } + .capacity-hero strong { font-size: 18px; } + .capacity-detail-grid { grid-template-columns: 1fr; } } @media (prefers-reduced-motion: reduce) { @@ -422,6 +479,19 @@
+