From 41f237c15d88a101d11125df71d1de8dafb55036 Mon Sep 17 00:00:00 2001 From: yinjianm Date: Sun, 19 Apr 2026 04:18:33 +0800 Subject: [PATCH] feat(frontend): revamp monitor cards and process table Add a dedicated CPU history chart with per-core live indicators, switch the network block to a vertical stack with a tighter height cap, and lift process counts into header pills. Also make the process list columns sortable, add spacing around the close button, and extend backend CPU sampling to include per-core usage data. --- .helloagents/CHANGELOG.md | 22 +- .../.status.json | 1 + .../proposal.md | 113 ++++++++ .../tasks.md | 47 ++++ .../.status.json | 1 + .../proposal.md | 138 ++++++++++ .../tasks.md | 56 ++++ .../proposal.md | 161 +++++++++++ .../tasks.md | 54 ++++ .../.status.json | 1 + .../proposal.md | 109 ++++++++ .../tasks.md | 50 ++++ .helloagents/archive/_index.md | 7 + .helloagents/modules/backend.md | 8 +- .helloagents/modules/frontend.md | 4 +- .../src/services/status-monitor.service.ts | 95 +++++-- .../src/components/ProcessManagerModal.vue | 231 +++++++++++++++- .../frontend/src/components/StatusMonitor.vue | 221 ++++++++------- .../StatusMonitorCpuHistoryChart.vue | 254 ++++++++++++++++++ .../StatusMonitorNetworkHistoryChart.vue | 10 +- .../src/composables/useStatusMonitor.ts | 18 +- packages/frontend/src/locales/en-US.json | 2 + packages/frontend/src/locales/ja-JP.json | 2 + packages/frontend/src/locales/zh-CN.json | 2 + packages/frontend/src/types/server.types.ts | 1 + 25 files changed, 1449 insertions(+), 159 deletions(-) create mode 100644 .helloagents/archive/2026-04/202604190349_process-total-pill-display/.status.json create mode 100644 .helloagents/archive/2026-04/202604190349_process-total-pill-display/proposal.md create mode 100644 .helloagents/archive/2026-04/202604190349_process-total-pill-display/tasks.md create mode 100644 .helloagents/archive/2026-04/202604190351_status-monitor-cpu-total-and-per-core/.status.json create mode 100644 .helloagents/archive/2026-04/202604190351_status-monitor-cpu-total-and-per-core/proposal.md create mode 100644 .helloagents/archive/2026-04/202604190351_status-monitor-cpu-total-and-per-core/tasks.md create mode 100644 .helloagents/archive/2026-04/202604190352_process-manager-table-sort-and-close-spacing/proposal.md create mode 100644 .helloagents/archive/2026-04/202604190352_process-manager-table-sort-and-close-spacing/tasks.md create mode 100644 .helloagents/archive/2026-04/202604190358_status-monitor-network-vertical-stack/.status.json create mode 100644 .helloagents/archive/2026-04/202604190358_status-monitor-network-vertical-stack/proposal.md create mode 100644 .helloagents/archive/2026-04/202604190358_status-monitor-network-vertical-stack/tasks.md create mode 100644 packages/frontend/src/components/StatusMonitorCpuHistoryChart.vue diff --git a/.helloagents/CHANGELOG.md b/.helloagents/CHANGELOG.md index 16244c3..cfa5584 100644 --- a/.helloagents/CHANGELOG.md +++ b/.helloagents/CHANGELOG.md @@ -2,7 +2,22 @@ ## [Unreleased] -- **[frontend]**: 支持将快捷指令从一个标签组拖到另一个标签组内,允许把未标记命令直接拖入目标标签组,并修正 `manual / name / last_used` 排序按钮状态映射 — by yinjianm +- **[frontend]**: 将状态监控中的 CPU 卡片升级为总 CPU `canvas` 历史图 + 每核心实时条卡,并在极窄侧栏下自动切换为单列布局 — by yinjianm + - 方案: [202604190351_status-monitor-cpu-total-and-per-core](archive/2026-04/202604190351_status-monitor-cpu-total-and-per-core/) + +- **[backend]**: 扩展 `StatusMonitorService` 的 `/proc/stat` 采样链路,新增 `cpuCorePercents` 每核心实时占用字段并与总 CPU 百分比一同下发 — by yinjianm + - 方案: [202604190351_status-monitor-cpu-total-and-per-core](archive/2026-04/202604190351_status-monitor-cpu-total-and-per-core/) + +- **[frontend]**: 将状态监控里的网络历史图和网络统计表固定改为上下堆叠,并通过压缩图表 canvas、表格间距与统计项内边距,把整个网络模块限制在 350px 以内 — by yinjianm + - 方案: [202604190358_status-monitor-network-vertical-stack](archive/2026-04/202604190358_status-monitor-network-vertical-stack/) + +- **[frontend]**: 为进程管理详细视图补充可点击表头排序,支持按 `PID / USER / STATE / CPU / MEM / START / COMMAND` 列切换升降序,并拉开关闭按钮与刷新区的安全间距 — by yinjianm + - 方案: [202604190352_process-manager-table-sort-and-close-spacing](archive/2026-04/202604190352_process-manager-table-sort-and-close-spacing/) + +- **[frontend]**: 将状态监控“进程管理”的总数从独立摘要项提升为标题区右侧胶囊,并将默认摘要区收敛为“运行中 / 休眠中”两项,减少默认卡片纵向占用 - by yinjianm + - 方案: [202604190349_process-total-pill-display](archive/2026-04/202604190349_process-total-pill-display/) + +- **[frontend]**: 支持将快捷指令从一个标签组拖到另一个标签组内,允许把未标记命令直接拖入目标标签组,并修正 `manual / name / last_used` 排序按钮状态映射 - by yinjianm - 方案: [202604190322_quickcommands-cross-group-drag-move](archive/2026-04/202604190322_quickcommands-cross-group-drag-move/) - **[frontend]**: 将状态监控中的内存与网络卡片响应式阈值统一收紧到 250px,并把网络卡片的 SVG 趋势线升级为可 hover 查看最近 24 个采样点的 canvas 历史图 — by yinjianm @@ -82,7 +97,10 @@ - 方案: [202603250614_terminal-ansi-color-effects](archive/2026-03/202603250614_terminal-ansi-color-effects/) ### 快速修改 -- **[frontend]**: 将状态监控模块区从默认并排调整回基于常用 300px 右栏比例的单列布局,并用弹性高度把普通卡控制在约 200、进程管理控制在约 400 的视觉比例 — by yinjianm +- **[frontend]**: 将状态监控“进程管理”的“运行中 / 休眠中”也收纳进标题区胶囊组,和“总数”一起以内联小显示呈现,不再保留独立摘要行 - by yinjianm + - 类型: 快速修改(无方案包) + - 文件: packages/frontend/src/components/StatusMonitor.vue:217-230,571-575,875-880 +- **[frontend]**: 将状态监控模块区从默认并排调整回基于常用 300px 右栏比例的单列布局,并用弹性高度把普通卡控制在约 200、进程管理控制在约 400 的视觉比例 - by yinjianm - 类型: 快速修改(无方案包) - 文件: packages/frontend/src/components/StatusMonitor.vue - **[frontend]**: 将状态监控模块区从大断点固定分列改为更高密度的 auto-fit 自适应网格,让内存/网络/磁盘在正常宽度下默认并排,只有非常窄时才回落为单列 — by yinjianm diff --git a/.helloagents/archive/2026-04/202604190349_process-total-pill-display/.status.json b/.helloagents/archive/2026-04/202604190349_process-total-pill-display/.status.json new file mode 100644 index 0000000..1898f17 --- /dev/null +++ b/.helloagents/archive/2026-04/202604190349_process-total-pill-display/.status.json @@ -0,0 +1 @@ +{"status":"completed","completed":3,"failed":0,"pending":0,"total":3,"done":3,"percent":100,"current":"已完成并等待归档","updated_at":"2026-04-19 04:00:00"} diff --git a/.helloagents/archive/2026-04/202604190349_process-total-pill-display/proposal.md b/.helloagents/archive/2026-04/202604190349_process-total-pill-display/proposal.md new file mode 100644 index 0000000..6790073 --- /dev/null +++ b/.helloagents/archive/2026-04/202604190349_process-total-pill-display/proposal.md @@ -0,0 +1,113 @@ +# 变更提案: process-total-pill-display + +## 元信息 +```yaml +类型: 优化 +方案类型: implementation +优先级: P2 +状态: 草案 +创建: 2026-04-19 +``` + +--- + +## 1. 需求 + +### 背景 +工作区右侧状态监控中的“进程管理”模块当前把“总数 / 运行中 / 休眠中”都放在独立摘要行里展示,其中“总数”作为单独卡片占用了一行摘要位。用户希望“总数”像现有 `monitor-module__pill` 一样以内联小胶囊显示,并挂在模块标题区域,减少纵向占用,让默认监控卡片更紧凑。 + +### 目标 +- 将“总数”从 `process-summary-strip` 中移出。 +- 在“进程管理”标题区右侧新增与现有监控胶囊一致的小显示。 +- 保持“运行中 / 休眠中”摘要和高占用进程预览逻辑不变。 + +### 约束条件 +```yaml +时间约束: 本次仅做局部前端样式与模板调整 +性能约束: 不新增数据请求,不改变状态监控刷新频率 +兼容性约束: 继续复用现有 StatusMonitor 深色监控视觉体系 +业务约束: 不修改进程统计来源和 process manager modal 行为 +``` + +### 验收标准 +- [ ] “总数”不再以独立 `process-summary-item` 单独占一行显示。 +- [ ] “总数”在“进程管理”模块标题右侧以内联胶囊展示,视觉风格与现有 `monitor-module__pill` 一致。 +- [ ] “运行中 / 休眠中”摘要仍正常显示,进程预览列表不受影响。 +- [ ] 前端构建通过。 + +--- + +## 2. 方案 + +### 技术方案 +在 `packages/frontend/src/components/StatusMonitor.vue` 中拆分进程摘要数据: +- 新增一个仅供标题区展示的总数文案或值。 +- 将 `processSummaryItems` 缩减为“运行中 / 休眠中”两项。 +- 在 `monitor-module--process` 的标题区追加一个 `monitor-module__pill` 样式节点,用于显示总数。 +- 视需要微调标题区布局与摘要区网格列数,保证小宽度下仍然稳定。 + +### 影响范围 +```yaml +涉及模块: + - frontend: 调整 StatusMonitor.vue 的模板、计算属性和局部样式 +预计变更文件: 1 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 标题区新增胶囊后在窄宽度下换行异常 | 低 | 复用现有 heading 布局并补充最小样式约束 | +| 摘要区列数变化导致视觉密度失衡 | 低 | 同步调整 `process-summary-strip` 的网格列定义 | + +--- + +## 3. 技术设计(可选) + +> 本次不涉及架构变更、API 设计或数据模型变更,N/A。 + +--- + +## 4. 核心场景 + +### 场景: 默认监控卡片中查看进程管理摘要 +**模块**: frontend +**条件**: 用户已进入 `/workspace`,右侧状态监控卡片正常渲染进程统计。 +**行为**: “总数”以内联胶囊形式显示在“进程管理”标题右侧,摘要区仅保留“运行中 / 休眠中”。 +**结果**: 模块纵向占用更紧凑,摘要信息层级更清晰。 + +--- + +## 5. 技术决策 + +### process-total-pill-display#D001: 将“总数”提升为标题区状态胶囊 +**日期**: 2026-04-19 +**状态**: 已采纳 +**背景**: “总数”属于模块级总览指标,相比“运行中 / 休眠中”更适合作为标题区状态而不是并列摘要项。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 保持在摘要区 | 改动最小 | 继续占用摘要行,层级不突出 | +| B: 提升到标题区胶囊 | 更紧凑,和现有监控胶囊风格一致 | 需要调整标题区模板与布局 | +**决策**: 选择方案 B +**理由**: 更符合该指标的总览属性,也与用户明确指定的位置一致。 +**影响**: 影响 `StatusMonitor.vue` 内进程管理模块的标题区和摘要区布局。 + +--- + +## 6. 成果设计 + +### 设计方向 +- **美学基调**: 延续现有深色终端监控卡片风格,在不改变整体主题的前提下压缩信息层级。 +- **记忆点**: “进程总数”从块级摘要变成标题区状态胶囊,信息更像实时监控标签而不是表格式统计。 +- **参考**: 直接复用当前组件内已存在的 `monitor-module__pill` 视觉语言。 + +### 视觉要素 +- **配色**: 继续使用现有绿色轻发光胶囊配色。 +- **字体**: 沿用组件现有无衬线监控字体栈,不引入新字体。 +- **布局**: 标题区左侧保留标题文案,右侧增加总数胶囊;摘要区缩减为两列。 +- **动效**: N/A,本次不新增动画。 +- **氛围**: 保持控制台式深色监控观感,不做主题偏移。 + +### 技术约束 +- **可访问性**: 保持文本可读性,不用纯图标替代文字。 +- **响应式**: 标题区在窄容器下允许自然换行但不应破坏内容顺序。 diff --git a/.helloagents/archive/2026-04/202604190349_process-total-pill-display/tasks.md b/.helloagents/archive/2026-04/202604190349_process-total-pill-display/tasks.md new file mode 100644 index 0000000..38d126f --- /dev/null +++ b/.helloagents/archive/2026-04/202604190349_process-total-pill-display/tasks.md @@ -0,0 +1,47 @@ +# 任务清单: process-total-pill-display + +> **@status:** completed | 2026-04-19 03:54 + +```yaml +@feature: process-total-pill-display +@created: 2026-04-19 +@status: completed +@mode: R2 +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 3 | 0 | 0 | 3 | + +--- + +## 任务列表 + +### 1. frontend 组件调整 + +- [√] 1.1 在 `packages/frontend/src/components/StatusMonitor.vue` 中把进程总数从摘要列表中拆分出来,并挂到模块标题区域 +- [√] 1.2 在 `packages/frontend/src/components/StatusMonitor.vue` 中调整进程摘要区布局,使“运行中 / 休眠中”保持紧凑展示 + - 依赖: 1.1 + +### 2. 验证 + +- [√] 2.1 执行前端构建验证,确认 `StatusMonitor.vue` 改动未引入编译错误 + - 依赖: 1.2 + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-04-19 03:49 | 创建方案包 | 已完成 | `create_package.py` 已生成 proposal/tasks 模板 | +| 2026-04-19 03:58 | 完成组件改动 | 已完成 | `StatusMonitor.vue` 已将总数提升到标题区胶囊,并将摘要区收敛为两项 | +| 2026-04-19 04:00 | 前端构建验证 | 已完成 | `npm --prefix "E:/code/vue/nexus-terminal/packages/frontend" run build` 通过 | + +--- + +## 执行备注 + +> 本次为局部 UI 优化,目标是减少“进程管理”默认卡片的纵向占用,不改变进程数据来源、刷新机制和 modal 行为。 diff --git a/.helloagents/archive/2026-04/202604190351_status-monitor-cpu-total-and-per-core/.status.json b/.helloagents/archive/2026-04/202604190351_status-monitor-cpu-total-and-per-core/.status.json new file mode 100644 index 0000000..c992364 --- /dev/null +++ b/.helloagents/archive/2026-04/202604190351_status-monitor-cpu-total-and-per-core/.status.json @@ -0,0 +1 @@ +{"status":"completed","completed":6,"failed":0,"pending":0,"total":6,"done":6,"percent":100,"current":"总 CPU 历史图与每核心实时条卡已完成实现并通过前后端构建校验","updated_at":"2026-04-19 04:06:18"} diff --git a/.helloagents/archive/2026-04/202604190351_status-monitor-cpu-total-and-per-core/proposal.md b/.helloagents/archive/2026-04/202604190351_status-monitor-cpu-total-and-per-core/proposal.md new file mode 100644 index 0000000..f2ddcb6 --- /dev/null +++ b/.helloagents/archive/2026-04/202604190351_status-monitor-cpu-total-and-per-core/proposal.md @@ -0,0 +1,138 @@ +# 变更提案: status-monitor-cpu-total-and-per-core + +## 元信息 +```yaml +类型: 优化 +方案类型: implementation +优先级: P1 +状态: 已确认 +创建: 2026-04-19 +``` + +--- + +## 1. 需求 + +### 背景 +当前 `/workspace` 右侧状态监控中,CPU 区只有一个装饰性的 SVG sparkline 和一条总占用进度条,无法像网络卡片那样直接查看近期历史图,也无法区分不同 CPU 核心的实时负载。用户明确要求将 CPU 区改成“总 CPU 历史图 + 不同核心分别显示”的结构,并已确认采用“保留总 CPU 历史图、下方按核心显示独立实时条卡”的方案。 + +### 目标 +- 将 CPU 卡片升级为 `canvas` 总 CPU 历史图,展示最近采样趋势。 +- 为每个 CPU 核心增加独立的实时显示条卡,明确区分不同核心当前占用。 +- 扩展后端状态采样 payload,提供每核心 CPU 百分比数据,前端直接消费。 +- 保持现有总 CPU 历史图缓存链路可用,不影响内存/网络/磁盘卡片。 + +### 约束条件 +```yaml +时间约束: 本轮限定为状态监控 CPU 链路,不扩展到设置页、审计或外部图表页面 +性能约束: 每核心仅传当前百分比,不增加每核心历史数组,避免 websocket payload 膨胀 +兼容性约束: 总 CPU 百分比和既有 cpuHistory 保持兼容,底部 StatusCharts 继续工作 +业务约束: 每核心数据来自现有 Linux /proc/stat 采样,不引入额外系统依赖 +``` + +### 验收标准 +- [ ] 后端状态 payload 新增每核心 CPU 百分比字段,并能与总 CPU 百分比一同返回。 +- [ ] 前端 CPU 卡片改为总 CPU 历史 `canvas` 图,显示最近采样趋势。 +- [ ] CPU 卡片下方按核心显示独立实时条卡,每个核心有单独标签和值。 +- [ ] 前后端构建通过;若本地无法完成登录后工作区的运行态验证,需要在执行备注中明确说明。 + +--- + +## 2. 方案 + +### 技术方案 +后端继续使用 `/proc/stat` 作为 CPU 采样源,但从只解析总 `cpu` 行扩展为同时解析 `cpu` 和 `cpuN` 行。服务端为每个会话缓存“上一次总 CPU 快照 + 每核心快照”,在下一轮采样时计算总百分比与每核心百分比,并通过 `status_update` 一起返回。前端类型定义增加 `cpuCorePercents`,CPU 卡片新增一个 `StatusMonitorCpuHistoryChart.vue` 子组件承载总 CPU `canvas` 历史图,`StatusMonitor.vue` 则在图下方或右侧渲染每核心实时条卡。 + +### 影响范围 +```yaml +涉及模块: + - backend: StatusMonitorService 的 CPU 采样与状态 payload 扩展 + - frontend: ServerStatus 类型、CPU 卡片结构与新图表子组件 + - frontend-i18n: 每核心标签与 CPU 历史图说明文案 +预计变更文件: 8 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 多核心服务器返回大量核心数据,导致 CPU 卡片过高 | 中 | 每核心条卡采用紧凑网格并允许卡片内部滚动 | +| 总 CPU 与各核心百分比计算方式不一致,导致数值观感异常 | 中 | 统一基于同一轮 `/proc/stat` 差值计算,采用相同的百分比裁剪规则 | +| 旧连接会话在首次采样前没有前序快照,导致每核心值为空 | 低 | 首轮统一返回 0,并在第二轮后进入稳定值 | + +--- + +## 3. 技术设计(可选) + +### 架构设计 +```mermaid +flowchart TD + A[/proc/stat cpu + cpuN/] --> B[StatusMonitorService] + B --> C[status.cpuPercent] + B --> D[status.cpuCorePercents[]] + C --> E[useStatusMonitor cpuHistory] + D --> F[StatusMonitor.vue core cards] + E --> G[StatusMonitorCpuHistoryChart.vue] +``` + +### 数据模型 +| 字段 | 类型 | 说明 | +|------|------|------| +| `cpuCorePercents` | `number[]` | 每个核心当前 CPU 使用率百分比,顺序对应 `cpu0..cpuN` | +| `cpuHistory` | `(number \| null)[]` | 现有总 CPU 历史采样,继续保留 | + +--- + +## 4. 核心场景 + +> 执行完成后同步到对应模块文档 + +### 场景: 侧栏中查看总 CPU 趋势 +**模块**: frontend +**条件**: 用户在 `/workspace` 查看右侧状态监控,当前会话已持续收到 CPU 状态采样。 +**行为**: CPU 卡片显示一个 `canvas` 总 CPU 历史图,用于观察最近采样趋势。 +**结果**: 用户可以不依赖底部大图,也能在 CPU 卡片中快速判断近期整体负载波动。 + +### 场景: 区分不同核心当前负载 +**模块**: frontend, backend +**条件**: 后端已为当前会话返回 `cpuCorePercents`。 +**行为**: CPU 卡片按核心渲染独立实时条卡,例如 `Core 1`、`Core 2`,每个核心都有单独百分比和进度条。 +**结果**: 用户可以快速识别负载是否集中在某些核心,而不是只能看总 CPU 百分比。 + +--- + +## 5. 技术决策 + +> 本方案涉及的技术决策,归档后成为决策的唯一完整记录 + +### status-monitor-cpu-total-and-per-core#D001: 每核心只传当前值,不维护每核心历史数组 +**日期**: 2026-04-19 +**状态**: ✅采纳 +**背景**: 用户要“总 CPU 历史图 + 每核心分别显示”。其中总图需要历史数据,但每核心展示仅要求分别显示,不一定需要每核心历史。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 总 CPU 历史 + 每核心当前值(推荐) | 数据量小、实现稳、对侧栏更友好 | 不能直接查看单核心历史 | +| B: 总 CPU 历史 + 每核心历史数组 | 信息最全,后续可扩成每核心迷你图 | websocket payload 和前端渲染复杂度明显上升 | +**决策**: 选择方案 A +**理由**: 这是最符合当前需求和侧栏密度约束的实现路径,既满足“总历史图”,也满足“不同核心不同显示”,同时避免后端和前端缓存结构膨胀。 +**影响**: 扩展后端 CPU 采样与前端 CPU 卡片结构,但不影响现有内存/网络历史缓存策略。 + +--- + +## 6. 成果设计 + +### 设计方向 +- **美学基调**: 延续当前状态监控的深色控制台面板风格,把 CPU 区从“单条概览”提升为更接近真实监控屏的图表 + 分核条卡组合。 +- **记忆点**: 同一张 CPU 卡里上方是总历史图,下方是一组按核心编号排列的独立实时条卡。 +- **参考**: 视觉语言参考已完成的网络 `canvas` 历史图和现有 usage lane 组件风格。 + +### 视觉要素 +- **配色**: 保持 CPU 既有蓝色语义色作为总图和条卡主色,不引入额外杂色。 +- **字体**: 数值继续使用 monospace,核心标签保持当前状态监控字重体系。 +- **布局**: CPU 卡片采用“历史图 + 条卡网格”组合,窄宽度时自动退为单列。 +- **动效**: 图表关闭动画,仅保留 hover tooltip;条卡用静态宽度反馈,不做额外动画。 +- **氛围**: 保持深色服务器面板的克制风格,避免把 CPU 卡做成单独花哨模块。 + +### 技术约束 +- **可访问性**: 核心条卡需包含可读标签与数值,不只靠颜色区分。 +- **响应式**: 多核心条卡在窄侧栏下要自动换列或纵向堆叠。 diff --git a/.helloagents/archive/2026-04/202604190351_status-monitor-cpu-total-and-per-core/tasks.md b/.helloagents/archive/2026-04/202604190351_status-monitor-cpu-total-and-per-core/tasks.md new file mode 100644 index 0000000..5cd6ef5 --- /dev/null +++ b/.helloagents/archive/2026-04/202604190351_status-monitor-cpu-total-and-per-core/tasks.md @@ -0,0 +1,56 @@ +# 任务清单: status-monitor-cpu-total-and-per-core + +> **@status:** completed | 2026-04-19 04:08 + +```yaml +@feature: status-monitor-cpu-total-and-per-core +@created: 2026-04-19 +@status: completed +@mode: R3 +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 6 | 0 | 0 | 6 | + +--- + +## 任务列表 + +### 1. 后端 CPU 状态扩展 + +- [√] 1.1 在 `packages/backend/src/services/status-monitor.service.ts` 中扩展 `/proc/stat` 解析,计算每核心 CPU 百分比并写入状态 payload | depends_on: [] +- [√] 1.2 运行 `@nexus-terminal/backend` 构建校验,确认每核心 CPU 字段扩展未破坏现有服务端类型与编译 | depends_on: [1.1] + +### 2. 前端 CPU 卡片重构 + +- [√] 2.1 在 `packages/frontend/src/types/server.types.ts` 与 `packages/frontend/src/composables/useStatusMonitor.ts` 中接入每核心 CPU 状态字段 | depends_on: [1.1] +- [√] 2.2 新增 `packages/frontend/src/components/StatusMonitorCpuHistoryChart.vue`,显示总 CPU canvas 历史图 | depends_on: [2.1] +- [√] 2.3 在 `packages/frontend/src/components/StatusMonitor.vue` 中将 CPU 卡片改为“总历史图 + 每核心实时条卡”布局 | depends_on: [2.2] +- [√] 2.4 在 `packages/frontend/src/locales/zh-CN.json`、`packages/frontend/src/locales/en-US.json`、`packages/frontend/src/locales/ja-JP.json` 中补充 CPU 历史与核心标签文案,并运行 `@nexus-terminal/frontend` 构建校验 | depends_on: [2.3] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-04-19 03:57 | design | completed | 已创建 implementation 方案包,范围锁定为总 CPU 历史图与每核心实时条卡的前后端联动实现 | +| 2026-04-19 04:00 | 1.1 | completed | 已扩展 `/proc/stat` 解析为总 CPU + `cpuN` 每核心快照,并将 `cpuCorePercents` 一并写入状态 payload | +| 2026-04-19 04:01 | 1.2 | completed | `npm --workspace @nexus-terminal/backend run build` 通过 | +| 2026-04-19 04:02 | 2.1 | completed | 已在前端状态类型与状态监控 composable 中接入 `cpuCorePercents` 并做数组归一化 | +| 2026-04-19 04:03 | 2.2 | completed | 已新增 `StatusMonitorCpuHistoryChart.vue`,用 canvas 展示最近 24 个总 CPU 历史采样点 | +| 2026-04-19 04:04 | 2.3 | completed | 已将 CPU 卡片改为“总历史图 + 每核心实时条卡”布局,并在极窄宽度下切换为单列 | +| 2026-04-19 04:06 | 2.4 | completed | 已补充中英日 CPU 文案;`npm --workspace @nexus-terminal/frontend run build` 通过,并额外确认 `npm --workspace @nexus-terminal/frontend run preview -- --host 127.0.0.1 --port 4173` 可返回 HTTP 200 | + +--- + +## 执行备注 + +> 记录执行过程中的重要说明、决策变更、风险提示等 + +- 本轮选择方案 A:保留总 CPU 历史图,但每核心仅展示当前实时值,不额外缓存每核心历史数组。 +- CPU 差值计算中的空闲时间按 `idle + iowait` 处理,避免高负载判断偏高。 +- 运行态限制: 已确认前端预览服务能正常启动并返回首页,但未在本地接入真实活动 SSH 会话,因此“带真实 CPU 数据的分核条卡与 tooltip 最终观感”仍建议在你的 `/workspace` 环境手动看一遍。 diff --git a/.helloagents/archive/2026-04/202604190352_process-manager-table-sort-and-close-spacing/proposal.md b/.helloagents/archive/2026-04/202604190352_process-manager-table-sort-and-close-spacing/proposal.md new file mode 100644 index 0000000..9da7df5 --- /dev/null +++ b/.helloagents/archive/2026-04/202604190352_process-manager-table-sort-and-close-spacing/proposal.md @@ -0,0 +1,161 @@ +# 变更提案: process-manager-table-sort-and-close-spacing + +## 元信息 +```yaml +类型: 优化 +方案类型: implementation +优先级: P1 +状态: 已完成 +创建: 2026-04-19 +完成: 2026-04-19 +``` + +--- + +## 1. 需求 + +### 背景 +当前 `ProcessManagerModal.vue` 已经能展示服务器进程明细、支持搜索和手动刷新,但明细表仍然是纯静态表头。用户进入进程管理详细视图后,无法按 `PID`、`CPU`、`MEM` 等列快速切换视角来定位高占用进程或按进程号排查问题。同时,右上角绝对定位的关闭按钮与顶部刷新控制区距离过近,在当前布局下容易产生视觉拥挤,影响操作清晰度。 + +### 目标 +- 允许点击进程管理表格头,按主要数据列切换排序显示。 +- 为当前激活的排序列显示明确的升序/降序方向标记。 +- 保持默认列表顺序与现有后端返回顺序一致,避免未点击表头前的展示语义回归。 +- 拉开关闭按钮与刷新区的安全间距,避免顶部工具区拥挤。 + +### 约束条件 +```yaml +时间约束: 本轮完成前端实现、构建验证与知识库同步 +性能约束: 不新增依赖,继续使用现有 Vue 计算属性完成前端本地排序 +兼容性约束: 不修改后端 process:list 消息结构,不改变默认未排序态 +业务约束: 所有主要数据列都支持排序,但操作列保持不可排序 +``` + +### 验收标准 +- [ ] 进程管理表头至少支持 `PID / USER / STATE / CPU / MEM / START / COMMAND` 点击排序。 +- [ ] 点击同一列表头时可以在升序和降序之间切换,并显示方向标记。 +- [ ] 数值列首次点击默认按更符合排障习惯的方向排序,文本列首次点击默认按字母升序排序。 +- [ ] 默认未点击表头时,进程列表仍保持现有后端返回顺序。 +- [ ] 关闭按钮与刷新区之间有明确安全间距,不再显得贴近或覆盖。 +- [ ] `npm run build --workspace @nexus-terminal/frontend` 通过。 + +--- + +## 2. 方案 + +### 技术方案 +本次改动只落在前端 `ProcessManagerModal.vue`。 + +第一部分是表格排序。新增本地排序状态 `sortKey + sortDirection`,基于当前 `processItems` 先执行搜索过滤,再在计算属性中按所选列进行稳定排序。默认 `sortKey=null`,保持后端原始顺序;点击表头后进入排序态。同一列重复点击则在升序/降序间切换,不同列首次点击则采用按列预设的默认方向,其中 `PID / CPU / MEM` 首次点击走降序或更符合当前字段的排障方向,文本列首次点击走升序。 + +第二部分是表头交互。把静态 `` 改成内嵌按钮的可点击表头,复用现有 `common.sortAscending` / `common.sortDescending` 文案做无障碍标签和 title,并为当前激活列显示 `fa-chevron-up/down` 方向标记,未激活列显示弱提示排序图标。 + +第三部分是顶部布局修正。在 toolbar 区域给右上角关闭按钮预留固定安全空间,同时把关闭按钮本身做成固定尺寸的独立点击区,避免它和右侧刷新按钮在视觉上挤在一起。 + +### 影响范围 +```yaml +涉及模块: + - frontend: ProcessManagerModal 需要新增本地排序状态、表头交互和顶部安全间距修正 + - knowledge-base: 需要同步 frontend 模块文档与 CHANGELOG +预计变更文件: 4-6 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 自动刷新后排序状态丢失,用户每次都要重新点击表头 | 中 | 排序状态存放在组件 ref 中,列表刷新后通过计算属性持续应用 | +| 默认直接改成某个列排序,导致现有展示语义回归 | 中 | 默认保持 `sortKey=null`,只在点击表头后进入排序态 | +| 关闭按钮安全区处理不当,导致窄屏下搜索区被额外压缩 | 低 | 只在 toolbar 右侧预留最小必要空间,并保留移动端纵向布局回退 | + +--- + +## 3. 技术设计 + +### 架构设计 +```mermaid +flowchart LR + A[processItems] --> B[搜索过滤] + B --> C{sortKey 是否为空} + C -->|是| D[保持原始顺序] + C -->|否| E[按 sortKey 和 sortDirection 排序] + E --> F[表格渲染 + 方向图标] +``` + +### 数据模型 +| 字段 | 类型 | 说明 | +|------|------|------| +| `ProcessSortKey` | `'pid' | 'user' | 'state' | 'cpu' | 'mem' | 'startedAt' | 'command'` | 进程表可排序字段集合 | +| `sortKey` | `ProcessSortKey \| null` | 当前激活排序列,`null` 表示保持后端原始顺序 | +| `sortDirection` | `'asc' \| 'desc'` | 当前排序方向 | + +--- + +## 4. 核心场景 + +### 场景: 按 CPU 或内存占用定位高占用进程 +**模块**: frontend +**条件**: 用户打开进程管理详细视图并查看当前服务器进程列表。 +**行为**: 用户点击 `CPU` 或 `MEM` 表头,表格立即按所选列排序,再次点击则切换方向。 +**结果**: 高占用进程可以被快速顶到表格前部,便于排障。 + +### 场景: 按 PID 或用户筛查进程 +**模块**: frontend +**条件**: 用户需要按 PID 区间或用户归属查找进程。 +**行为**: 用户点击 `PID` 或 `USER` 表头,表格按对应列排序,并显示当前方向图标。 +**结果**: 进程定位路径更短,不再依赖滚动肉眼扫描。 + +### 场景: 顶部工具区避免按钮挤压 +**模块**: frontend +**条件**: 用户打开进程管理详细视图,右上角显示关闭按钮,工具栏右侧显示刷新区。 +**行为**: 组件为关闭按钮保留独立安全区,并增加它与刷新控制区的水平间距。 +**结果**: 关闭按钮与刷新区视觉分离,点击目标更清晰。 + +--- + +## 5. 技术决策 + +### process-manager-table-sort-and-close-spacing#D001: 默认保持后端原始顺序,仅在点击表头后进入排序态 +**日期**: 2026-04-19 +**状态**: ✅采纳 +**背景**: 用户要求增加按列排序,但没有要求改变当前默认列表的初始顺序。直接默认按某列排序会改变已有视图语义。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 默认直接按 CPU 或 PID 排序 | 打开即有更强导向性 | 会改变当前默认展示语义,带来回归风险 | +| B: 默认保持原顺序,点击后再进入排序态 | 兼容现有展示逻辑,只新增明确可控交互 | 首次使用需要多一次点击 | +**决策**: 选择方案 B +**理由**: 这是最保守也最清晰的扩展方式。用户获得排序能力,但现有默认列表不会被悄悄改写。 +**影响**: frontend + +### process-manager-table-sort-and-close-spacing#D002: 数值列和文本列使用不同的首次点击默认方向 +**日期**: 2026-04-19 +**状态**: ✅采纳 +**背景**: 进程表里 `CPU / MEM` 更常用于找“最大值”,而 `USER / COMMAND` 更符合按字母升序浏览。统一所有列首次点击方向会降低使用效率。 +**选项分析**: +| 选项 | 优点 | 缺点 | +|------|------|------| +| A: 所有列首次点击统一升序 | 规则简单 | 不符合 CPU / MEM 等排障高频场景 | +| B: 按列预设首次点击方向 | 更贴合实际使用心智 | 需要额外维护一份列配置 | +**决策**: 选择方案 B +**理由**: 进程管理是高频排障界面,优先服务常见操作路径比统一规则更重要。 +**影响**: frontend + +--- + +## 6. 成果设计 + +### 设计方向 +- **美学基调**: 延续现有深色控制台式 modal 风格,只做信息结构强化,不做风格重绘。 +- **记忆点**: 表头从纯文本升级为可点击的控制条,并以低干扰方向箭头提示当前排序状态。 +- **参考**: 复用仓库内连接页与仪表盘已有的排序按钮语言。 + +### 视觉要素 +- **配色**: 继续沿用当前表头前景色、hover 背景和边框色,不新增独立主题色。 +- **字体**: 继承现有表头大写小字号风格,方向图标作为次级提示。 +- **布局**: 保持原表格列宽和工具栏结构,仅在表头内部嵌入按钮、在 toolbar 右侧留出关闭按钮安全区。 +- **动效**: 复用当前 hover 和颜色过渡,方向图标不做额外动画。 +- **氛围**: 保持深色服务器控制台气质,让交互增强看起来像原生能力扩展。 + +### 技术约束 +- **可访问性**: 排序表头使用 button,提供 aria-label 和 title。 +- **响应式**: 继续兼容当前 860px 以下的 toolbar 纵向布局。 diff --git a/.helloagents/archive/2026-04/202604190352_process-manager-table-sort-and-close-spacing/tasks.md b/.helloagents/archive/2026-04/202604190352_process-manager-table-sort-and-close-spacing/tasks.md new file mode 100644 index 0000000..d74472e --- /dev/null +++ b/.helloagents/archive/2026-04/202604190352_process-manager-table-sort-and-close-spacing/tasks.md @@ -0,0 +1,54 @@ +# 任务清单: process-manager-table-sort-and-close-spacing + +> **@status:** completed | 2026-04-19 04:03 + +```yaml +@feature: process-manager-table-sort-and-close-spacing +@created: 2026-04-19 +@status: completed +@mode: R2 +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 6 | 0 | 0 | 6 | + +--- + +## 任务列表 + +### 1. 方案与现状确认 + +- [√] 1.1 确认 `packages/frontend/src/components/ProcessManagerModal.vue` 当前仅支持搜索与刷新,不支持表头排序,并定位关闭按钮与刷新区的布局关系 | depends_on: [] + +### 2. 前端实现 + +- [√] 2.1 在 `packages/frontend/src/components/ProcessManagerModal.vue` 中新增本地排序状态与排序计算逻辑,支持主要数据列点击排序 | depends_on: [1.1] +- [√] 2.2 在 `packages/frontend/src/components/ProcessManagerModal.vue` 中把静态表头改为可点击排序按钮,并显示升降序方向标记 | depends_on: [2.1] +- [√] 2.3 在 `packages/frontend/src/components/ProcessManagerModal.vue` 中调整 toolbar 与关闭按钮的安全间距,避免关闭按钮与刷新区过近 | depends_on: [1.1] + +### 3. 验证与同步 + +- [√] 3.1 执行 `npm run build --workspace @nexus-terminal/frontend`,确认类型检查和构建通过 | depends_on: [2.2, 2.3] +- [√] 3.2 同步更新 `.helloagents/modules/frontend.md` 与 `.helloagents/CHANGELOG.md`,记录进程管理表头排序与顶部间距优化 | depends_on: [3.1] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-04-19 03:51 | EVALUATE | completed | 已按 R2 确认进程管理表需支持全量主要列排序,用户选择所有主要数据列可排序 | +| 2026-04-19 03:52 | DESIGN | completed | 已将用户补充的“关闭按钮与刷新区过近”并入同一方案包处理 | +| 2026-04-19 03:54 | 2.1 / 2.2 / 2.3 | completed | `ProcessManagerModal.vue` 已补齐本地排序状态、可点击表头方向标记和 toolbar 安全间距 | +| 2026-04-19 03:55 | validate_package | completed | `validate_package.py 202604190352_process-manager-table-sort-and-close-spacing --path E:/code/vue/nexus-terminal` 通过 | +| 2026-04-19 04:01 | 3.1 | completed | `npm run build --workspace @nexus-terminal/frontend` 通过;期间顺手将 `server.types.ts` 中 `cpuCorePercents` 放宽为只读数组以匹配当前 StatusMonitor 用法 | +| 2026-04-19 04:03 | 3.2 | completed | 已同步 frontend 模块文档与 CHANGELOG,准备归档方案包 | + +--- + +## 执行备注 + +> 默认顺序保持当前后端返回结果,只在点击表头后进入排序态,以避免进程表的初始展示语义回归;为打通前端构建,还顺手把现有 `ServerStatus.cpuCorePercents` 类型从可变数组放宽为只读数组,但不改变任何运行时行为。 diff --git a/.helloagents/archive/2026-04/202604190358_status-monitor-network-vertical-stack/.status.json b/.helloagents/archive/2026-04/202604190358_status-monitor-network-vertical-stack/.status.json new file mode 100644 index 0000000..d6eb11a --- /dev/null +++ b/.helloagents/archive/2026-04/202604190358_status-monitor-network-vertical-stack/.status.json @@ -0,0 +1 @@ +{"status":"completed","completed":4,"failed":0,"pending":0,"total":4,"done":4,"percent":100,"current":"网络模块上下堆叠与 350px 高度约束已完成,待归档方案包","updated_at":"2026-04-19 04:04:00"} diff --git a/.helloagents/archive/2026-04/202604190358_status-monitor-network-vertical-stack/proposal.md b/.helloagents/archive/2026-04/202604190358_status-monitor-network-vertical-stack/proposal.md new file mode 100644 index 0000000..6b7c5e5 --- /dev/null +++ b/.helloagents/archive/2026-04/202604190358_status-monitor-network-vertical-stack/proposal.md @@ -0,0 +1,109 @@ +# 变更提案: status-monitor-network-vertical-stack + +## 元信息 +```yaml +类型: 修复增强 +方案类型: implementation +优先级: P2 +状态: 已完成 +状态说明: 网络模块已固定为上下堆叠,并通过样式压缩将总高度限制在 350px 以内 +创建: 2026-04-19 +``` + +--- + +## 1. 需求 + +### 背景 +当前状态监控中的网络模块在大多数宽度下采用“左侧网络历史图 + 右侧网络统计表”的横向布局。用户明确要求这两块不再左右并排,而是始终改为上下排列;同时新增硬性约束,整个网络模块的总高度不能超过 `350px`,以免在右侧状态监控栏里挤占过多纵向空间。 + +### 目标 +- 让网络历史图和网络统计表在所有宽度下都改为上下堆叠。 +- 将网络模块总高度控制在 `350px` 以内。 +- 保持现有图表、接口名、上下行统计内容与主题风格不变,只调整布局和密度。 + +### 约束条件 +```yaml +范围约束: 只调整 packages/frontend/src/components/StatusMonitor.vue 与 StatusMonitorNetworkHistoryChart.vue 及相关知识库记录 +布局约束: 不再保留大宽度横向并排分栏,网络模块始终单列堆叠 +高度约束: 网络模块整体高度必须 <= 350px +验证约束: 当前项目存在登录态依赖,不使用 Playwright 做页面验收,改为构建与代码级布局审查 +兼容约束: 保持现有网络接口名、上下行图例、统计字段与容器查询响应式体系 +``` + +### 验收标准 +- [x] 网络历史图与网络统计表在所有宽度下都按上下顺序展示 +- [x] 网络模块总高度被显式限制在 `350px` 以内 +- [x] 图表 canvas 和统计表间距同步压缩,避免堆叠后出现纵向溢出 +- [x] 前端构建通过,frontend 模块文档与归档记录同步更新 + +--- + +## 2. 方案 + +### 技术方案 +将 `StatusMonitor.vue` 中网络模块的 `.module-split--network` 从双列 grid 改为单列 grid,并为 `.monitor-module--network` 增加 `max-height: 350px` 与更紧凑的 gap。同步压缩网络图和网络表的垂直占用:降低 `StatusMonitorNetworkHistoryChart.vue` 中 canvas 的固定高度,收紧 header 与 legend 间距;在 `StatusMonitor.vue` 中缩小 `network-table`、`network-stat-stack` 与 `network-stat` 的 padding/gap,让堆叠后的两块内容能稳定落在总高度约束内。 + +### 影响范围 +```yaml +涉及模块: + - frontend: StatusMonitor.vue + - frontend: StatusMonitorNetworkHistoryChart.vue + - frontend: .helloagents/modules/frontend.md +预计变更文件: 5-7 +``` + +### 风险评估 +| 风险 | 等级 | 应对 | +|------|------|------| +| 图表高度压缩过多后可读性下降 | 中 | 只收缩到能满足 350px 的最小必要值,保留标题、图例和折线区的基本层级 | +| 单列堆叠后网络表在窄栏里仍可能被内部 padding 撑高 | 低 | 同步压缩 `network-table` 和 `network-stat` 的 gap/padding,而不是只改父容器方向 | +| 仅用代码级验收可能遗漏运行态细节 | 低 | 在方案包和最终结果中明确标注本次未走 Playwright,建议在登录态环境做人工目视补验 | + +--- + +## 3. 技术设计(可选) + +> N/A,本次不涉及架构、API 或数据模型变更。 + +--- + +## 4. 核心场景 + +### 场景: 网络模块固定上下堆叠 +**模块**: frontend +**条件**: 用户在 `/workspace` 右侧状态监控面板查看网络模块 +**行为**: 网络历史图先显示在上方,网络统计表显示在下方,不再根据宽度切回左右双列 +**结果**: 网络模块信息阅读顺序变为自上而下,符合用户期望 + +### 场景: 网络模块高度受控 +**模块**: frontend +**条件**: 状态监控右侧栏按常规窄栏宽度渲染网络模块 +**行为**: 图表高度、卡片间距和统计项内边距同时收紧,使整个网络模块保持在 350px 以内 +**结果**: 网络模块不会因改成竖排而明显拉长整列布局 + +--- + +## 5. 技术决策 + +> 本方案不涉及新的长期技术决策;沿用现有状态监控组件拆分与容器查询体系。 + +--- + +## 6. 成果设计 + +### 设计方向 +- **美学基调**: 保持现有深色服务器监控面板风格,但把网络模块收紧为更紧凑的“上图下表”信息块,强调纵向节奏感 +- **记忆点**: 网络模块被压成一张纵向卡片,上方是折线历史,下方是双行上下行统计,整体更像窄屏监控屏的小组件 +- **参考**: 当前 `StatusMonitor.vue` 右侧监控栏和现有网络模块视觉体系 + +### 视觉要素 +- **配色**: 保留现有蓝色下行、绿色上行以及深色玻璃卡片背景 +- **字体**: 沿用当前模块内的 monospace 数值与默认界面字重层级 +- **布局**: 父容器改为单列堆叠,图表区高度收紧,表格区用更小 gap 和 padding 承接 +- **动效**: 不新增动画,保留现有 Chart.js 实时更新反馈 +- **氛围**: 维持当前深色科技感卡片和柔和描边,不改变网络模块的视觉语言 + +### 技术约束 +- **可访问性**: 标题、图例、上下行标签仍需完整可读,不因压缩而丢失语义 +- **响应式**: 不破坏现有容器查询体系,但网络模块不再依赖宽度阈值切换双列 diff --git a/.helloagents/archive/2026-04/202604190358_status-monitor-network-vertical-stack/tasks.md b/.helloagents/archive/2026-04/202604190358_status-monitor-network-vertical-stack/tasks.md new file mode 100644 index 0000000..37d51eb --- /dev/null +++ b/.helloagents/archive/2026-04/202604190358_status-monitor-network-vertical-stack/tasks.md @@ -0,0 +1,50 @@ +# 任务清单: status-monitor-network-vertical-stack + +> **@status:** completed | 2026-04-19 04:03 + +```yaml +@feature: status-monitor-network-vertical-stack +@created: 2026-04-19 +@status: completed +@mode: R2 +``` + +## 进度概览 + +| 完成 | 失败 | 跳过 | 总数 | +|------|------|------|------| +| 4 | 0 | 0 | 4 | + +--- + +## 任务列表 + +### 1. 方案与约束确认 + +- [√] 1.1 创建网络模块上下堆叠方案包并锁定“总高度 <= 350px”约束 | depends_on: [] + +### 2. 网络模块布局调整 + +- [√] 2.1 将网络模块父容器改为固定上下堆叠,不再保留左右双列布局 | depends_on: [1.1] +- [√] 2.2 压缩图表和网络表内部高度占用,使网络模块总高度不超过 350px | depends_on: [2.1] + +### 3. 验证与同步 + +- [√] 3.1 运行前端构建并同步 frontend 模块文档、CHANGELOG 与归档记录 | depends_on: [2.2] + +--- + +## 执行日志 + +| 时间 | 任务 | 状态 | 备注 | +|------|------|------|------| +| 2026-04-19 03:58 | 1.1 | 完成 | 创建 implementation 方案包,范围锁定为网络模块上下堆叠与 350px 高度约束 | +| 2026-04-19 04:02 | 2.1 | 完成 | `.module-split--network` 改为固定单列堆叠,不再按宽度切回双列 | +| 2026-04-19 04:03 | 2.2 | 完成 | 为网络模块增加 `max-height: 350px`,并同步压缩图表 canvas 与网络表间距 | +| 2026-04-19 04:04 | 3.1 | 完成 | `npm run build --workspace @nexus-terminal/frontend` 通过;本次按用户要求未使用 Playwright | + +--- + +## 执行备注 + +> 用户已确认网络图表和统计表在所有宽度下都要改为上下堆叠,并新增“网络模块总高度不超过 350px”的硬性限制。当前项目存在登录态依赖,本次不使用 Playwright 作为验收手段,运行态效果建议在有登录态的环境中人工补验一次。 diff --git a/.helloagents/archive/_index.md b/.helloagents/archive/_index.md index 3562b15..1c84cd1 100644 --- a/.helloagents/archive/_index.md +++ b/.helloagents/archive/_index.md @@ -7,6 +7,10 @@ | 时间戳 | 名称 | 类型 | 涉及模块 | 决策 | 结果 | |--------|------|------|---------|------|------| +| 202604190351 | status-monitor-cpu-total-and-per-core | implementation | frontend, backend | status-monitor-cpu-total-and-per-core#D001 | ✅完成 | +| 202604190358 | status-monitor-network-vertical-stack | implementation | frontend | - | ✅完成 | +| 202604190352 | process-manager-table-sort-and-close-spacing | implementation | frontend | process-manager-table-sort-and-close-spacing#D001, process-manager-table-sort-and-close-spacing#D002 | ✅完成 | +| 202604190349 | process-total-pill-display | - | - | - | ✅完成 | | 202604190322 | quickcommands-cross-group-drag-move | implementation | frontend | quickcommands-cross-group-drag-move#D001, quickcommands-cross-group-drag-move#D002 | ✅完成 | | 202604190319 | status-monitor-memory-network-canvas-history | - | - | - | ✅完成 | | 202604190208 | quickcommands-drag-reorder | - | - | - | ✅完成 | @@ -61,6 +65,9 @@ ## 按月归档 ### 2026-04 +- [202604190351_status-monitor-cpu-total-and-per-core](./2026-04/202604190351_status-monitor-cpu-total-and-per-core/) - 将状态监控 CPU 卡片升级为总 CPU canvas 历史图,并新增每核心实时条卡与后端 `cpuCorePercents` 采样链路 +- [202604190358_status-monitor-network-vertical-stack](./2026-04/202604190358_status-monitor-network-vertical-stack/) - 将状态监控网络模块固定改为“上方历史图 + 下方统计表”的纵向堆叠,并把总高度限制在 350px 以内 +- [202604190352_process-manager-table-sort-and-close-spacing](./2026-04/202604190352_process-manager-table-sort-and-close-spacing/) - 为进程管理详细视图补充可点击表头排序,并拉开关闭按钮与刷新区的安全间距 - [202604190322_quickcommands-cross-group-drag-move](./2026-04/202604190322_quickcommands-cross-group-drag-move/) - 支持将快捷指令从一个标签组拖到另一个标签组内,并允许把未标记命令直接拖入目标标签组 - [202604190319_status-monitor-memory-network-canvas-history](./2026-04/202604190319_status-monitor-memory-network-canvas-history/) - 将状态监控中的内存与网络卡片响应式阈值统一收紧到 250px,并把网络卡片的 SVG 趋势线升级为可 hover 查看最近 24 个采样点的 canvas 历史图 - [202604190210_connection-card-default-test-button](./2026-04/202604190210_connection-card-default-test-button/) - 将连接管理页 SSH 连接卡片的默认操作区调整为“连接 / 测试 / 更多”,并移除更多菜单中的重复测试入口 diff --git a/.helloagents/modules/backend.md b/.helloagents/modules/backend.md index f1cd0e4..2accc6f 100644 --- a/.helloagents/modules/backend.md +++ b/.helloagents/modules/backend.md @@ -66,8 +66,8 @@ ### 状态监控 **条件**: 前端工作区通过 WebSocket 订阅服务器状态。 -**行为**: `StatusMonitorService` 通过 SSH 读取 `free`、`df`、`/proc/stat`、`/proc/net/dev`、`date` 与 `/proc/uptime`,同时计算瞬时网速与默认网卡自开机以来的累计上下行字节数,并在常规 `status_update` 中附带服务器时区、运行秒数和轻量级进程摘要(总数、运行中、休眠中、Top 进程预览)。 -**结果**: 前端状态监控既能展示实时资源状态,也能直接展示服务器时区、运行时间和默认进程概览,而无需再为这些基础信息单独请求后端。 +**行为**: `StatusMonitorService` 通过 SSH 读取 `free`、`df`、`/proc/stat`、`/proc/net/dev`、`date` 与 `/proc/uptime`,同时计算瞬时网速与默认网卡自开机以来的累计上下行字节数,并在常规 `status_update` 中附带服务器时区、运行秒数和轻量级进程摘要(总数、运行中、休眠中、Top 进程预览);其中 CPU 采样当前会同时解析 `/proc/stat` 中的总 `cpu` 行与各 `cpuN` 行,为总 CPU 与每核心分别计算差值百分比,并把 `cpuCorePercents` 一并下发。 +**结果**: 前端状态监控既能展示实时资源状态,也能直接展示服务器时区、运行时间、默认进程概览以及每核心 CPU 实时占用,而无需再为这些基础信息单独请求后端。 ## 依赖关系 @@ -78,8 +78,8 @@ ### 状态监控字段扩展 **条件**: `StatusMonitorService` 为前端工作区持续轮询服务器状态。 -**行为**: 当前状态采集链路除 `free`、`df`、`/proc/stat` 与 `/proc/net/dev` 外,还会补充解析 `memFree`、`memCached`、`diskAvailable`、`diskMountPoint`、`diskFsType`、`diskDevice`,并基于 `/proc/diskstats` 计算根设备的磁盘读写速率;CPU 规格信息则会先读取 CPU 型号,再通过 `nproc`、`getconf _NPROCESSORS_ONLN`、`grep -c '^processor' /proc/cpuinfo` 与 `lscpu` 多级回退获取 `cpuCores`;本轮还新增服务器时区、运行时间和默认进程摘要采集。与此同时,`websocket/connection.ts` 新增 `process:list` 与 `process:signal` 消息分发,后端会在当前活动 SSH 会话上下文中执行 `ps` 与 `kill` 指令,返回完整进程列表及结束/强制结束结果。 -**结果**: 前端默认状态监控可以展示更完整的小屏监控信息,而“查看全部”进程管理 modal 也能沿同一 SSH 会话上下文安全复用进程查询与操作能力。 +**行为**: 当前状态采集链路除 `free`、`df`、`/proc/stat` 与 `/proc/net/dev` 外,还会补充解析 `memFree`、`memCached`、`diskAvailable`、`diskMountPoint`、`diskFsType`、`diskDevice`,并基于 `/proc/diskstats` 计算根设备的磁盘读写速率;CPU 规格信息则会先读取 CPU 型号,再通过 `nproc`、`getconf _NPROCESSORS_ONLN`、`grep -c '^processor' /proc/cpuinfo` 与 `lscpu` 多级回退获取 `cpuCores`;CPU 使用率则基于 `/proc/stat` 的总快照和各 `cpuN` 快照做会话级差值计算,空闲时间按 `idle + iowait` 处理,并新增 `cpuCorePercents` 字段供前端展示每核心实时占用;本轮还新增服务器时区、运行时间和默认进程摘要采集。与此同时,`websocket/connection.ts` 新增 `process:list` 与 `process:signal` 消息分发,后端会在当前活动 SSH 会话上下文中执行 `ps` 与 `kill` 指令,返回完整进程列表及结束/强制结束结果。 +**结果**: 前端默认状态监控可以展示更完整的小屏监控信息,包括总 CPU 历史趋势所依赖的总占用与每核心实时条卡所需的数据,而“查看全部”进程管理 modal 也能沿同一 SSH 会话上下文安全复用进程查询与操作能力。 ### 快捷指令顺序持久化 **条件**: 前端快捷指令视图提交分组拖拽、标签内命令拖拽或扁平列表命令拖拽结果。 diff --git a/.helloagents/modules/frontend.md b/.helloagents/modules/frontend.md index da3ef83..eab146d 100644 --- a/.helloagents/modules/frontend.md +++ b/.helloagents/modules/frontend.md @@ -58,8 +58,8 @@ ### 状态监控卡片 **条件**: 用户在 `/workspace` 右侧状态监控面板查看服务器资源状态。 -**行为**: `StatusMonitor.vue` 当前已从通用卡片栅格重排为更接近参考图的窄屏监控结构:顶部改为成对的信息条,资源概览改为带编号的紧凑使用率行,内存/网络/磁盘模块都采用明显的左右分区关系;其中内存卡片现在会在容器宽度大于等于 250px 时维持环形概览与统计块的高密度横向布局,仅在低于 250px 时切为手机式竖排;网络卡片则通过新增 `StatusMonitorNetworkHistoryChart.vue` 把原本的 SVG 趋势线替换为基于 Chart.js `canvas` 的最近 24 个采样点历史图,并在宽度大于等于 250px 时保持“左侧历史图 + 右侧统计表”的横向布局,低于 250px 再切为竖排;磁盘模块继续展示设备视觉块与紧凑磁盘摘要;默认视图底部继续保留“进程管理”概览与高占用进程预览,并通过“查看全部”打开 `ProcessManagerModal.vue`。该 modal 继续采用深色控制台式表格布局,支持搜索 PID / 用户 / 命令、自动刷新、手动刷新,以及对单个进程执行“结束”或“强制结束”操作,并通过当前活动 SSH 会话的 `wsManager` 与后端 `process:list` / `process:signal` 消息交互。 -**结果**: 前端状态监控形成了“更贴近参考图的默认小屏监控 + 独立进程管理页”的双层结构:默认面板不仅保持了侧栏内的高密度布局,还允许用户直接在网络卡片里查看近期网络历史波动,而完整进程管理继续独立存在,不挤占侧栏本体。 +**行为**: `StatusMonitor.vue` 当前已从通用卡片栅格重排为更接近参考图的窄屏监控结构:顶部改为成对的信息条,资源概览改为带编号的紧凑使用率行,内存/网络/磁盘模块都采用明显的左右分区关系;其中内存卡片现在会在容器宽度大于等于 250px 时维持环形概览与统计块的高密度横向布局,仅在低于 250px 时切为手机式竖排;网络卡片则通过新增 `StatusMonitorNetworkHistoryChart.vue` 把原本的 SVG 趋势线替换为基于 Chart.js `canvas` 的最近 24 个采样点历史图,并进一步固定为“上方历史图 + 下方统计表”的纵向堆叠结构,不再切回左右双列,同时通过压缩图表 canvas 高度、网络表间距与统计项 padding,把整个网络模块约束在 `350px` 以内;CPU 卡片也已升级为 `StatusMonitorCpuHistoryChart.vue` 驱动的总 CPU `canvas` 历史图,并在图表旁/下方用紧凑网格展示每个核心的独立实时条卡,在容器宽度低于 250px 时自动退化为单列;磁盘模块继续展示设备视觉块与紧凑磁盘摘要;默认视图底部继续保留“进程管理”概览与高占用进程预览,并通过“查看全部”打开 `ProcessManagerModal.vue`。其中进程统计当前已整体收纳到模块标题区右侧的一组 `monitor-module__pill` 胶囊中,统一展示“总数 / 运行中 / 休眠中”,不再额外保留独立摘要行,以减少默认卡片的纵向占用。该 modal 继续采用深色控制台式表格布局,支持搜索 PID / 用户 / 命令、自动刷新、手动刷新,以及对单个进程执行“结束”或“强制结束”操作,并通过当前活动 SSH 会话的 `wsManager` 与后端 `process:list` / `process:signal` 消息交互;当前还支持点击 `PID / USER / STATE / CPU / MEM / START / COMMAND` 表头做本地升降序排序,并为激活列显示方向标记,同时给右上角关闭按钮预留了独立安全区,避免与刷新区过近。 +**结果**: 前端状态监控形成了“更贴近参考图的默认小屏监控 + 独立进程管理页”的双层结构:默认面板不仅保持了侧栏内的高密度布局,还允许用户直接在网络和 CPU 卡片里查看近期历史波动,并在 CPU 区快速识别负载集中在哪些核心;完整进程管理继续独立存在,不挤占侧栏本体;进入进程管理详细视图后,也能更快按字段维度筛查进程。 ### 快捷指令拖拽排序 **条件**: 用户在 Workbench 的快捷指令视图中浏览分组或扁平命令列表,且当前未启用搜索过滤。 diff --git a/packages/backend/src/services/status-monitor.service.ts b/packages/backend/src/services/status-monitor.service.ts index f49cc79..cb152cc 100644 --- a/packages/backend/src/services/status-monitor.service.ts +++ b/packages/backend/src/services/status-monitor.service.ts @@ -6,6 +6,7 @@ import { settingsService } from '../settings/settings.service'; interface ServerStatus { cpuPercent?: number; cpuCores?: number; + cpuCorePercents?: number[]; memPercent?: number; memUsed?: number; // MB memTotal?: number; // MB @@ -63,6 +64,16 @@ interface DiskIoStats { }; } +interface CpuTimesSnapshot { + total: number; + idle: number; +} + +interface ParsedCpuStatSnapshot { + overall: CpuTimesSnapshot; + perCore: CpuTimesSnapshot[]; +} + const previousNetStats = new Map(); const previousDiskStats = new Map(); const monthMap: Record = { @@ -82,7 +93,7 @@ const monthMap: Record = { export class StatusMonitorService { private clientStates: Map; - private previousCpuStats = new Map(); + private previousCpuStats = new Map(); constructor(clientStates: Map) { this.clientStates = clientStates; @@ -159,30 +170,38 @@ export class StatusMonitorService { try { const procStatOutput = await this.executeSshCommand(sshClient, 'cat /proc/stat'); - const currentCpuTimes = this.parseProcStat(procStatOutput); + const currentCpuSnapshot = this.parseProcStat(procStatOutput); const now = Date.now(); - if (currentCpuTimes) { + if (currentCpuSnapshot) { + if (currentCpuSnapshot.perCore.length > 0) { + status.cpuCores = currentCpuSnapshot.perCore.length; + } + const prevCpuStats = this.previousCpuStats.get(sessionId); if (prevCpuStats && prevCpuStats.timestamp < now) { - const totalDiff = currentCpuTimes.total - prevCpuStats.total; - const idleDiff = currentCpuTimes.idle - prevCpuStats.idle; const timeDiffMs = now - prevCpuStats.timestamp; - if (totalDiff > 0 && timeDiffMs > 100) { - const usageRatio = 1.0 - (idleDiff / totalDiff); - status.cpuPercent = parseFloat((Math.max(0, Math.min(100, usageRatio * 100))).toFixed(1)); + if (timeDiffMs > 100) { + status.cpuPercent = this.calculateCpuPercent(prevCpuStats.overall, currentCpuSnapshot.overall); + status.cpuCorePercents = currentCpuSnapshot.perCore.map((coreSnapshot, index) => { + const previousCore = prevCpuStats.perCore[index]; + return previousCore ? this.calculateCpuPercent(previousCore, coreSnapshot) : 0; + }); } else { - status.cpuPercent = prevCpuStats.total > 0 ? status.cpuPercent : 0; + status.cpuPercent = 0; + status.cpuCorePercents = currentCpuSnapshot.perCore.map(() => 0); } } else { status.cpuPercent = 0; + status.cpuCorePercents = currentCpuSnapshot.perCore.map(() => 0); } - this.previousCpuStats.set(sessionId, { ...currentCpuTimes, timestamp: now }); + this.previousCpuStats.set(sessionId, { ...currentCpuSnapshot, timestamp: now }); } } catch (err) { status.cpuPercent = undefined; + status.cpuCorePercents = undefined; } try { @@ -664,25 +683,57 @@ export class StatusMonitorService { }); } - private parseProcStat(output: string): { total: number; idle: number } | null { + private calculateCpuPercent(previous: CpuTimesSnapshot, current: CpuTimesSnapshot): number { + const totalDiff = current.total - previous.total; + const idleDiff = current.idle - previous.idle; + + if (totalDiff <= 0) { + return 0; + } + + const usageRatio = 1.0 - (idleDiff / totalDiff); + return parseFloat((Math.max(0, Math.min(100, usageRatio * 100))).toFixed(1)); + } + + private parseProcStat(output: string): ParsedCpuStatSnapshot | null { try { - const cpuLine = output.split('\n').find(line => line.startsWith('cpu ')); - if (!cpuLine) { + const cpuLines = output + .split('\n') + .map(line => line.trim()) + .filter(line => /^cpu(?:\d+)?\s+/.test(line)); + + if (cpuLines.length === 0) { return null; } - const fields = cpuLine.trim().split(/\s+/).slice(1).map(Number); - if (fields.length < 4 || fields.slice(0, 4).some(isNaN)) { + let overall: CpuTimesSnapshot | null = null; + const perCore: CpuTimesSnapshot[] = []; + + for (const cpuLine of cpuLines) { + const parts = cpuLine.split(/\s+/); + const cpuLabel = parts[0]; + const fields = parts.slice(1).map(Number); + if (fields.length < 4 || fields.slice(0, 4).some(isNaN)) { + continue; + } + + const snapshot: CpuTimesSnapshot = { + idle: fields[3] + (fields[4] ?? 0), + total: fields.reduce((sum, value) => sum + (isNaN(value) ? 0 : value), 0), + }; + + if (cpuLabel === 'cpu') { + overall = snapshot; + } else { + perCore.push(snapshot); + } + } + + if (!overall) { return null; } - const idle = fields[3]; - const total = fields.reduce((sum, value) => sum + (isNaN(value) ? 0 : value), 0); - if (isNaN(total) || isNaN(idle)) { - return null; - } - - return { total, idle }; + return { overall, perCore }; } catch (e) { return null; } diff --git a/packages/frontend/src/components/ProcessManagerModal.vue b/packages/frontend/src/components/ProcessManagerModal.vue index 34e3b7c..b5ba4bc 100644 --- a/packages/frontend/src/components/ProcessManagerModal.vue +++ b/packages/frontend/src/components/ProcessManagerModal.vue @@ -7,6 +7,9 @@ import { useUiNotificationsStore } from '../stores/uiNotifications.store'; import type { ProcessListItem } from '../types/server.types'; import type { ProcessListResponsePayload, ProcessSignalResponsePayload, WebSocketMessage } from '../types/websocket.types'; +type ProcessSortKey = 'pid' | 'user' | 'state' | 'cpu' | 'mem' | 'startedAt' | 'command'; +type ProcessSortDirection = 'asc' | 'desc'; + const props = defineProps<{ isVisible: boolean; sessionId: string | null; @@ -30,6 +33,8 @@ const runningProcesses = ref(0); const sleepingProcesses = ref(0); const lastUpdatedAt = ref(null); const processError = ref(null); +const sortKey = ref(null); +const sortDirection = ref('asc'); let unregisterListResponse: (() => void) | null = null; let unregisterListError: (() => void) | null = null; @@ -54,6 +59,67 @@ const filteredProcesses = computed(() => { }); }); +const defaultSortDirections: Record = { + pid: 'asc', + user: 'asc', + state: 'asc', + cpu: 'desc', + mem: 'desc', + startedAt: 'desc', + command: 'asc', +}; + +const compareText = (left: string, right: string) => + left.localeCompare(right, undefined, { sensitivity: 'base', numeric: true }); + +const sortedProcesses = computed(() => { + const currentSortKey = sortKey.value; + if (!currentSortKey) { + return filteredProcesses.value; + } + + const directionFactor = sortDirection.value === 'asc' ? 1 : -1; + + return filteredProcesses.value + .map((item, index) => ({ item, index })) + .sort((leftEntry, rightEntry) => { + const left = leftEntry.item; + const right = rightEntry.item; + + let result = 0; + switch (currentSortKey) { + case 'pid': + result = left.pid - right.pid; + break; + case 'cpu': + result = left.cpu - right.cpu; + break; + case 'mem': + result = left.memMb - right.memMb; + break; + case 'user': + result = compareText(left.user, right.user); + break; + case 'state': + result = compareText(left.state, right.state); + break; + case 'startedAt': + result = compareText(left.startedAt, right.startedAt); + break; + case 'command': + result = compareText(left.command, right.command); + break; + } + + if (result !== 0) { + return result * directionFactor; + } + + return leftEntry.index - rightEntry.index; + }) + .map(({ item }) => item); +}); + const formatMemoryMb = (value: number): string => { if (!Number.isFinite(value)) { return t('statusMonitor.notAvailable'); @@ -71,6 +137,38 @@ const lastUpdatedText = computed(() => { return new Date(lastUpdatedAt.value).toLocaleTimeString(); }); +const toggleSort = (key: ProcessSortKey) => { + if (sortKey.value === key) { + sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc'; + return; + } + + sortKey.value = key; + sortDirection.value = defaultSortDirections[key]; +}; + +const isSortedBy = (key: ProcessSortKey) => sortKey.value === key; + +const getSortIcon = (key: ProcessSortKey) => { + if (!isSortedBy(key)) { + return 'fas fa-sort'; + } + + return sortDirection.value === 'asc' ? 'fas fa-chevron-up' : 'fas fa-chevron-down'; +}; + +const getSortLabel = (key: ProcessSortKey, label: string) => { + if (!isSortedBy(key)) { + return label; + } + + return `${label} - ${ + sortDirection.value === 'asc' + ? t('common.sortAscending', '升序') + : t('common.sortDescending', '降序') + }`; +}; + const stateTone = (state: string) => { switch (state) { case 'R': @@ -285,25 +383,102 @@ onUnmounted(() => {
-
+
{{ t('statusMonitor.processManager.empty') }}
- - - - - - - + + + + + + + - +
{{ t('statusMonitor.processManager.columns.pid') }}{{ t('statusMonitor.processManager.columns.user') }}{{ t('statusMonitor.processManager.columns.state') }}{{ t('statusMonitor.processManager.columns.cpu') }}{{ t('statusMonitor.processManager.columns.mem') }}{{ t('statusMonitor.processManager.columns.start') }}{{ t('statusMonitor.processManager.columns.command') }} + + + + + + + + + + + + + + {{ t('statusMonitor.processManager.columns.actions') }}
{{ item.pid }} {{ item.user }} @@ -374,9 +549,21 @@ onUnmounted(() => { right: 14px; border: none; background: transparent; + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 10px; color: #cbd5e1; font-size: 18px; cursor: pointer; + transition: background-color 0.2s ease, color 0.2s ease; +} + +.process-modal-close:hover { + background: rgba(255, 255, 255, 0.08); + color: #f8fbff; } .process-modal-toolbar { @@ -384,6 +571,7 @@ onUnmounted(() => { gap: 12px; align-items: center; justify-content: space-between; + padding-right: 52px; } .process-modal-search { @@ -505,6 +693,30 @@ onUnmounted(() => { text-transform: uppercase; } +.process-sort-button { + display: inline-flex; + align-items: center; + gap: 8px; + width: 100%; + border: none; + background: transparent; + padding: 0; + color: inherit; + font: inherit; + letter-spacing: inherit; + text-transform: inherit; + cursor: pointer; +} + +.process-sort-button__icon { + color: rgba(159, 176, 191, 0.56); + font-size: 11px; +} + +.process-sort-button__icon--active { + color: #f8fbff; +} + .process-table__mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } @@ -569,6 +781,7 @@ onUnmounted(() => { .process-modal-toolbar { flex-direction: column; align-items: stretch; + padding-right: 52px; } .process-modal-controls { diff --git a/packages/frontend/src/components/StatusMonitor.vue b/packages/frontend/src/components/StatusMonitor.vue index a50d4dd..d2c8821 100644 --- a/packages/frontend/src/components/StatusMonitor.vue +++ b/packages/frontend/src/components/StatusMonitor.vue @@ -58,30 +58,34 @@
-
+
{{ t('statusMonitor.cpuLabel') }} -
CPU
-
- + {{ displayCpuCores }}
-
-
-
-
- {{ displayCpuCores }} - {{ cpuUsageLane.value }} +
+ + +
+
+
+
+ {{ item.label }} + {{ item.value }} +
+
+ +
-
- -
-
-
+ +
@@ -219,15 +223,15 @@ {{ t('statusMonitor.processManager.title') }}
{{ t('statusMonitor.processManager.subtitle') }}
- - - -
-
- {{ item.label }} - {{ item.value }} +
+
+ + {{ item.label }} {{ item.value }} + +
+
@@ -274,6 +278,7 @@ import { computed, ref, watch, type CSSProperties, type PropType } from 'vue'; import { useI18n } from 'vue-i18n'; import { storeToRefs } from 'pinia'; import ProcessManagerModal from './ProcessManagerModal.vue'; +import StatusMonitorCpuHistoryChart from './StatusMonitorCpuHistoryChart.vue'; import StatusCharts from './StatusCharts.vue'; import StatusMonitorNetworkHistoryChart from './StatusMonitorNetworkHistoryChart.vue'; import { useSessionStore } from '../stores/session.store'; @@ -509,28 +514,29 @@ const systemCardMetaItems = computed(() => [ { key: 'uptime', label: t('statusMonitor.uptimeLabel'), value: uptimeDisplay.value }, ]); -const cpuUsageLane = computed(() => ({ - value: `${Math.round(displayCpuPercent.value)}%`, - percent: displayCpuPercent.value, -})); +const cpuCoreItems = computed(() => { + const rawPercents = currentServerStatus.value?.cpuCorePercents; + const fallbackCoreCount = (() => { + const currentCores = currentServerStatus.value?.cpuCores; + if (typeof currentCores !== 'number' || !Number.isFinite(currentCores) || currentCores <= 0) { + return 0; + } + return Math.round(currentCores); + })(); -const buildSparklinePath = (samples: readonly number[], width: number, height: number, usableHeight: number): string => { - if (samples.length === 0) { - return `M0 ${height - 4} L${width} ${height - 4}`; - } + const normalizedPercents = Array.isArray(rawPercents) && rawPercents.length > 0 + ? rawPercents + : Array.from({ length: fallbackCoreCount }, () => 0); - const step = samples.length > 1 ? width / (samples.length - 1) : width; - - return samples.map((value, index) => { - const x = Number((index * step).toFixed(2)); - const y = Number((height - 4 - (value / 100) * usableHeight).toFixed(2)); - return `${index === 0 ? 'M' : 'L'}${x} ${y}`; - }).join(' '); -}; - -const cpuSparklinePath = computed(() => { - const samples = currentCpuHistory.value.slice(-24).map(value => clampPercent(value ?? 0)); - return buildSparklinePath(samples, 160, 28, 18); + return normalizedPercents.map((percent, index) => { + const clampedPercent = clampPercent(percent); + return { + key: `cpu-core-${index + 1}`, + label: t('statusMonitor.cpuCoreLabel', { index: index + 1 }), + value: `${Math.round(clampedPercent)}%`, + percent: clampedPercent, + }; + }); }); const networkFlowItems = computed(() => [ @@ -864,6 +870,21 @@ const copyIpToClipboard = async (ipAddress: string | null) => { gap: 12px; } +.monitor-module__heading-actions { + display: inline-flex; + flex-wrap: wrap; + justify-content: flex-end; + align-items: flex-start; + gap: 8px; +} + +.process-summary-pills { + display: inline-flex; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; +} + .monitor-module__title { margin: 6px 0 0; color: #f8fbff; @@ -872,36 +893,6 @@ const copyIpToClipboard = async (ipAddress: string | null) => { line-height: 1.3; } -.cpu-module__hero { - display: grid; - grid-template-columns: auto minmax(96px, 1fr); - align-items: center; - gap: 12px; -} - -.cpu-module__sparkline { - height: 28px; - min-width: 0; - border-top: 1px solid rgba(148, 163, 184, 0.14); - border-bottom: 1px solid rgba(148, 163, 184, 0.1); -} - -.cpu-module__sparkline svg { - display: block; - width: 100%; - height: 100%; -} - -.cpu-module__sparkline-path { - fill: none; - stroke: #7dd3fc; - stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; - filter: drop-shadow(0 0 6px rgba(125, 211, 252, 0.28)); -} - -.usage-lane-list, .memory-stat-stack, .network-stat-stack, .disk-stat-stack, @@ -994,6 +985,16 @@ const copyIpToClipboard = async (ipAddress: string | null) => { background: linear-gradient(90deg, #7dd3fc, #2563eb); } +.cpu-core-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); + align-content: start; + gap: 8px; + max-height: 208px; + overflow-y: auto; + padding-right: 2px; +} + .module-split { display: grid; gap: 12px; @@ -1004,11 +1005,22 @@ const copyIpToClipboard = async (ipAddress: string | null) => { align-items: stretch; } -.module-split--network { - grid-template-columns: minmax(0, 0.92fr) minmax(0, 1.08fr); +.module-split--cpu { + grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); align-items: stretch; } +.module-split--network { + grid-template-columns: 1fr; + align-content: start; + gap: 8px; +} + +.monitor-module--network { + max-height: 350px; + gap: 8px; +} + .memory-ring-panel, .disk-device-card, .disk-io-card, @@ -1126,9 +1138,9 @@ const copyIpToClipboard = async (ipAddress: string | null) => { .network-table { display: grid; - gap: 8px; - height: 100%; - padding: 10px 12px; + gap: 6px; + height: auto; + padding: 8px 10px; } .network-table__header, @@ -1143,17 +1155,21 @@ const copyIpToClipboard = async (ipAddress: string | null) => { } .network-table__header { - padding-bottom: 8px; + padding-bottom: 6px; border-bottom: 1px solid rgba(148, 163, 184, 0.1); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } .network-table__columns { - padding-top: 2px; + padding-top: 0; color: #9cb0c2; font-weight: 700; } +.network-stat-stack { + gap: 6px; +} + .network-table__columns span, .network-stat span, .disk-summary-table__head span, @@ -1171,7 +1187,7 @@ const copyIpToClipboard = async (ipAddress: string | null) => { border-radius: 10px; border: 1px solid rgba(148, 163, 184, 0.06); background: rgba(255, 255, 255, 0.03); - padding: 10px 10px; + padding: 8px 10px; } .network-stat__label { @@ -1189,7 +1205,7 @@ const copyIpToClipboard = async (ipAddress: string | null) => { .network-stat__value, .network-stat__total { color: #f8fbff; - font-size: 14px; + font-size: 13px; font-weight: 800; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } @@ -1337,31 +1353,6 @@ const copyIpToClipboard = async (ipAddress: string | null) => { color: #ffffff; } -.process-summary-strip { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 8px; -} - -.process-summary-item { - padding: 10px; -} - -.process-summary-item__label { - display: block; - font-weight: 700; - letter-spacing: 0.06em; - text-transform: uppercase; -} - -.process-summary-item__value { - display: block; - margin-top: 6px; - color: #f8fbff; - font-size: 20px; - font-weight: 800; -} - .process-preview-item { display: grid; gap: 8px; @@ -1448,6 +1439,7 @@ const copyIpToClipboard = async (ipAddress: string | null) => { @container (max-width: 250px) { .module-split--memory, + .module-split--cpu, .module-split--network, .disk-compact-top { grid-template-columns: 1fr; @@ -1457,6 +1449,10 @@ const copyIpToClipboard = async (ipAddress: string | null) => { grid-template-columns: 1fr; } + .cpu-core-grid { + grid-template-columns: 1fr; + } + .network-table__header, .network-table__columns, .network-stat, @@ -1487,9 +1483,6 @@ const copyIpToClipboard = async (ipAddress: string | null) => { white-space: normal; } - .cpu-module__hero { - grid-template-columns: 1fr; - } } @container (max-width: 360px) { diff --git a/packages/frontend/src/components/StatusMonitorCpuHistoryChart.vue b/packages/frontend/src/components/StatusMonitorCpuHistoryChart.vue new file mode 100644 index 0000000..e5116b5 --- /dev/null +++ b/packages/frontend/src/components/StatusMonitorCpuHistoryChart.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/packages/frontend/src/components/StatusMonitorNetworkHistoryChart.vue b/packages/frontend/src/components/StatusMonitorNetworkHistoryChart.vue index 7f8d585..e32f48b 100644 --- a/packages/frontend/src/components/StatusMonitorNetworkHistoryChart.vue +++ b/packages/frontend/src/components/StatusMonitorNetworkHistoryChart.vue @@ -259,22 +259,22 @@ onBeforeUnmount(() => {