feat(ui): 功能区重构阶段二——洞察页 + 全局/项目层级归位(N3/N4/N5)
N3 洞察页三 tab(飞轮/健康中心/全局治理):
- FlywheelPanel(版本队列/阻塞/停转归档/Git质量/梦核)整体迁出 BlueprintModal
——跨项目内容不再寄生单项目弹窗,BlueprintModal 减重 414 行
- CONVENTIONS 规则来源 + 应用梦核建议迁至全局治理 tab(影响所有项目的操作归全局层)
- BlueprintModal 侧板只留单项目内容(模块详情/余票/可派发/项目治理)
N4 Gitea 层级归位:实例管理(全局配置)拆为 GiteaInstancesSection 挂设置页;
项目绑定+webhook 留治理面板,无实例时提示去设置页添加;共用 queryKey 缓存
N5 仓库注册表并入 项目→管理→仓库 tab(RegistryTab 复用),Sidebar 底部入口
与弹窗死壳移除;总览页更名对齐"跨项目行动项"定位
至此 ui-restructure N1-N5 全部完成:三条公理(名实一致/层级对齐/频率分层)落地。
零后端变更,typecheck 全绿。
This commit is contained in:
parent
bc07dd41bf
commit
bf697b5e28
@ -400,9 +400,9 @@ modules:
|
|||||||
- id: ui-restructure
|
- id: ui-restructure
|
||||||
name: 功能区 UI 重构
|
name: 功能区 UI 重构
|
||||||
area: frontend
|
area: frontend
|
||||||
status: in_progress
|
status: done
|
||||||
progress: 40
|
progress: 100
|
||||||
note: "阶段一完成(N1 六组导航 + N2 卡片分层);阶段二 N3/N4/N5 待用一周后启动"
|
note: "N1-N5 全部完成(2026-07-02):六组导航 + 卡片分层 + 洞察页 + Gitea/仓库归位"
|
||||||
position: [3750, 300]
|
position: [3750, 300]
|
||||||
|
|
||||||
edges:
|
edges:
|
||||||
|
|||||||
@ -96,8 +96,8 @@ Gitea push → POST /webhook/gitea/:project_id
|
|||||||
|
|
||||||
> ⚠️ push 事件部分已被 G3/G4 实现取代;剩余范围收窄为「PR/CI/review 事件 + 轮询降级」(见「PR 事件 ↔ 蓝图任务卡联动」concept 卡)。
|
> ⚠️ push 事件部分已被 G3/G4 实现取代;剩余范围收窄为「PR/CI/review 事件 + 轮询降级」(见「PR 事件 ↔ 蓝图任务卡联动」concept 卡)。
|
||||||
|
|
||||||
### 📋 Gitea Actions workflow 生成
|
### 🔵 Gitea Actions workflow 生成
|
||||||
- status: todo
|
- status: in_progress
|
||||||
- complexity: M
|
- complexity: M
|
||||||
- depends: G1
|
- depends: G1
|
||||||
- files: src-tauri/src/commands/cicd.rs, src/components/dashboard/CicdModal.tsx
|
- files: src-tauri/src/commands/cicd.rs, src/components/dashboard/CicdModal.tsx
|
||||||
|
|||||||
@ -37,22 +37,22 @@ Gitea 全局实例配置错位在单项目弹窗内。
|
|||||||
- files: src/components/dashboard/ProjectCard.tsx
|
- files: src/components/dashboard/ProjectCard.tsx
|
||||||
- acceptance: 底部操作区只显示 🗺️ 蓝图 / 🎓 导师 / 编辑 / 动态 四个明按钮 + ⋯ 更多下拉(收纳 🤖 Claude 配置、📝 个人备忘、🖥️ 关联服务器、🚀 CI/CD、🔬 环境扫描、🧰 工具管理,带文字标签);下拉点击外部自动关闭;所有原功能可达;typecheck 通过
|
- acceptance: 底部操作区只显示 🗺️ 蓝图 / 🎓 导师 / 编辑 / 动态 四个明按钮 + ⋯ 更多下拉(收纳 🤖 Claude 配置、📝 个人备忘、🖥️ 关联服务器、🚀 CI/CD、🔬 环境扫描、🧰 工具管理,带文字标签);下拉点击外部自动关闭;所有原功能可达;typecheck 通过
|
||||||
|
|
||||||
### 📋 N3. 洞察页:飞轮跨项目内容迁出 BlueprintModal + 健康中心并入
|
### ✅ N3. 洞察页:飞轮跨项目内容迁出 BlueprintModal + 健康中心并入
|
||||||
- status: todo
|
- status: done
|
||||||
- complexity: M
|
- complexity: M
|
||||||
- depends: N1
|
- depends: N1
|
||||||
- files: src/components/insight/InsightPage.tsx(新增), src/components/dashboard/BlueprintModal.tsx, src/components/health/HealthPage.tsx
|
- files: src/components/insight/InsightPage.tsx(新增), src/components/dashboard/BlueprintModal.tsx, src/components/health/HealthPage.tsx
|
||||||
- acceptance: 洞察页 3 tab:飞轮 / 健康 / 全局治理;FlywheelPanel 的跨项目内容(梦核提示词、归档候选、全项目 Git 质量表、全量导入按钮)迁至飞轮 tab;CONVENTIONS 版本管理与应用梦核建议迁至全局治理 tab;BlueprintModal 侧板只保留单项目内容;typecheck 通过
|
- acceptance: 洞察页 3 tab:飞轮 / 健康 / 全局治理;FlywheelPanel 的跨项目内容(梦核提示词、归档候选、全项目 Git 质量表、全量导入按钮)迁至飞轮 tab;CONVENTIONS 版本管理与应用梦核建议迁至全局治理 tab;BlueprintModal 侧板只保留单项目内容;typecheck 通过
|
||||||
|
|
||||||
### 📋 N4. Gitea 实例管理迁设置页(GiteaPanel 拆全局/项目级两半)
|
### ✅ N4. Gitea 实例管理迁设置页(GiteaPanel 拆全局/项目级两半)
|
||||||
- status: todo
|
- status: done
|
||||||
- complexity: S
|
- complexity: S
|
||||||
- depends: N1
|
- depends: N1
|
||||||
- files: src/components/dashboard/GiteaPanel.tsx, src/components/settings/SettingsPage.tsx
|
- files: src/components/dashboard/GiteaPanel.tsx, src/components/settings/SettingsPage.tsx
|
||||||
- acceptance: 实例管理区(添加/测试/删除)拆为独立组件挂到设置页;BlueprintModal 治理面板只保留项目级绑定与 webhook 区(无实例时提示去设置页添加);两处共用 ["gitea-instances"] queryKey;typecheck 通过
|
- acceptance: 实例管理区(添加/测试/删除)拆为独立组件挂到设置页;BlueprintModal 治理面板只保留项目级绑定与 webhook 区(无实例时提示去设置页添加);两处共用 ["gitea-instances"] queryKey;typecheck 通过
|
||||||
|
|
||||||
### 📋 N5. 仓库注册表并入项目页管理 tab + 总览收敛为行动项
|
### ✅ N5. 仓库注册表并入项目页管理 tab + 总览收敛为行动项
|
||||||
- status: todo
|
- status: done
|
||||||
- complexity: S
|
- complexity: S
|
||||||
- depends: N1
|
- depends: N1
|
||||||
- files: src/components/manage/ManagePage.tsx, src/components/layout/Sidebar.tsx, src/components/overview/OverviewPage.tsx, src/components/dashboard/RepoRegistryModal.tsx
|
- files: src/components/manage/ManagePage.tsx, src/components/layout/Sidebar.tsx, src/components/overview/OverviewPage.tsx, src/components/dashboard/RepoRegistryModal.tsx
|
||||||
@ -75,3 +75,13 @@ Gitea 全局实例配置错位在单项目弹窗内。
|
|||||||
遗留风险: ① 洞察页暂挂健康中心(N3 未做前名实略不符,导航先行占位);② ProjectCard 更多菜单向上弹出,卡片在视口顶部时可能被裁剪(overflow 场景待实际使用观察)
|
遗留风险: ① 洞察页暂挂健康中心(N3 未做前名实略不符,导航先行占位);② ProjectCard 更多菜单向上弹出,卡片在视口顶部时可能被裁剪(overflow 场景待实际使用观察)
|
||||||
auto_applied_rules: R01
|
auto_applied_rules: R01
|
||||||
rule_effectiveness: R01=未触发(无新 command,纯前端)
|
rule_effectiveness: R01=未触发(无新 command,纯前端)
|
||||||
|
|
||||||
|
【复盘】N3+N4+N5 阶段二:洞察页 + 层级归位(2026-07-02)
|
||||||
|
任务类型: 前端组件
|
||||||
|
复杂度: M + S×2
|
||||||
|
实际轮数: 1轮(预估 1 轮)
|
||||||
|
是否返工: 否
|
||||||
|
有效策略: FlywheelPanel 整体函数级迁移(组件+子组件+查询原样搬家,仅去掉 onClose),BlueprintModal 用脚本按锚点批量切除 414 行避免手工 Edit 匹配失败;全局/项目级拆分以"操作影响范围"为判据(影响所有项目→洞察/设置,影响单项目→弹窗)
|
||||||
|
遗留风险: ① RegistryTab 在 ManagePage 中的 max-h-[70vh] 容器是估计值,长列表滚动体验待实际验证;② BlueprintModal 删除跨项目内容后侧板宽度 340px 未调整,治理面板可能显空旷
|
||||||
|
auto_applied_rules: R04
|
||||||
|
rule_effectiveness: R04=有效(本 session 改动 10+ 文件但全部在预建任务卡范围内,无漂移)
|
||||||
|
|||||||
@ -8318,6 +8318,246 @@
|
|||||||
"todo": 4,
|
"todo": 4,
|
||||||
"total": 188
|
"total": 188
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"at": "2026-07-02T05:41:07Z",
|
||||||
|
"blocked_reasons": [],
|
||||||
|
"conventions_version": "1.9.0",
|
||||||
|
"delta": {
|
||||||
|
"newly_blocked": [],
|
||||||
|
"newly_done": [
|
||||||
|
"agent-infrastructure/F3. Agent 健康诊断 + 可复制修正指令",
|
||||||
|
"flywheel-intelligence/R2. CONVENTIONS 规则边界字段 + R11 失败记录规则",
|
||||||
|
"gitea-integration/G2. 仓库创建 / 迁移 / 绑定",
|
||||||
|
"gitea-integration/G3. Webhook 接收端(MCP Server 扩展)",
|
||||||
|
"gitea-integration/G4. Webhook 注册命令",
|
||||||
|
"gitea-integration/G5. Gitea 治理面板 UI",
|
||||||
|
"github-decommission/D1. 免登录改造(去掉 GitHub OAuth 登录门槛)",
|
||||||
|
"github-decommission/D2. 移除 GitHub 发布 / 导入 / PR / CI 状态入口",
|
||||||
|
"github-decommission/D3. 删除 spacesSync(单机化,跨空间 GitHub 云同步下线)",
|
||||||
|
"github-decommission/D4. git_ops 凭据去 GitHub 化",
|
||||||
|
"ui-restructure/N1. 资源/工具一级页 + Sidebar 六组重组 + Dashboard 瘦身",
|
||||||
|
"ui-restructure/N2. ProjectCard 操作分层(4 明按钮 + ⋯ 更多下拉)"
|
||||||
|
],
|
||||||
|
"unblocked": []
|
||||||
|
},
|
||||||
|
"iteration": 1,
|
||||||
|
"modules": {
|
||||||
|
"blocked": 0,
|
||||||
|
"concept": 0,
|
||||||
|
"done": 48,
|
||||||
|
"in_progress": 3,
|
||||||
|
"planned": 0,
|
||||||
|
"stalled": [
|
||||||
|
"flywheel-intelligence",
|
||||||
|
"ui-restructure"
|
||||||
|
],
|
||||||
|
"total": 53
|
||||||
|
},
|
||||||
|
"task_ids": {
|
||||||
|
"blocked": [],
|
||||||
|
"done": [
|
||||||
|
"agent-infrastructure/A. 框架层扫描 + 契约路径推断",
|
||||||
|
"agent-infrastructure/B. 三件套文件生成",
|
||||||
|
"agent-infrastructure/C. 文档腐化评分",
|
||||||
|
"agent-infrastructure/D. 栈确认 UI + 生成按钮",
|
||||||
|
"agent-infrastructure/E. 看板腐化指示器",
|
||||||
|
"agent-infrastructure/F3. Agent 健康诊断 + 可复制修正指令",
|
||||||
|
"api-keys/API 密钥 CRUD 与安全展示",
|
||||||
|
"auto-update/Capabilities 权限配置",
|
||||||
|
"auto-update/下载 & 安装",
|
||||||
|
"auto-update/更新检测",
|
||||||
|
"auto-update/自动检查更新",
|
||||||
|
"auto-update/错误提示",
|
||||||
|
"blueprint-buff/BlueprintView 跨项目切换",
|
||||||
|
"blueprint-buff/Buff 生命周期 Rust 命令",
|
||||||
|
"blueprint-buff/blueprint_buffs 数据表",
|
||||||
|
"blueprint-buff/git watcher + 任务匹配写回 + 飞轮快照",
|
||||||
|
"blueprint-buff/前端 Buff 管理 UI",
|
||||||
|
"blueprint-buff/前端实时刷新",
|
||||||
|
"blueprint-feedback/归档 command",
|
||||||
|
"blueprint-feedback/归档候选展示",
|
||||||
|
"blueprint-feedback/梦核分析入口(蓝图弹窗)",
|
||||||
|
"blueprint-feedback/梦核提示词增加停滞模块数据",
|
||||||
|
"blueprint-flywheel/get_flywheel_stats command",
|
||||||
|
"blueprint-flywheel/get_flywheel_stats 支持迭代感知",
|
||||||
|
"blueprint-flywheel/usage.json 埋点写入",
|
||||||
|
"blueprint-flywheel/梦核提示词生成 command",
|
||||||
|
"blueprint-flywheel/飞轮健康面板",
|
||||||
|
"blueprint-governance/CLAUDE.md 模板定义",
|
||||||
|
"blueprint-governance/CONVENTIONS.md 版本化",
|
||||||
|
"blueprint-governance/GovernancePanel 规则编辑器 UI",
|
||||||
|
"blueprint-governance/get_blueprint_status command",
|
||||||
|
"blueprint-governance/sync/梦核/summary 切换为 effective 内容",
|
||||||
|
"blueprint-governance/sync_blueprint_rules command",
|
||||||
|
"blueprint-governance/批量同步所有项目规则",
|
||||||
|
"blueprint-governance/用户版 CONVENTIONS 存储层初始化",
|
||||||
|
"blueprint-governance/用户版 CONVENTIONS 读写 command",
|
||||||
|
"blueprint-onboarding/generate_blueprint_prompt command",
|
||||||
|
"blueprint-onboarding/蓝图操作弹窗",
|
||||||
|
"blueprint-onboarding/项目列表蓝图状态徽章",
|
||||||
|
"blueprint-reader/合并返回完整蓝图数据",
|
||||||
|
"blueprint-reader/读取 manifest.yaml",
|
||||||
|
"blueprint-reader/读取 modules/*.md",
|
||||||
|
"blueprint-view/.blueprint/ 数据结构设计",
|
||||||
|
"blueprint-view/React Flow 节点图渲染",
|
||||||
|
"blueprint-view/blocked 状态支持",
|
||||||
|
"blueprint-view/下一步汇总面板",
|
||||||
|
"blueprint-view/任务卡复制派发",
|
||||||
|
"blueprint-view/批量执行提示词入口",
|
||||||
|
"blueprint-view/智能连线路由",
|
||||||
|
"blueprint-view/模块详情面板",
|
||||||
|
"blueprint-view/统计汇总条",
|
||||||
|
"blueprint-view/蓝图治理面板",
|
||||||
|
"blueprint-view/蓝图读取 Command",
|
||||||
|
"blueprint-view/边高亮与模块聚焦",
|
||||||
|
"blueprint-view/顶部状态行扩展",
|
||||||
|
"blueprint-view/项目卡片蓝图入口",
|
||||||
|
"cicd-workflow/Actions 运行历史",
|
||||||
|
"cicd-workflow/CI/CD 配置 CRUD",
|
||||||
|
"cicd-workflow/Tauri OSS 分发模板",
|
||||||
|
"cicd-workflow/Web SSH 部署模板",
|
||||||
|
"cicd-workflow/Workflow 文件生成",
|
||||||
|
"claude-autonomy-config/Beast 全局状态读取与展示",
|
||||||
|
"cloud-db-instances/云数据库实例信息管理",
|
||||||
|
"cloud-docker-apps/Docker 应用列表与状态管理",
|
||||||
|
"cloud-products/DB 迁移:cloud_products 表",
|
||||||
|
"cloud-products/Dashboard 接入:云产品标签页",
|
||||||
|
"cloud-products/Rust CRUD:cloud_products.rs",
|
||||||
|
"cloud-products/前端 Panel:CloudProductsPanel.tsx",
|
||||||
|
"cloud-products/接线:mod.rs + lib.rs + commands.ts",
|
||||||
|
"deploy-info/项目部署信息记录面板",
|
||||||
|
"devtools-scan/WSL 工具扫描",
|
||||||
|
"devtools-scan/Windows 工具扫描",
|
||||||
|
"devtools-scan/工具信息持久化",
|
||||||
|
"flywheel-intelligence/F1. Git 健康度后端——全量历史导入 + 跨项目聚合",
|
||||||
|
"flywheel-intelligence/F2. 飞轮面板——Git 健康度区块 + 全量导入按钮",
|
||||||
|
"flywheel-intelligence/MCP Server 支持 SSE 传输",
|
||||||
|
"flywheel-intelligence/R2. CONVENTIONS 规则边界字段 + R11 失败记录规则",
|
||||||
|
"flywheel-intelligence/复盘笔记覆盖率可见性",
|
||||||
|
"flywheel-intelligence/独立项目自动创建产品组",
|
||||||
|
"flywheel-intelligence/补测试:飞轮存量函数安全网",
|
||||||
|
"flywheel-intelligence/阶段一:复盘笔记接入梦核飞轮",
|
||||||
|
"flywheel-intelligence/阶段二:usage.json 扩展采集维度",
|
||||||
|
"git-ops/上游状态检测",
|
||||||
|
"git-ops/分支管理",
|
||||||
|
"git-ops/基础操作",
|
||||||
|
"gitea-integration/G1. DB schema + Gitea 实例 CRUD",
|
||||||
|
"gitea-integration/G2. 仓库创建 / 迁移 / 绑定",
|
||||||
|
"gitea-integration/G3. Webhook 接收端(MCP Server 扩展)",
|
||||||
|
"gitea-integration/G4. Webhook 注册命令",
|
||||||
|
"gitea-integration/G5. Gitea 治理面板 UI",
|
||||||
|
"github-auth/OAuth Device Flow",
|
||||||
|
"github-auth/Token 管理",
|
||||||
|
"github-decommission/D1. 免登录改造(去掉 GitHub OAuth 登录门槛)",
|
||||||
|
"github-decommission/D2. 移除 GitHub 发布 / 导入 / PR / CI 状态入口",
|
||||||
|
"github-decommission/D3. 删除 spacesSync(单机化,跨空间 GitHub 云同步下线)",
|
||||||
|
"github-decommission/D4. git_ops 凭据去 GitHub 化",
|
||||||
|
"github-publish/GitHub 仓库创建与推送",
|
||||||
|
"health-center/T1 · Backend 健康检查聚合命令",
|
||||||
|
"health-center/T2 · 质量指标聚合",
|
||||||
|
"health-center/T3 · 前端健康中心页面",
|
||||||
|
"health-center/T4 · 扩展 MCP get_diagnostics",
|
||||||
|
"host-apps/宿主机应用清单与快启",
|
||||||
|
"mcp-config-inject/加入产品组时注入配置",
|
||||||
|
"mcp-config-inject/端口变更时重新注入所有配置",
|
||||||
|
"mcp-config-inject/退出产品组时清除配置",
|
||||||
|
"mcp-server/MCP 端口可配置(Rust 侧)",
|
||||||
|
"mcp-server/启动时绑定 HTTP 端口",
|
||||||
|
"mcp-server/实现 MCP JSON-RPC 基础协议",
|
||||||
|
"mcp-server/实现产品组上下文工具",
|
||||||
|
"onboarding-pack/A 模板分发引擎",
|
||||||
|
"onboarding-pack/B apply_onboarding_pack 编排 + 前端入口",
|
||||||
|
"onboarding-pack/C1 commit_metrics 表 + Conventional 解析器",
|
||||||
|
"onboarding-pack/C2 挂接 buff watcher + 飞轮快照",
|
||||||
|
"onboarding-pack/D 数据可信度提示",
|
||||||
|
"product-group-mcp/产品组详情显示 MCP 注入状态",
|
||||||
|
"product-group-mcp/设置页添加 MCP 端口配置",
|
||||||
|
"project-dashboard/分组画布",
|
||||||
|
"project-dashboard/项目卡片列表",
|
||||||
|
"project-dashboard/项目发现面板",
|
||||||
|
"project-dashboard/项目快捷操作",
|
||||||
|
"project-env-scan/环境信息持久化",
|
||||||
|
"project-env-scan/项目环境检测",
|
||||||
|
"project-mentor/T1 · 后端聚合 command(MentorContext)",
|
||||||
|
"project-mentor/T10 · 启动健康扫描 + 前端 Alert Banner",
|
||||||
|
"project-mentor/T11 · 项目历史快照",
|
||||||
|
"project-mentor/T2 · MCP 项目导师查询接口",
|
||||||
|
"project-mentor/T3 · 前端导师面板",
|
||||||
|
"project-mentor/T4 · 炼境自指接入",
|
||||||
|
"project-mentor/T5 · project_notes 数据层 + 后端命令",
|
||||||
|
"project-mentor/T6 · MCP append_project_note 工具",
|
||||||
|
"project-mentor/T7 · MentorPanel 展示 AI 笔记",
|
||||||
|
"project-mentor/T8 · 后端全组巡检命令",
|
||||||
|
"project-mentor/T9 · 前端全组巡检按钮",
|
||||||
|
"project-mgmt/子文件夹扫描",
|
||||||
|
"project-mgmt/批量导入项目",
|
||||||
|
"project-mgmt/项目 CRUD",
|
||||||
|
"project-mgmt/项目档案(Profile)",
|
||||||
|
"project-templates/P1+P3 模板扩充(React 生态全覆盖)",
|
||||||
|
"project-templates/从模板创建项目",
|
||||||
|
"project-templates/内置模板种植",
|
||||||
|
"project-templates/模板 CRUD",
|
||||||
|
"project-tools/工具 CRUD",
|
||||||
|
"project-tools/工具启动",
|
||||||
|
"repo-registry/仓库导入",
|
||||||
|
"repo-registry/仓库挂载",
|
||||||
|
"repo-registry/仓库注册表管理",
|
||||||
|
"runtime-diagnostics/T1 · SQLite runtime_logs 表",
|
||||||
|
"runtime-diagnostics/T2 · 替换 eprintln! 为结构化日志",
|
||||||
|
"runtime-diagnostics/T3 · MCP Server 新增 get_diagnostics 工具",
|
||||||
|
"runtime-diagnostics/T4 · 注入失败结果返回前端",
|
||||||
|
"runtime-diagnostics/T5 · 设置页 MCP 真实健康检查",
|
||||||
|
"server-registry/服务器 CRUD 与项目关联",
|
||||||
|
"server-software/服务器软件扫描与展示",
|
||||||
|
"service-config/服务配置 CRUD 与环境分组",
|
||||||
|
"settings-page/WSL 发行版选择",
|
||||||
|
"settings-page/关于 & 更新",
|
||||||
|
"settings-page/终端配置",
|
||||||
|
"settings-page/编辑器检测",
|
||||||
|
"shell-launch/打开终端",
|
||||||
|
"shell-launch/打开编辑器",
|
||||||
|
"source-library/源码库分类管理与项目详情",
|
||||||
|
"sqlite-db/数据库迁移",
|
||||||
|
"sqlite-db/连接池初始化",
|
||||||
|
"ssh-config-sync/SSH Config 写入与同步",
|
||||||
|
"tags-groups/分组管理",
|
||||||
|
"tags-groups/标签管理",
|
||||||
|
"tray-service/关闭窗口最小化到托盘",
|
||||||
|
"tray-service/系统托盘图标与菜单",
|
||||||
|
"tray-service/集成 tauri-plugin-single-instance",
|
||||||
|
"ui-restructure/N1. 资源/工具一级页 + Sidebar 六组重组 + Dashboard 瘦身",
|
||||||
|
"ui-restructure/N2. ProjectCard 操作分层(4 明按钮 + ⋯ 更多下拉)",
|
||||||
|
"user-project-todos/前端:备忘清单 UI 组件",
|
||||||
|
"user-project-todos/数据层:user_project_todos 表 + Rust commands",
|
||||||
|
"v2rayn-import/当前节点读取",
|
||||||
|
"v2rayn-import/数据库路径检测",
|
||||||
|
"v2rayn-import/节点列表扫描",
|
||||||
|
"vscode-remote/VS Code Remote SSH 一键连接",
|
||||||
|
"workspace-mgmt/空间 CRUD",
|
||||||
|
"workspace-mgmt/空间级设置",
|
||||||
|
"workspace-mgmt/部署命令管理",
|
||||||
|
"wsl-proxy/代理一致性测试",
|
||||||
|
"wsl-proxy/代理同步",
|
||||||
|
"wsl-proxy/代理清除",
|
||||||
|
"wsl-proxy/代理状态检测"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"avg_rounds_M": null,
|
||||||
|
"blocked": 0,
|
||||||
|
"complexity_dist": {
|
||||||
|
"L": 6,
|
||||||
|
"M": 80,
|
||||||
|
"S": 53
|
||||||
|
},
|
||||||
|
"dispatched": 0,
|
||||||
|
"done": 184,
|
||||||
|
"in_progress": 0,
|
||||||
|
"rework_count": 23,
|
||||||
|
"todo": 7,
|
||||||
|
"total": 201
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -5,7 +5,7 @@ import { ProjectModal } from "./components/dashboard/ProjectModal";
|
|||||||
import { ResourcesPage } from "./components/resources/ResourcesPage";
|
import { ResourcesPage } from "./components/resources/ResourcesPage";
|
||||||
import { ToolsPage } from "./components/tools/ToolsPage";
|
import { ToolsPage } from "./components/tools/ToolsPage";
|
||||||
import { SettingsPage } from "./components/settings/SettingsPage";
|
import { SettingsPage } from "./components/settings/SettingsPage";
|
||||||
import { HealthPage } from "./components/health/HealthPage";
|
import { InsightPage } from "./components/insight/InsightPage";
|
||||||
import { Sidebar, type Page } from "./components/layout/Sidebar";
|
import { Sidebar, type Page } from "./components/layout/Sidebar";
|
||||||
import { Toast } from "./components/ui/Toast";
|
import { Toast } from "./components/ui/Toast";
|
||||||
import { getStartupAlerts, type StartupAlert } from "./lib/commands";
|
import { getStartupAlerts, type StartupAlert } from "./lib/commands";
|
||||||
@ -68,8 +68,7 @@ export default function App() {
|
|||||||
{page === "overview" && <OverviewPage />}
|
{page === "overview" && <OverviewPage />}
|
||||||
{page === "dashboard" && <Dashboard />}
|
{page === "dashboard" && <Dashboard />}
|
||||||
{page === "resources" && <ResourcesPage />}
|
{page === "resources" && <ResourcesPage />}
|
||||||
{/* 洞察页阶段一暂挂健康中心,N3 扩充为 飞轮/健康/全局治理 三 tab */}
|
{page === "insight" && <InsightPage />}
|
||||||
{page === "insight" && <HealthPage />}
|
|
||||||
{page === "tools" && <ToolsPage />}
|
{page === "tools" && <ToolsPage />}
|
||||||
{page === "settings" && <SettingsPage />}
|
{page === "settings" && <SettingsPage />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -20,33 +20,19 @@ import {
|
|||||||
getBlueprintStatus,
|
getBlueprintStatus,
|
||||||
syncBlueprintRules,
|
syncBlueprintRules,
|
||||||
generateBlueprintPrompt,
|
generateBlueprintPrompt,
|
||||||
getProjects,
|
|
||||||
getFlywheelStats,
|
|
||||||
generateMuhePrompt,
|
|
||||||
archiveProjectDocs,
|
|
||||||
applyOnboardingPack,
|
applyOnboardingPack,
|
||||||
getCommitStats,
|
getCommitStats,
|
||||||
removeBlueprintBuff,
|
removeBlueprintBuff,
|
||||||
getBuffStatus,
|
getBuffStatus,
|
||||||
getUserConventionsStatus,
|
|
||||||
saveUserConventions,
|
|
||||||
resetConventionsToBuiltin,
|
|
||||||
type BlueprintData,
|
type BlueprintData,
|
||||||
type BlueprintModule,
|
type BlueprintModule,
|
||||||
type BlueprintTask,
|
type BlueprintTask,
|
||||||
type BlueprintArea,
|
type BlueprintArea,
|
||||||
type BlueprintStatus,
|
type BlueprintStatus,
|
||||||
type SyncResult,
|
type SyncResult,
|
||||||
type Project,
|
|
||||||
type FlywheelStats,
|
|
||||||
type StalledProject,
|
|
||||||
type BuffStatus,
|
type BuffStatus,
|
||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
type CommitStats,
|
type CommitStats,
|
||||||
type UserConventionsStatus,
|
|
||||||
type CommitHealthWithName,
|
|
||||||
ingestFullGitHistory,
|
|
||||||
getAllCommitHealth,
|
|
||||||
} from "../../lib/commands";
|
} from "../../lib/commands";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -253,7 +239,7 @@ function pickHandles(
|
|||||||
|
|
||||||
// ── 主组件 ───────────────────────────────────────────────────────────────────
|
// ── 主组件 ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type SidePanel = { type: "module"; module: BlueprintModule } | { type: "next" } | { type: "remaining" } | { type: "governance" } | { type: "flywheel" } | null;
|
type SidePanel = { type: "module"; module: BlueprintModule } | { type: "next" } | { type: "remaining" } | { type: "governance" } | null;
|
||||||
|
|
||||||
export function BlueprintModal({ projectName, projectPath, projectId, onClose }: Props) {
|
export function BlueprintModal({ projectName, projectPath, projectId, onClose }: Props) {
|
||||||
const [sidePanel, setSidePanel] = useState<SidePanel>(null);
|
const [sidePanel, setSidePanel] = useState<SidePanel>(null);
|
||||||
@ -429,16 +415,6 @@ export function BlueprintModal({ projectName, projectPath, projectId, onClose }:
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
|
||||||
onClick={() => setSidePanel((p) => p?.type === "flywheel" ? null : { type: "flywheel" })}
|
|
||||||
className={`text-xs px-2.5 py-1 rounded-lg transition-colors ${
|
|
||||||
sidePanel?.type === "flywheel"
|
|
||||||
? "text-white bg-emerald-600"
|
|
||||||
: "text-emerald-600 bg-emerald-50 hover:bg-emerald-100"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
♻️ 飞轮
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setSidePanel((p) => p?.type === "governance" ? null : { type: "governance" })}
|
onClick={() => setSidePanel((p) => p?.type === "governance" ? null : { type: "governance" })}
|
||||||
className={`text-xs px-2.5 py-1 rounded-lg transition-colors ${
|
className={`text-xs px-2.5 py-1 rounded-lg transition-colors ${
|
||||||
@ -514,17 +490,6 @@ export function BlueprintModal({ projectName, projectPath, projectId, onClose }:
|
|||||||
{bpStatus.status === "synced" ? "🟢 规则" : "🟡 规则"}
|
{bpStatus.status === "synced" ? "🟢 规则" : "🟡 规则"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
|
||||||
onClick={() => setSidePanel((p) => p?.type === "flywheel" ? null : { type: "flywheel" })}
|
|
||||||
className={`text-[10px] px-1.5 py-0.5 rounded transition-colors shrink-0 ${
|
|
||||||
sidePanel?.type === "flywheel"
|
|
||||||
? "text-white bg-emerald-600"
|
|
||||||
: "text-emerald-600 bg-emerald-50 hover:bg-emerald-100"
|
|
||||||
}`}
|
|
||||||
title="梦核·记忆巩固 / AI文档归档"
|
|
||||||
>
|
|
||||||
♻️ AI文档
|
|
||||||
</button>
|
|
||||||
{/* 总进度条(固定在右侧) */}
|
{/* 总进度条(固定在右侧) */}
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
{pinnedModuleId && (
|
{pinnedModuleId && (
|
||||||
@ -639,10 +604,7 @@ export function BlueprintModal({ projectName, projectPath, projectId, onClose }:
|
|||||||
projectName={projectName}
|
projectName={projectName}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
onClose={() => setSidePanel(null)}
|
onClose={() => setSidePanel(null)}
|
||||||
onOpenFlywheel={() => setSidePanel({ type: "flywheel" })}
|
|
||||||
/>
|
/>
|
||||||
) : sidePanel.type === "flywheel" ? (
|
|
||||||
<FlywheelPanel onClose={() => setSidePanel(null)} />
|
|
||||||
) : (
|
) : (
|
||||||
<NextActionsPanel
|
<NextActionsPanel
|
||||||
modules={blueprint?.manifest.modules ?? []}
|
modules={blueprint?.manifest.modules ?? []}
|
||||||
@ -1050,13 +1012,11 @@ function GovernancePanel({
|
|||||||
projectName,
|
projectName,
|
||||||
projectId,
|
projectId,
|
||||||
onClose,
|
onClose,
|
||||||
onOpenFlywheel,
|
|
||||||
}: {
|
}: {
|
||||||
projectPath: string;
|
projectPath: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onOpenFlywheel?: () => void;
|
|
||||||
}) {
|
}) {
|
||||||
const { data: bpStatus, isPending: statusLoading, refetch: refetchStatus } = useQuery<BlueprintStatus>({
|
const { data: bpStatus, isPending: statusLoading, refetch: refetchStatus } = useQuery<BlueprintStatus>({
|
||||||
queryKey: ["bp-status-governance", projectPath],
|
queryKey: ["bp-status-governance", projectPath],
|
||||||
@ -1092,17 +1052,6 @@ function GovernancePanel({
|
|||||||
const [promptError, setPromptError] = useState<string | null>(null);
|
const [promptError, setPromptError] = useState<string | null>(null);
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
|
||||||
// 用户版 CONVENTIONS 编辑器
|
|
||||||
const { data: userConventions, refetch: refetchUserConventions } = useQuery<UserConventionsStatus>({
|
|
||||||
queryKey: ["user-conventions-status"],
|
|
||||||
queryFn: () => getUserConventionsStatus(),
|
|
||||||
});
|
|
||||||
const [conventionsInput, setConventionsInput] = useState("");
|
|
||||||
const [conventionsEditorOpen, setConventionsEditorOpen] = useState(false);
|
|
||||||
const [conventionsSaving, setConventionsSaving] = useState(false);
|
|
||||||
const [conventionsSaveResult, setConventionsSaveResult] = useState<string | null>(null);
|
|
||||||
const [conventionsSaveError, setConventionsSaveError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleApplyBuff = async () => {
|
const handleApplyBuff = async () => {
|
||||||
if (!projectId) return;
|
if (!projectId) return;
|
||||||
setBuffLoading(true);
|
setBuffLoading(true);
|
||||||
@ -1189,41 +1138,6 @@ function GovernancePanel({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveConventions = async () => {
|
|
||||||
if (!conventionsInput.trim()) return;
|
|
||||||
setConventionsSaving(true);
|
|
||||||
setConventionsSaveError(null);
|
|
||||||
setConventionsSaveResult(null);
|
|
||||||
try {
|
|
||||||
const res = await saveUserConventions(conventionsInput);
|
|
||||||
setConventionsSaveResult(`已应用 v${res.version},同步了 ${res.synced_count} 个项目`);
|
|
||||||
setConventionsInput("");
|
|
||||||
setConventionsEditorOpen(false);
|
|
||||||
refetchUserConventions();
|
|
||||||
refetchStatus();
|
|
||||||
} catch (e) {
|
|
||||||
setConventionsSaveError(String(e));
|
|
||||||
} finally {
|
|
||||||
setConventionsSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResetConventions = async () => {
|
|
||||||
setConventionsSaving(true);
|
|
||||||
setConventionsSaveError(null);
|
|
||||||
setConventionsSaveResult(null);
|
|
||||||
try {
|
|
||||||
const res = await resetConventionsToBuiltin();
|
|
||||||
setConventionsSaveResult(`已重置为内置基线 v${res.version},同步了 ${res.synced_count} 个项目`);
|
|
||||||
refetchUserConventions();
|
|
||||||
refetchStatus();
|
|
||||||
} catch (e) {
|
|
||||||
setConventionsSaveError(String(e));
|
|
||||||
} finally {
|
|
||||||
setConventionsSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const rulesOutdated = bpStatus?.status === "rules_outdated";
|
const rulesOutdated = bpStatus?.status === "rules_outdated";
|
||||||
const currentVersion = bpStatus?.conventions_version ?? "—";
|
const currentVersion = bpStatus?.conventions_version ?? "—";
|
||||||
const masterVersion = bpStatus?.master_version ?? "—";
|
const masterVersion = bpStatus?.master_version ?? "—";
|
||||||
@ -1265,88 +1179,6 @@ function GovernancePanel({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 规则来源 badge */}
|
|
||||||
{userConventions && (
|
|
||||||
<div className="flex items-center justify-between bg-gray-50 rounded-lg px-3 py-2">
|
|
||||||
<span className="text-[11px] text-gray-500">规则来源</span>
|
|
||||||
{userConventions.source === "user" ? (
|
|
||||||
<span className="text-[11px] font-mono text-purple-600 bg-purple-50 px-2 py-0.5 rounded">
|
|
||||||
用户自定义 v{userConventions.version}
|
|
||||||
{userConventions.outdated_from_builtin && (
|
|
||||||
<span className="ml-1 text-yellow-600">⚠️ 低于内置</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-[11px] font-mono text-gray-500 bg-white border border-gray-200 px-2 py-0.5 rounded">
|
|
||||||
内置基线 v{userConventions.builtin_version}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 应用梦核建议 */}
|
|
||||||
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">应用梦核建议</p>
|
|
||||||
<button
|
|
||||||
onClick={() => { setConventionsEditorOpen(v => !v); setConventionsSaveError(null); }}
|
|
||||||
className="text-[11px] text-blue-500 hover:text-blue-600"
|
|
||||||
>
|
|
||||||
{conventionsEditorOpen ? "收起" : "展开"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{conventionsEditorOpen && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<p className="text-[11px] text-gray-500 leading-relaxed">
|
|
||||||
将梦核 Opus 输出的新 CONVENTIONS 内容粘贴到下方,确认后将写入并同步所有项目。
|
|
||||||
</p>
|
|
||||||
<textarea
|
|
||||||
value={conventionsInput}
|
|
||||||
onChange={e => setConventionsInput(e.target.value)}
|
|
||||||
placeholder="粘贴完整的 CONVENTIONS.md 内容(须包含 frontmatter version 字段)…"
|
|
||||||
rows={6}
|
|
||||||
className="w-full text-[11px] font-mono border border-gray-200 rounded p-2 resize-none focus:outline-none focus:border-blue-400"
|
|
||||||
/>
|
|
||||||
{conventionsSaveError && (
|
|
||||||
<p className="text-[11px] text-red-600 font-mono break-all">{conventionsSaveError}</p>
|
|
||||||
)}
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={handleSaveConventions}
|
|
||||||
disabled={conventionsSaving || !conventionsInput.trim()}
|
|
||||||
className="flex-1 py-1.5 rounded-lg bg-blue-500 text-white text-xs hover:bg-blue-600 disabled:opacity-50 transition-colors"
|
|
||||||
>
|
|
||||||
{conventionsSaving ? "写入中…" : "确认应用"}
|
|
||||||
</button>
|
|
||||||
{userConventions?.source === "user" && (
|
|
||||||
<button
|
|
||||||
onClick={handleResetConventions}
|
|
||||||
disabled={conventionsSaving}
|
|
||||||
className="px-3 py-1.5 rounded-lg border border-gray-200 text-gray-600 text-xs hover:bg-gray-50 disabled:opacity-50 transition-colors"
|
|
||||||
>
|
|
||||||
重置为基线
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!conventionsEditorOpen && userConventions?.source === "user" && (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-[11px] text-gray-500">当前使用用户自定义规则</p>
|
|
||||||
<button
|
|
||||||
onClick={handleResetConventions}
|
|
||||||
disabled={conventionsSaving}
|
|
||||||
className="text-[11px] text-gray-400 hover:text-red-500 disabled:opacity-50 transition-colors"
|
|
||||||
>
|
|
||||||
重置为基线
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{conventionsSaveResult && (
|
|
||||||
<p className="text-[11px] text-green-600 bg-green-50 rounded px-2 py-1">✓ {conventionsSaveResult}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 同步规则区域 */}
|
{/* 同步规则区域 */}
|
||||||
{rulesOutdated && !syncResult && (
|
{rulesOutdated && !syncResult && (
|
||||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 space-y-2">
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 space-y-2">
|
||||||
@ -1437,21 +1269,6 @@ function GovernancePanel({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 梦核分析入口 */}
|
{/* 梦核分析入口 */}
|
||||||
{onOpenFlywheel && (
|
|
||||||
<div className="bg-emerald-50 rounded-lg border border-emerald-100 p-3 space-y-2">
|
|
||||||
<p className="text-[10px] font-semibold text-emerald-600 uppercase tracking-wider">梦核 · 记忆巩固</p>
|
|
||||||
<p className="text-[11px] text-emerald-700 leading-relaxed">
|
|
||||||
基于跨项目飞轮数据,生成梦核提示词,由 Claude Opus 完成固化 / 遗忘判断。
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={onOpenFlywheel}
|
|
||||||
className="w-full py-1.5 rounded-lg bg-emerald-600 text-white text-xs hover:bg-emerald-700 transition-colors"
|
|
||||||
>
|
|
||||||
♻️ 打开飞轮面板
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ── 蓝图 Buff ─────────────────────────────────────── */}
|
{/* ── 蓝图 Buff ─────────────────────────────────────── */}
|
||||||
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
|
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@ -1636,417 +1453,3 @@ function RemainingTasksPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 飞轮健康面板 ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function StalledProjectRow({ p, projectsMap, onArchive }: {
|
|
||||||
p: StalledProject;
|
|
||||||
projectsMap: Record<string, string>; // name → path
|
|
||||||
onArchive?: (path: string, name: string) => void;
|
|
||||||
}) {
|
|
||||||
const path = projectsMap[p.name];
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<div className="min-w-0">
|
|
||||||
<p className="text-[11px] text-amber-700 truncate">· {p.name}</p>
|
|
||||||
<p className="text-[10px] text-amber-400">最后活跃: {p.last_active}</p>
|
|
||||||
</div>
|
|
||||||
{onArchive && path && (
|
|
||||||
<button
|
|
||||||
onClick={() => onArchive(path, p.name)}
|
|
||||||
className="text-[10px] text-amber-600 bg-amber-100 hover:bg-amber-200 px-1.5 py-0.5 rounded shrink-0 transition-colors"
|
|
||||||
>
|
|
||||||
归档
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FlywheelPanel({ onClose }: { onClose: () => void }) {
|
|
||||||
const { data: projects } = useQuery<Project[]>({
|
|
||||||
queryKey: ["projects-for-flywheel"],
|
|
||||||
queryFn: () => getProjects(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { projectPaths, projectsMap, projectsWithPath } = useMemo(() => {
|
|
||||||
const paths = (projects ?? [])
|
|
||||||
.map((p) => p.win_path || p.wsl_path || "")
|
|
||||||
.filter(Boolean);
|
|
||||||
const map: Record<string, string> = {};
|
|
||||||
const withPath: Array<{ id: string; name: string; path: string }> = [];
|
|
||||||
for (const p of (projects ?? [])) {
|
|
||||||
const path = p.win_path || p.wsl_path || "";
|
|
||||||
if (!path) continue;
|
|
||||||
withPath.push({ id: p.id, name: p.name, path });
|
|
||||||
map[p.name] = path;
|
|
||||||
const dirName = path.replace(/\\/g, "/").split("/").filter(Boolean).pop() ?? "";
|
|
||||||
if (dirName && !map[dirName]) map[dirName] = path;
|
|
||||||
}
|
|
||||||
return { projectPaths: paths, projectsMap: map, projectsWithPath: withPath };
|
|
||||||
}, [projects]);
|
|
||||||
|
|
||||||
const { data: commitHealth, refetch: refetchHealth } = useQuery<CommitHealthWithName[]>({
|
|
||||||
queryKey: ["commit-health-all"],
|
|
||||||
queryFn: () => getAllCommitHealth(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const [ingesting, setIngesting] = useState(false);
|
|
||||||
const [ingestSummary, setIngestSummary] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const { data: stats, isLoading } = useQuery<FlywheelStats>({
|
|
||||||
queryKey: ["flywheel-stats", projectPaths],
|
|
||||||
queryFn: () => getFlywheelStats(projectPaths),
|
|
||||||
enabled: projectPaths.length > 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [muhePrompt, setMuhePrompt] = useState("");
|
|
||||||
const [generating, setGenerating] = useState(false);
|
|
||||||
const [copied, setCopied] = useState(false);
|
|
||||||
const [archiveMsg, setArchiveMsg] = useState<string | null>(null);
|
|
||||||
const [archiving, setArchiving] = useState<string | null>(null);
|
|
||||||
const [pendingArchive, setPendingArchive] = useState<{ path: string; name: string } | null>(null);
|
|
||||||
|
|
||||||
const handleIngestAll = async () => {
|
|
||||||
setIngesting(true);
|
|
||||||
setIngestSummary(null);
|
|
||||||
let totalImported = 0;
|
|
||||||
let totalSkipped = 0;
|
|
||||||
for (const { id, path } of projectsWithPath) {
|
|
||||||
try {
|
|
||||||
const r = await ingestFullGitHistory(id, path);
|
|
||||||
totalImported += r.imported;
|
|
||||||
totalSkipped += r.skipped;
|
|
||||||
} catch {
|
|
||||||
// 单个项目失败不中断其他项目
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await refetchHealth();
|
|
||||||
setIngestSummary(`新导入 ${totalImported} 条,跳过 ${totalSkipped} 条`);
|
|
||||||
setIngesting(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGenerateMuhe = async () => {
|
|
||||||
if (!stats) return;
|
|
||||||
setGenerating(true);
|
|
||||||
try {
|
|
||||||
const result = await generateMuhePrompt(stats);
|
|
||||||
setMuhePrompt(result);
|
|
||||||
} finally {
|
|
||||||
setGenerating(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCopy = () => {
|
|
||||||
navigator.clipboard.writeText(muhePrompt).then(() => {
|
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => setCopied(false), 2000);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleArchiveConfirm = async () => {
|
|
||||||
if (!pendingArchive) return;
|
|
||||||
const { path, name } = pendingArchive;
|
|
||||||
setPendingArchive(null);
|
|
||||||
setArchiving(name);
|
|
||||||
setArchiveMsg(null);
|
|
||||||
try {
|
|
||||||
const result = await archiveProjectDocs(path);
|
|
||||||
setArchiveMsg(`✓ 已归档 ${result.archived_files.length} 个文件 → ${result.archive_dir}`);
|
|
||||||
} catch (e) {
|
|
||||||
setArchiveMsg(`归档失败: ${String(e)}`);
|
|
||||||
} finally {
|
|
||||||
setArchiving(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fmtPct = (v: number) => `${(v * 100).toFixed(1)}%`;
|
|
||||||
|
|
||||||
// Fix 2:去重——90天休眠的项目不在60天停转里重复展示
|
|
||||||
const stalledOnlyProjects = (stats?.stalled_projects ?? []).filter(
|
|
||||||
(p) => !(stats?.ai_doc_stale_projects ?? []).some((q) => q.name === p.name)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-4 space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-semibold text-gray-800">飞轮健康</h3>
|
|
||||||
{stats && (
|
|
||||||
<p className="text-[10px] text-gray-400 mt-0.5">
|
|
||||||
{stats.projects_with_data}/{stats.total_projects_scanned} 个项目有数据
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<button onClick={onClose} className="text-gray-300 hover:text-gray-500 text-lg">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isLoading ? (
|
|
||||||
<p className="text-xs text-gray-400 text-center py-6">聚合中…</p>
|
|
||||||
) : !stats || stats.projects_with_data === 0 ? (
|
|
||||||
<div className="text-center py-6 space-y-2">
|
|
||||||
<p className="text-xs text-gray-400">暂无飞轮数据</p>
|
|
||||||
<p className="text-[11px] text-gray-300">打开各项目蓝图后数据将自动积累</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{/* CONVENTIONS 版本队列对比 */}
|
|
||||||
{stats.cohorts.length > 0 && (
|
|
||||||
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
|
|
||||||
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">版本队列对比</p>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{stats.cohorts.map((c) => (
|
|
||||||
<div key={c.conventions_version} className="space-y-1.5">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-xs font-mono text-gray-600">v{c.conventions_version}</span>
|
|
||||||
<span className="text-[10px] text-gray-400">{c.project_count} 个项目</span>
|
|
||||||
</div>
|
|
||||||
{/* Fix 4:标签改为"任务完成率",字段改为 completion_rate */}
|
|
||||||
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5">
|
|
||||||
<MetricRow label="可派发率" value={fmtPct(c.dispatchable_rate)} good={c.dispatchable_rate > 0.3} />
|
|
||||||
<MetricRow label="任务完成率" value={fmtPct(c.completion_rate)} good={c.completion_rate > 0.5} />
|
|
||||||
<MetricRow label="飞轮持续率" value={fmtPct(c.continuity_rate)} good={c.continuity_rate > 0.6} />
|
|
||||||
<MetricRow label="阻塞密度" value={fmtPct(c.blocked_density)} good={c.blocked_density < 0.1} invert />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* TOP blocked reasons */}
|
|
||||||
{stats.top_blocked_reasons.length > 0 && (
|
|
||||||
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
|
|
||||||
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">TOP 阻塞原因</p>
|
|
||||||
<ul className="space-y-1">
|
|
||||||
{stats.top_blocked_reasons.map((r, i) => (
|
|
||||||
<li key={i} className="flex items-start gap-2">
|
|
||||||
<span className="text-[10px] text-gray-400 shrink-0 mt-0.5">#{i + 1}</span>
|
|
||||||
<span className="text-[11px] text-red-600 flex-1">{r.reason}</span>
|
|
||||||
<span className="text-[10px] text-gray-400 shrink-0">×{r.count}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Fix 2:仅展示"停转但未到90天"的项目 */}
|
|
||||||
{stalledOnlyProjects.length > 0 && (
|
|
||||||
<div className="bg-amber-50 rounded-lg border border-amber-100 p-3 space-y-1.5">
|
|
||||||
<p className="text-[10px] font-semibold text-amber-500 uppercase tracking-wider">
|
|
||||||
飞轮停转(60–90天无快照)
|
|
||||||
</p>
|
|
||||||
{stalledOnlyProjects.map((p, i) => (
|
|
||||||
<StalledProjectRow key={i} p={p} projectsMap={projectsMap} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* AI文档休眠项目(≥90天) */}
|
|
||||||
{stats.ai_doc_stale_projects.length > 0 && (
|
|
||||||
<div className="bg-red-50 rounded-lg border border-red-100 p-3 space-y-1.5">
|
|
||||||
<p className="text-[10px] font-semibold text-red-500 uppercase tracking-wider">
|
|
||||||
AI文档休眠(90天+无快照)
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px] text-red-400 mb-1">
|
|
||||||
建议归档 .blueprint/modules/,清空 Claude 上下文负担。
|
|
||||||
</p>
|
|
||||||
{/* Fix 1:归档确认弹窗 */}
|
|
||||||
{pendingArchive ? (
|
|
||||||
<div className="bg-red-100 border border-red-200 rounded p-2.5 space-y-2">
|
|
||||||
<p className="text-[11px] text-red-700 font-semibold">
|
|
||||||
确认归档「{pendingArchive.name}」?
|
|
||||||
</p>
|
|
||||||
<p className="text-[10px] text-red-500 leading-relaxed">
|
|
||||||
将删除 .blueprint/modules/*.md,文件会备份到 .blueprint/archive/,但操作本身不可一键撤销。
|
|
||||||
</p>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={handleArchiveConfirm}
|
|
||||||
disabled={!!archiving}
|
|
||||||
className="flex-1 py-1 rounded bg-red-600 text-white text-[11px] hover:bg-red-700 disabled:opacity-50 transition-colors"
|
|
||||||
>
|
|
||||||
确认归档
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setPendingArchive(null)}
|
|
||||||
className="flex-1 py-1 rounded border border-red-200 text-red-500 text-[11px] hover:bg-red-50 transition-colors"
|
|
||||||
>
|
|
||||||
取消
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
stats.ai_doc_stale_projects.map((p, i) => (
|
|
||||||
<StalledProjectRow
|
|
||||||
key={i}
|
|
||||||
p={p}
|
|
||||||
projectsMap={projectsMap}
|
|
||||||
onArchive={archiving ? undefined : (path, name) => setPendingArchive({ path, name })}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
{archiving && (
|
|
||||||
<p className="text-[10px] text-red-400">归档 {archiving} 中…</p>
|
|
||||||
)}
|
|
||||||
{archiveMsg && (
|
|
||||||
<p className={`text-[10px] break-all ${archiveMsg.startsWith("✓") ? "text-green-600" : "text-red-600"}`}>
|
|
||||||
{archiveMsg}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 迭代跳变项目 */}
|
|
||||||
{(stats.iteration_jumps ?? []).length > 0 && (
|
|
||||||
<div className="bg-purple-50 rounded-lg border border-purple-100 p-3 space-y-1.5">
|
|
||||||
<p className="text-[10px] font-semibold text-purple-500 uppercase tracking-wider">
|
|
||||||
迭代跳变(主动重构,完成率重置)
|
|
||||||
</p>
|
|
||||||
{(stats.iteration_jumps ?? []).map((j, i) => (
|
|
||||||
<div key={i} className="flex items-center gap-2 text-[11px]">
|
|
||||||
<span className="text-gray-600 truncate flex-1">{j.project}</span>
|
|
||||||
<span className="text-purple-400 shrink-0">迭代 {j.from_iteration}→{j.to_iteration}</span>
|
|
||||||
<span className={`shrink-0 font-mono ${j.rate_after < j.rate_before - 0.3 ? "text-amber-500 font-semibold" : "text-gray-400"}`}>
|
|
||||||
{(j.rate_before * 100).toFixed(0)}%→{(j.rate_after * 100).toFixed(0)}%
|
|
||||||
{j.rate_after < j.rate_before - 0.3 && " ⚠️"}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 停滞模块 */}
|
|
||||||
{(stats.stalled_modules ?? []).length > 0 && (
|
|
||||||
<div className="bg-gray-50 rounded-lg border border-gray-100 p-3 space-y-1.5">
|
|
||||||
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">
|
|
||||||
停滞模块(in_progress 但无可推进任务)
|
|
||||||
</p>
|
|
||||||
<ul className="space-y-0.5">
|
|
||||||
{(stats.stalled_modules ?? []).map((m, i) => (
|
|
||||||
<li key={i} className="text-[11px] text-gray-500 font-mono">{m}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Git 执行质量 */}
|
|
||||||
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Git 执行质量</p>
|
|
||||||
<button
|
|
||||||
onClick={handleIngestAll}
|
|
||||||
disabled={ingesting || projectsWithPath.length === 0}
|
|
||||||
className="text-[10px] px-2 py-0.5 rounded bg-indigo-50 text-indigo-600 border border-indigo-200 hover:bg-indigo-100 disabled:opacity-40 transition-colors"
|
|
||||||
>
|
|
||||||
{ingesting ? "导入中…" : "全量导入"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{ingestSummary && (
|
|
||||||
<p className="text-[10px] text-green-600">{ingestSummary}</p>
|
|
||||||
)}
|
|
||||||
{commitHealth && commitHealth.length > 0 ? (
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full text-[10px]">
|
|
||||||
<thead>
|
|
||||||
<tr className="text-gray-400 border-b border-gray-100">
|
|
||||||
<th className="text-left pb-1 font-medium">项目</th>
|
|
||||||
<th className="text-right pb-1 font-medium">commits</th>
|
|
||||||
<th className="text-right pb-1 font-medium">规范化</th>
|
|
||||||
<th className="text-right pb-1 font-medium">返工率</th>
|
|
||||||
<th className="text-left pb-1 font-medium pl-2">TOP scope</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-50">
|
|
||||||
{commitHealth.map((h) => {
|
|
||||||
return (
|
|
||||||
<tr key={h.project_id} className="hover:bg-gray-50">
|
|
||||||
<td className="py-1 text-gray-700 truncate max-w-[80px]" title={h.project_name}>
|
|
||||||
{h.project_name}
|
|
||||||
</td>
|
|
||||||
<td className="py-1 text-right font-mono text-gray-500">{h.total_commits}</td>
|
|
||||||
<td className={`py-1 text-right font-mono font-semibold ${h.conventional_rate >= 0.8 ? "text-green-600" : h.conventional_rate >= 0.5 ? "text-amber-500" : "text-red-500"}`}>
|
|
||||||
{Math.round(h.conventional_rate * 100)}%
|
|
||||||
</td>
|
|
||||||
<td className={`py-1 text-right font-mono ${h.rework_rate > 0.2 ? "text-red-500" : h.rework_rate > 0.1 ? "text-amber-500" : "text-gray-500"}`}>
|
|
||||||
{Math.round(h.rework_rate * 100)}%
|
|
||||||
</td>
|
|
||||||
<td className="py-1 pl-2 text-gray-400 truncate max-w-[100px]">
|
|
||||||
{h.top_scopes.slice(0, 2).map((s) => `${s.scope}(${s.commit_count})`).join(" ")}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-[10px] text-gray-400 italic">
|
|
||||||
暂无数据——点"全量导入"从各项目 git 历史初始化
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 梦核分析 */}
|
|
||||||
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
|
|
||||||
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">梦核分析</p>
|
|
||||||
<p className="text-[11px] text-gray-500 leading-relaxed">
|
|
||||||
生成包含以上数据的提示词,交给 Claude Opus 做固化/遗忘判断。
|
|
||||||
</p>
|
|
||||||
{!muhePrompt ? (
|
|
||||||
<button
|
|
||||||
onClick={handleGenerateMuhe}
|
|
||||||
disabled={generating}
|
|
||||||
className="w-full py-1.5 rounded-lg bg-emerald-600 text-white text-xs hover:bg-emerald-700 disabled:opacity-50 transition-colors"
|
|
||||||
>
|
|
||||||
{generating ? "生成中…" : "生成梦核提示词"}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<textarea
|
|
||||||
readOnly
|
|
||||||
value={muhePrompt}
|
|
||||||
rows={10}
|
|
||||||
className="w-full font-mono text-[11px] text-gray-700 bg-gray-50 border border-gray-200 rounded-lg p-2.5 resize-none outline-none"
|
|
||||||
onClick={(e) => (e.target as HTMLTextAreaElement).select()}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={handleCopy}
|
|
||||||
className="w-full py-1.5 rounded-lg bg-emerald-600 text-white text-xs hover:bg-emerald-700 transition-colors"
|
|
||||||
>
|
|
||||||
{copied ? "已复制 ✓" : "复制提示词"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setMuhePrompt("")}
|
|
||||||
className="w-full py-1 rounded-lg text-xs text-gray-400 hover:text-gray-600 transition-colors"
|
|
||||||
>
|
|
||||||
重新生成
|
|
||||||
</button>
|
|
||||||
{/* Fix 3:说清楚实际流程——梦核不自动写入,需手动更新源码再发版 */}
|
|
||||||
<p className="text-[10px] text-gray-400 text-center leading-relaxed">
|
|
||||||
复制 → 粘贴给 Claude Opus → 将建议反馈给炼境维护者 → 更新 CONVENTIONS.md 并发布新版本
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MetricRow({ label, value, good, invert = false }: {
|
|
||||||
label: string;
|
|
||||||
value: string;
|
|
||||||
good: boolean;
|
|
||||||
invert?: boolean;
|
|
||||||
}) {
|
|
||||||
const isGood = invert ? !good : good;
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<span className="text-[10px] text-gray-400">{label}</span>
|
|
||||||
<span className={`text-[10px] font-mono font-semibold ${isGood ? "text-green-600" : "text-yellow-600"}`}>
|
|
||||||
{value}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
// Gitea 集成治理区块:实例管理 + 当前项目仓库绑定 + webhook 注册
|
// Gitea 集成,拆两半(ui-restructure N4):
|
||||||
|
// - GiteaInstancesSection:实例管理(全局配置)→ 挂设置页
|
||||||
|
// - GiteaPanel:当前项目仓库绑定 + webhook(项目级)→ 留项目蓝图治理面板
|
||||||
// 控制面在 gitea.rs,观测面(webhook 接收)在 mcp_server.rs
|
// 控制面在 gitea.rs,观测面(webhook 接收)在 mcp_server.rs
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
@ -18,11 +20,6 @@ import {
|
|||||||
} from "../../lib/commands";
|
} from "../../lib/commands";
|
||||||
import { useUIStore } from "../../store/ui";
|
import { useUIStore } from "../../store/ui";
|
||||||
|
|
||||||
interface Props {
|
|
||||||
projectId: string;
|
|
||||||
projectName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
type BindMode = "create" | "migrate" | "link";
|
type BindMode = "create" | "migrate" | "link";
|
||||||
|
|
||||||
const CALLBACK_BASE_KEY = "gitea_callback_base";
|
const CALLBACK_BASE_KEY = "gitea_callback_base";
|
||||||
@ -32,6 +29,121 @@ const inputCls =
|
|||||||
const btnCls =
|
const btnCls =
|
||||||
"px-2 py-1 rounded text-[11px] border border-gray-200 text-gray-500 hover:bg-gray-50 transition-colors";
|
"px-2 py-1 rounded text-[11px] border border-gray-200 text-gray-500 hover:bg-gray-50 transition-colors";
|
||||||
|
|
||||||
|
// ── 实例管理(全局,挂设置页)────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function GiteaInstancesSection() {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
const showToast = useUIStore((s) => s.showToast);
|
||||||
|
|
||||||
|
const { data: instances = [] } = useQuery({
|
||||||
|
queryKey: ["gitea-instances"],
|
||||||
|
queryFn: giteaListInstances,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [showAdd, setShowAdd] = useState(false);
|
||||||
|
const [instName, setInstName] = useState("");
|
||||||
|
const [instUrl, setInstUrl] = useState("");
|
||||||
|
const [instToken, setInstToken] = useState("");
|
||||||
|
const [testing, setTesting] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleAdd = async () => {
|
||||||
|
if (!instName.trim() || !instUrl.trim() || !instToken.trim()) return;
|
||||||
|
try {
|
||||||
|
await giteaSaveInstance({
|
||||||
|
name: instName.trim(),
|
||||||
|
url: instUrl.trim(),
|
||||||
|
access_token: instToken.trim(),
|
||||||
|
is_default: instances.length === 0,
|
||||||
|
});
|
||||||
|
qc.invalidateQueries({ queryKey: ["gitea-instances"] });
|
||||||
|
setInstName(""); setInstUrl(""); setInstToken("");
|
||||||
|
setShowAdd(false);
|
||||||
|
showToast("Gitea 实例已添加 ✓");
|
||||||
|
} catch (e) {
|
||||||
|
showToast(`❌ ${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTest = async (inst: GiteaInstance) => {
|
||||||
|
setTesting(inst.id);
|
||||||
|
try {
|
||||||
|
const user = await giteaTestConnection(inst.id);
|
||||||
|
showToast(`✓ 连接成功,账户: ${user.login}`);
|
||||||
|
} catch (e) {
|
||||||
|
showToast(`❌ ${e}`);
|
||||||
|
} finally {
|
||||||
|
setTesting(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (inst: GiteaInstance) => {
|
||||||
|
if (!confirm(`删除实例「${inst.name}」?其下所有项目绑定会一并删除。`)) return;
|
||||||
|
try {
|
||||||
|
await giteaDeleteInstance(inst.id);
|
||||||
|
qc.invalidateQueries({ queryKey: ["gitea-instances"] });
|
||||||
|
qc.invalidateQueries({ queryKey: ["gitea-linked-repos"] });
|
||||||
|
showToast("已删除");
|
||||||
|
} catch (e) {
|
||||||
|
showToast(`❌ ${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-white rounded-xl border border-gray-200 shadow-sm px-6 py-5">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-sm font-semibold text-gray-700">🪁 Gitea 实例</h2>
|
||||||
|
{!showAdd && (
|
||||||
|
<button onClick={() => setShowAdd(true)} className="text-xs text-blue-500 hover:text-blue-600">
|
||||||
|
+ 添加实例
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{instances.length === 0 && !showAdd && (
|
||||||
|
<p className="text-xs text-gray-400">尚未添加 Gitea 实例——添加后可在各项目蓝图的治理面板中绑定仓库</p>
|
||||||
|
)}
|
||||||
|
{instances.map((inst) => (
|
||||||
|
<div key={inst.id} className="flex items-center gap-2 text-xs">
|
||||||
|
<span className="font-medium text-gray-700 shrink-0">{inst.name}</span>
|
||||||
|
{inst.is_default && (
|
||||||
|
<span className="text-[10px] px-1 rounded bg-blue-50 text-blue-500 shrink-0">默认</span>
|
||||||
|
)}
|
||||||
|
<span className="text-gray-400 font-mono truncate flex-1">{inst.url}</span>
|
||||||
|
<button onClick={() => handleTest(inst)} disabled={testing === inst.id} className={btnCls}>
|
||||||
|
{testing === inst.id ? "…" : "测试连接"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => handleDelete(inst)}
|
||||||
|
className="px-2 py-1 rounded text-[11px] border border-gray-200 text-red-400 hover:bg-red-50"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{showAdd && (
|
||||||
|
<div className="space-y-2 border-t border-gray-100 pt-3">
|
||||||
|
<input className={inputCls} placeholder="实例名称(如:家里 NAS)" value={instName} onChange={(e) => setInstName(e.target.value)} />
|
||||||
|
<input className={inputCls} placeholder="https://gitea.example.com" value={instUrl} onChange={(e) => setInstUrl(e.target.value)} />
|
||||||
|
<input className={inputCls} type="password" placeholder="Access Token(用户设置 → 应用 → 生成令牌)" value={instToken} onChange={(e) => setInstToken(e.target.value)} />
|
||||||
|
<div className="flex gap-1.5">
|
||||||
|
<button onClick={handleAdd} className="px-3 py-1.5 rounded text-xs bg-blue-600 text-white hover:bg-blue-700">保存</button>
|
||||||
|
<button onClick={() => setShowAdd(false)} className="px-3 py-1.5 rounded text-xs border border-gray-200 text-gray-500 hover:bg-gray-50">取消</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 项目级绑定 + webhook(留项目蓝图治理面板)────────────────────────────────
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
projectId: string;
|
||||||
|
projectName: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function GiteaPanel({ projectId, projectName }: Props) {
|
export function GiteaPanel({ projectId, projectName }: Props) {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const showToast = useUIStore((s) => s.showToast);
|
const showToast = useUIStore((s) => s.showToast);
|
||||||
@ -56,56 +168,7 @@ export function GiteaPanel({ projectId, projectName }: Props) {
|
|||||||
qc.invalidateQueries({ queryKey: ["gitea-linked-repos"] });
|
qc.invalidateQueries({ queryKey: ["gitea-linked-repos"] });
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── 实例管理 ──
|
// ── 绑定操作 ──
|
||||||
const [showAddInstance, setShowAddInstance] = useState(false);
|
|
||||||
const [instName, setInstName] = useState("");
|
|
||||||
const [instUrl, setInstUrl] = useState("");
|
|
||||||
const [instToken, setInstToken] = useState("");
|
|
||||||
const [testing, setTesting] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const handleAddInstance = async () => {
|
|
||||||
if (!instName.trim() || !instUrl.trim() || !instToken.trim()) return;
|
|
||||||
try {
|
|
||||||
await giteaSaveInstance({
|
|
||||||
name: instName.trim(),
|
|
||||||
url: instUrl.trim(),
|
|
||||||
access_token: instToken.trim(),
|
|
||||||
is_default: instances.length === 0,
|
|
||||||
});
|
|
||||||
qc.invalidateQueries({ queryKey: ["gitea-instances"] });
|
|
||||||
setInstName(""); setInstUrl(""); setInstToken("");
|
|
||||||
setShowAddInstance(false);
|
|
||||||
showToast("Gitea 实例已添加 ✓");
|
|
||||||
} catch (e) {
|
|
||||||
showToast(`❌ ${e}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTest = async (inst: GiteaInstance) => {
|
|
||||||
setTesting(inst.id);
|
|
||||||
try {
|
|
||||||
const user = await giteaTestConnection(inst.id);
|
|
||||||
showToast(`✓ 连接成功,账户: ${user.login}`);
|
|
||||||
} catch (e) {
|
|
||||||
showToast(`❌ ${e}`);
|
|
||||||
} finally {
|
|
||||||
setTesting(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteInstance = async (inst: GiteaInstance) => {
|
|
||||||
if (!confirm(`删除实例「${inst.name}」?其下所有项目绑定会一并删除。`)) return;
|
|
||||||
try {
|
|
||||||
await giteaDeleteInstance(inst.id);
|
|
||||||
qc.invalidateQueries({ queryKey: ["gitea-instances"] });
|
|
||||||
refresh();
|
|
||||||
showToast("已删除");
|
|
||||||
} catch (e) {
|
|
||||||
showToast(`❌ ${e}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── 当前项目绑定 ──
|
|
||||||
const [bindMode, setBindMode] = useState<BindMode | null>(null);
|
const [bindMode, setBindMode] = useState<BindMode | null>(null);
|
||||||
const [bindInstanceId, setBindInstanceId] = useState("");
|
const [bindInstanceId, setBindInstanceId] = useState("");
|
||||||
const [repoName, setRepoName] = useState("");
|
const [repoName, setRepoName] = useState("");
|
||||||
@ -184,51 +247,11 @@ export function GiteaPanel({ projectId, projectName }: Props) {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-3">
|
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-3">
|
||||||
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">
|
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">
|
||||||
🪁 Gitea 集成
|
🪁 Gitea 绑定
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* ── 实例列表 ── */}
|
|
||||||
<div className="space-y-1.5">
|
|
||||||
{instances.length === 0 && !showAddInstance && (
|
|
||||||
<p className="text-[11px] text-gray-400">尚未添加 Gitea 实例</p>
|
|
||||||
)}
|
|
||||||
{instances.map((inst) => (
|
|
||||||
<div key={inst.id} className="flex items-center gap-2 text-[11px]">
|
|
||||||
<span className="font-medium text-gray-700 shrink-0">{inst.name}</span>
|
|
||||||
{inst.is_default && (
|
|
||||||
<span className="text-[10px] px-1 rounded bg-blue-50 text-blue-500 shrink-0">默认</span>
|
|
||||||
)}
|
|
||||||
<span className="text-gray-400 font-mono truncate flex-1">{inst.url}</span>
|
|
||||||
<button onClick={() => handleTest(inst)} disabled={testing === inst.id} className={btnCls}>
|
|
||||||
{testing === inst.id ? "…" : "测试"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleDeleteInstance(inst)}
|
|
||||||
className="px-2 py-1 rounded text-[11px] border border-gray-200 text-red-400 hover:bg-red-50"
|
|
||||||
>
|
|
||||||
删
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{showAddInstance ? (
|
|
||||||
<div className="space-y-1.5 border-t border-gray-50 pt-2">
|
|
||||||
<input className={inputCls} placeholder="实例名称(如:家里 NAS)" value={instName} onChange={(e) => setInstName(e.target.value)} />
|
|
||||||
<input className={inputCls} placeholder="https://gitea.example.com" value={instUrl} onChange={(e) => setInstUrl(e.target.value)} />
|
|
||||||
<input className={inputCls} type="password" placeholder="Access Token(用户设置 → 应用 → 生成令牌)" value={instToken} onChange={(e) => setInstToken(e.target.value)} />
|
|
||||||
<div className="flex gap-1.5">
|
|
||||||
<button onClick={handleAddInstance} className="px-2.5 py-1 rounded text-[11px] bg-blue-600 text-white hover:bg-blue-700">保存</button>
|
|
||||||
<button onClick={() => setShowAddInstance(false)} className={btnCls}>取消</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button onClick={() => setShowAddInstance(true)} className="text-[11px] text-blue-500 hover:text-blue-600">
|
|
||||||
+ 添加实例
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* ── 当前项目绑定 ── */}
|
{/* ── 当前项目绑定 ── */}
|
||||||
<div className="border-t border-gray-50 pt-2 space-y-2">
|
<div className="space-y-2">
|
||||||
{link ? (
|
{link ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-2 text-[11px]">
|
<div className="flex items-center gap-2 text-[11px]">
|
||||||
@ -280,6 +303,10 @@ export function GiteaPanel({ projectId, projectName }: Props) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
) : instances.length === 0 ? (
|
||||||
|
<p className="text-[11px] text-gray-400">
|
||||||
|
尚未配置 Gitea 实例——请先在「设置 → Gitea 实例」中添加
|
||||||
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
@ -328,9 +355,6 @@ export function GiteaPanel({ projectId, projectName }: Props) {
|
|||||||
>
|
>
|
||||||
{binding ? "执行中…" : "确认"}
|
{binding ? "执行中…" : "确认"}
|
||||||
</button>
|
</button>
|
||||||
{instances.length === 0 && (
|
|
||||||
<p className="text-[10px] text-amber-500">请先添加 Gitea 实例</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -9,10 +9,6 @@ import {
|
|||||||
} from "../../lib/commands";
|
} from "../../lib/commands";
|
||||||
import { useUIStore } from "../../store/ui";
|
import { useUIStore } from "../../store/ui";
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 部署信息编辑表单 ──────────────────────────────────────────────────────────
|
// ── 部署信息编辑表单 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function DeployForm({
|
function DeployForm({
|
||||||
@ -118,8 +114,9 @@ function DeployForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── 已注册仓库列表 ────────────────────────────────────────────────────────────
|
// ── 已注册仓库列表 ────────────────────────────────────────────────────────────
|
||||||
|
// 同时被 ManagePage「仓库」tab 复用(ui-restructure N5)
|
||||||
|
|
||||||
function RegistryTab() {
|
export function RegistryTab() {
|
||||||
const showToast = useUIStore((s) => s.showToast);
|
const showToast = useUIStore((s) => s.showToast);
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||||
@ -149,7 +146,7 @@ function RegistryTab() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex-1 flex flex-col items-center justify-center text-gray-400 py-16 gap-2">
|
<div className="flex-1 flex flex-col items-center justify-center text-gray-400 py-16 gap-2">
|
||||||
<p className="text-2xl">📦</p>
|
<p className="text-2xl">📦</p>
|
||||||
<p className="text-sm">还没有注册仓库,通过「发布 / 绑定仓库」流程注册</p>
|
<p className="text-sm">还没有注册仓库——在项目编辑中填写仓库地址或从仓库挂载后自动注册</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -214,22 +211,3 @@ function RegistryTab() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 主模态框 ──────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function RepoRegistryModal({ onClose }: Props) {
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
|
||||||
<div className="bg-white rounded-xl shadow-2xl w-[700px] max-h-[85vh] flex flex-col border border-gray-200">
|
|
||||||
<div className="px-5 py-4 border-b border-gray-100 flex items-center justify-between">
|
|
||||||
<h2 className="text-base font-semibold text-gray-800">仓库管理</h2>
|
|
||||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 text-xl leading-none">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col flex-1 min-h-0">
|
|
||||||
<RegistryTab />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
423
src/components/insight/FlywheelPanel.tsx
Normal file
423
src/components/insight/FlywheelPanel.tsx
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
// 飞轮健康面板(跨项目):版本队列对比 / 阻塞原因 / 停转与休眠归档 / Git 执行质量 / 梦核分析
|
||||||
|
// 2026-07-02 ui-restructure N3:从 BlueprintModal 侧板迁出为洞察页一级内容(跨项目内容不属于单项目弹窗)
|
||||||
|
import { useState, useMemo } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
getProjects,
|
||||||
|
getFlywheelStats,
|
||||||
|
generateMuhePrompt,
|
||||||
|
archiveProjectDocs,
|
||||||
|
ingestFullGitHistory,
|
||||||
|
getAllCommitHealth,
|
||||||
|
type Project,
|
||||||
|
type FlywheelStats,
|
||||||
|
type StalledProject,
|
||||||
|
type CommitHealthWithName,
|
||||||
|
} from "../../lib/commands";
|
||||||
|
|
||||||
|
function StalledProjectRow({ p, projectsMap, onArchive }: {
|
||||||
|
p: StalledProject;
|
||||||
|
projectsMap: Record<string, string>; // name → path
|
||||||
|
onArchive?: (path: string, name: string) => void;
|
||||||
|
}) {
|
||||||
|
const path = projectsMap[p.name];
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-[11px] text-amber-700 truncate">· {p.name}</p>
|
||||||
|
<p className="text-[10px] text-amber-400">最后活跃: {p.last_active}</p>
|
||||||
|
</div>
|
||||||
|
{onArchive && path && (
|
||||||
|
<button
|
||||||
|
onClick={() => onArchive(path, p.name)}
|
||||||
|
className="text-[10px] text-amber-600 bg-amber-100 hover:bg-amber-200 px-1.5 py-0.5 rounded shrink-0 transition-colors"
|
||||||
|
>
|
||||||
|
归档
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MetricRow({ label, value, good, invert = false }: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
good: boolean;
|
||||||
|
invert?: boolean;
|
||||||
|
}) {
|
||||||
|
const isGood = invert ? !good : good;
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-[10px] text-gray-400">{label}</span>
|
||||||
|
<span className={`text-[10px] font-mono font-semibold ${isGood ? "text-green-600" : "text-yellow-600"}`}>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FlywheelPanel() {
|
||||||
|
const { data: projects } = useQuery<Project[]>({
|
||||||
|
queryKey: ["projects-for-flywheel"],
|
||||||
|
queryFn: () => getProjects(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { projectPaths, projectsMap, projectsWithPath } = useMemo(() => {
|
||||||
|
const paths = (projects ?? [])
|
||||||
|
.map((p) => p.win_path || p.wsl_path || "")
|
||||||
|
.filter(Boolean);
|
||||||
|
const map: Record<string, string> = {};
|
||||||
|
const withPath: Array<{ id: string; name: string; path: string }> = [];
|
||||||
|
for (const p of (projects ?? [])) {
|
||||||
|
const path = p.win_path || p.wsl_path || "";
|
||||||
|
if (!path) continue;
|
||||||
|
withPath.push({ id: p.id, name: p.name, path });
|
||||||
|
map[p.name] = path;
|
||||||
|
const dirName = path.replace(/\\/g, "/").split("/").filter(Boolean).pop() ?? "";
|
||||||
|
if (dirName && !map[dirName]) map[dirName] = path;
|
||||||
|
}
|
||||||
|
return { projectPaths: paths, projectsMap: map, projectsWithPath: withPath };
|
||||||
|
}, [projects]);
|
||||||
|
|
||||||
|
const { data: commitHealth, refetch: refetchHealth } = useQuery<CommitHealthWithName[]>({
|
||||||
|
queryKey: ["commit-health-all"],
|
||||||
|
queryFn: () => getAllCommitHealth(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [ingesting, setIngesting] = useState(false);
|
||||||
|
const [ingestSummary, setIngestSummary] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const { data: stats, isLoading } = useQuery<FlywheelStats>({
|
||||||
|
queryKey: ["flywheel-stats", projectPaths],
|
||||||
|
queryFn: () => getFlywheelStats(projectPaths),
|
||||||
|
enabled: projectPaths.length > 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [muhePrompt, setMuhePrompt] = useState("");
|
||||||
|
const [generating, setGenerating] = useState(false);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [archiveMsg, setArchiveMsg] = useState<string | null>(null);
|
||||||
|
const [archiving, setArchiving] = useState<string | null>(null);
|
||||||
|
const [pendingArchive, setPendingArchive] = useState<{ path: string; name: string } | null>(null);
|
||||||
|
|
||||||
|
const handleIngestAll = async () => {
|
||||||
|
setIngesting(true);
|
||||||
|
setIngestSummary(null);
|
||||||
|
let totalImported = 0;
|
||||||
|
let totalSkipped = 0;
|
||||||
|
for (const { id, path } of projectsWithPath) {
|
||||||
|
try {
|
||||||
|
const r = await ingestFullGitHistory(id, path);
|
||||||
|
totalImported += r.imported;
|
||||||
|
totalSkipped += r.skipped;
|
||||||
|
} catch {
|
||||||
|
// 单个项目失败不中断其他项目
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await refetchHealth();
|
||||||
|
setIngestSummary(`新导入 ${totalImported} 条,跳过 ${totalSkipped} 条`);
|
||||||
|
setIngesting(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGenerateMuhe = async () => {
|
||||||
|
if (!stats) return;
|
||||||
|
setGenerating(true);
|
||||||
|
try {
|
||||||
|
const result = await generateMuhePrompt(stats);
|
||||||
|
setMuhePrompt(result);
|
||||||
|
} finally {
|
||||||
|
setGenerating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = () => {
|
||||||
|
navigator.clipboard.writeText(muhePrompt).then(() => {
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 2000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleArchiveConfirm = async () => {
|
||||||
|
if (!pendingArchive) return;
|
||||||
|
const { path, name } = pendingArchive;
|
||||||
|
setPendingArchive(null);
|
||||||
|
setArchiving(name);
|
||||||
|
setArchiveMsg(null);
|
||||||
|
try {
|
||||||
|
const result = await archiveProjectDocs(path);
|
||||||
|
setArchiveMsg(`✓ 已归档 ${result.archived_files.length} 个文件 → ${result.archive_dir}`);
|
||||||
|
} catch (e) {
|
||||||
|
setArchiveMsg(`归档失败: ${String(e)}`);
|
||||||
|
} finally {
|
||||||
|
setArchiving(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fmtPct = (v: number) => `${(v * 100).toFixed(1)}%`;
|
||||||
|
|
||||||
|
// 去重——90天休眠的项目不在60天停转里重复展示
|
||||||
|
const stalledOnlyProjects = (stats?.stalled_projects ?? []).filter(
|
||||||
|
(p) => !(stats?.ai_doc_stale_projects ?? []).some((q) => q.name === p.name)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-800">飞轮健康</h3>
|
||||||
|
{stats && (
|
||||||
|
<p className="text-[10px] text-gray-400 mt-0.5">
|
||||||
|
{stats.projects_with_data}/{stats.total_projects_scanned} 个项目有数据
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<p className="text-xs text-gray-400 text-center py-6">聚合中…</p>
|
||||||
|
) : !stats || stats.projects_with_data === 0 ? (
|
||||||
|
<div className="text-center py-6 space-y-2">
|
||||||
|
<p className="text-xs text-gray-400">暂无飞轮数据</p>
|
||||||
|
<p className="text-[11px] text-gray-300">打开各项目蓝图后数据将自动积累</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* CONVENTIONS 版本队列对比 */}
|
||||||
|
{stats.cohorts.length > 0 && (
|
||||||
|
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
|
||||||
|
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">版本队列对比</p>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{stats.cohorts.map((c) => (
|
||||||
|
<div key={c.conventions_version} className="space-y-1.5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs font-mono text-gray-600">v{c.conventions_version}</span>
|
||||||
|
<span className="text-[10px] text-gray-400">{c.project_count} 个项目</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-x-3 gap-y-0.5">
|
||||||
|
<MetricRow label="可派发率" value={fmtPct(c.dispatchable_rate)} good={c.dispatchable_rate > 0.3} />
|
||||||
|
<MetricRow label="任务完成率" value={fmtPct(c.completion_rate)} good={c.completion_rate > 0.5} />
|
||||||
|
<MetricRow label="飞轮持续率" value={fmtPct(c.continuity_rate)} good={c.continuity_rate > 0.6} />
|
||||||
|
<MetricRow label="阻塞密度" value={fmtPct(c.blocked_density)} good={c.blocked_density < 0.1} invert />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* TOP blocked reasons */}
|
||||||
|
{stats.top_blocked_reasons.length > 0 && (
|
||||||
|
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
|
||||||
|
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">TOP 阻塞原因</p>
|
||||||
|
<ul className="space-y-1">
|
||||||
|
{stats.top_blocked_reasons.map((r, i) => (
|
||||||
|
<li key={i} className="flex items-start gap-2">
|
||||||
|
<span className="text-[10px] text-gray-400 shrink-0 mt-0.5">#{i + 1}</span>
|
||||||
|
<span className="text-[11px] text-red-600 flex-1">{r.reason}</span>
|
||||||
|
<span className="text-[10px] text-gray-400 shrink-0">×{r.count}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 仅展示"停转但未到90天"的项目 */}
|
||||||
|
{stalledOnlyProjects.length > 0 && (
|
||||||
|
<div className="bg-amber-50 rounded-lg border border-amber-100 p-3 space-y-1.5">
|
||||||
|
<p className="text-[10px] font-semibold text-amber-500 uppercase tracking-wider">
|
||||||
|
飞轮停转(60–90天无快照)
|
||||||
|
</p>
|
||||||
|
{stalledOnlyProjects.map((p, i) => (
|
||||||
|
<StalledProjectRow key={i} p={p} projectsMap={projectsMap} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* AI文档休眠项目(≥90天) */}
|
||||||
|
{stats.ai_doc_stale_projects.length > 0 && (
|
||||||
|
<div className="bg-red-50 rounded-lg border border-red-100 p-3 space-y-1.5">
|
||||||
|
<p className="text-[10px] font-semibold text-red-500 uppercase tracking-wider">
|
||||||
|
AI文档休眠(90天+无快照)
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-red-400 mb-1">
|
||||||
|
建议归档 .blueprint/modules/,清空 Claude 上下文负担。
|
||||||
|
</p>
|
||||||
|
{pendingArchive ? (
|
||||||
|
<div className="bg-red-100 border border-red-200 rounded p-2.5 space-y-2">
|
||||||
|
<p className="text-[11px] text-red-700 font-semibold">
|
||||||
|
确认归档「{pendingArchive.name}」?
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-red-500 leading-relaxed">
|
||||||
|
将删除 .blueprint/modules/*.md,文件会备份到 .blueprint/archive/,但操作本身不可一键撤销。
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleArchiveConfirm}
|
||||||
|
disabled={!!archiving}
|
||||||
|
className="flex-1 py-1 rounded bg-red-600 text-white text-[11px] hover:bg-red-700 disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
确认归档
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setPendingArchive(null)}
|
||||||
|
className="flex-1 py-1 rounded border border-red-200 text-red-500 text-[11px] hover:bg-red-50 transition-colors"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
stats.ai_doc_stale_projects.map((p, i) => (
|
||||||
|
<StalledProjectRow
|
||||||
|
key={i}
|
||||||
|
p={p}
|
||||||
|
projectsMap={projectsMap}
|
||||||
|
onArchive={archiving ? undefined : (path, name) => setPendingArchive({ path, name })}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
{archiving && (
|
||||||
|
<p className="text-[10px] text-red-400">归档 {archiving} 中…</p>
|
||||||
|
)}
|
||||||
|
{archiveMsg && (
|
||||||
|
<p className={`text-[10px] break-all ${archiveMsg.startsWith("✓") ? "text-green-600" : "text-red-600"}`}>
|
||||||
|
{archiveMsg}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 迭代跳变项目 */}
|
||||||
|
{(stats.iteration_jumps ?? []).length > 0 && (
|
||||||
|
<div className="bg-purple-50 rounded-lg border border-purple-100 p-3 space-y-1.5">
|
||||||
|
<p className="text-[10px] font-semibold text-purple-500 uppercase tracking-wider">
|
||||||
|
迭代跳变(主动重构,完成率重置)
|
||||||
|
</p>
|
||||||
|
{(stats.iteration_jumps ?? []).map((j, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-2 text-[11px]">
|
||||||
|
<span className="text-gray-600 truncate flex-1">{j.project}</span>
|
||||||
|
<span className="text-purple-400 shrink-0">迭代 {j.from_iteration}→{j.to_iteration}</span>
|
||||||
|
<span className={`shrink-0 font-mono ${j.rate_after < j.rate_before - 0.3 ? "text-amber-500 font-semibold" : "text-gray-400"}`}>
|
||||||
|
{(j.rate_before * 100).toFixed(0)}%→{(j.rate_after * 100).toFixed(0)}%
|
||||||
|
{j.rate_after < j.rate_before - 0.3 && " ⚠️"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 停滞模块 */}
|
||||||
|
{(stats.stalled_modules ?? []).length > 0 && (
|
||||||
|
<div className="bg-gray-50 rounded-lg border border-gray-100 p-3 space-y-1.5">
|
||||||
|
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">
|
||||||
|
停滞模块(in_progress 但无可推进任务)
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-0.5">
|
||||||
|
{(stats.stalled_modules ?? []).map((m, i) => (
|
||||||
|
<li key={i} className="text-[11px] text-gray-500 font-mono">{m}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Git 执行质量 */}
|
||||||
|
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">Git 执行质量</p>
|
||||||
|
<button
|
||||||
|
onClick={handleIngestAll}
|
||||||
|
disabled={ingesting || projectsWithPath.length === 0}
|
||||||
|
className="text-[10px] px-2 py-0.5 rounded bg-indigo-50 text-indigo-600 border border-indigo-200 hover:bg-indigo-100 disabled:opacity-40 transition-colors"
|
||||||
|
>
|
||||||
|
{ingesting ? "导入中…" : "全量导入"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{ingestSummary && (
|
||||||
|
<p className="text-[10px] text-green-600">{ingestSummary}</p>
|
||||||
|
)}
|
||||||
|
{commitHealth && commitHealth.length > 0 ? (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-[10px]">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-gray-400 border-b border-gray-100">
|
||||||
|
<th className="text-left pb-1 font-medium">项目</th>
|
||||||
|
<th className="text-right pb-1 font-medium">commits</th>
|
||||||
|
<th className="text-right pb-1 font-medium">规范化</th>
|
||||||
|
<th className="text-right pb-1 font-medium">返工率</th>
|
||||||
|
<th className="text-left pb-1 font-medium pl-2">TOP scope</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-50">
|
||||||
|
{commitHealth.map((h) => {
|
||||||
|
return (
|
||||||
|
<tr key={h.project_id} className="hover:bg-gray-50">
|
||||||
|
<td className="py-1 text-gray-700 truncate max-w-[80px]" title={h.project_name}>
|
||||||
|
{h.project_name}
|
||||||
|
</td>
|
||||||
|
<td className="py-1 text-right font-mono text-gray-500">{h.total_commits}</td>
|
||||||
|
<td className={`py-1 text-right font-mono font-semibold ${h.conventional_rate >= 0.8 ? "text-green-600" : h.conventional_rate >= 0.5 ? "text-amber-500" : "text-red-500"}`}>
|
||||||
|
{Math.round(h.conventional_rate * 100)}%
|
||||||
|
</td>
|
||||||
|
<td className={`py-1 text-right font-mono ${h.rework_rate > 0.2 ? "text-red-500" : h.rework_rate > 0.1 ? "text-amber-500" : "text-gray-500"}`}>
|
||||||
|
{Math.round(h.rework_rate * 100)}%
|
||||||
|
</td>
|
||||||
|
<td className="py-1 pl-2 text-gray-400 truncate max-w-[100px]">
|
||||||
|
{h.top_scopes.slice(0, 2).map((s) => `${s.scope}(${s.commit_count})`).join(" ")}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-[10px] text-gray-400 italic">
|
||||||
|
暂无数据——点"全量导入"从各项目 git 历史初始化
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 梦核分析 */}
|
||||||
|
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
|
||||||
|
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">梦核分析</p>
|
||||||
|
<p className="text-[11px] text-gray-500 leading-relaxed">
|
||||||
|
生成包含以上数据的提示词,交给 Claude Opus 做固化/遗忘判断。
|
||||||
|
</p>
|
||||||
|
{!muhePrompt ? (
|
||||||
|
<button
|
||||||
|
onClick={handleGenerateMuhe}
|
||||||
|
disabled={generating}
|
||||||
|
className="w-full py-1.5 rounded-lg bg-emerald-600 text-white text-xs hover:bg-emerald-700 disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
{generating ? "生成中…" : "生成梦核提示词"}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<textarea
|
||||||
|
readOnly
|
||||||
|
value={muhePrompt}
|
||||||
|
rows={10}
|
||||||
|
className="w-full font-mono text-[11px] text-gray-700 bg-gray-50 border border-gray-200 rounded-lg p-2.5 resize-none outline-none"
|
||||||
|
onClick={(e) => (e.target as HTMLTextAreaElement).select()}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleCopy}
|
||||||
|
className="w-full py-1.5 rounded-lg bg-emerald-600 text-white text-xs hover:bg-emerald-700 transition-colors"
|
||||||
|
>
|
||||||
|
{copied ? "已复制 ✓" : "复制提示词"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setMuhePrompt("")}
|
||||||
|
className="w-full py-1 rounded-lg text-xs text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
重新生成
|
||||||
|
</button>
|
||||||
|
<p className="text-[10px] text-gray-400 text-center leading-relaxed">
|
||||||
|
复制 → 粘贴给 Claude Opus → 建议可在「全局治理」tab 直接应用(写入并同步所有项目)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
151
src/components/insight/GlobalGovernancePanel.tsx
Normal file
151
src/components/insight/GlobalGovernancePanel.tsx
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
// 全局治理:CONVENTIONS 规则来源 + 应用梦核建议(写入 master 并同步所有项目)
|
||||||
|
// 2026-07-02 ui-restructure N3:从单项目 GovernancePanel 迁出——这两块操作影响所有项目,属全局层
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import {
|
||||||
|
getUserConventionsStatus,
|
||||||
|
saveUserConventions,
|
||||||
|
resetConventionsToBuiltin,
|
||||||
|
type UserConventionsStatus,
|
||||||
|
} from "../../lib/commands";
|
||||||
|
|
||||||
|
export function GlobalGovernancePanel() {
|
||||||
|
const { data: userConventions, refetch: refetchUserConventions } = useQuery<UserConventionsStatus>({
|
||||||
|
queryKey: ["user-conventions-status"],
|
||||||
|
queryFn: () => getUserConventionsStatus(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [conventionsInput, setConventionsInput] = useState("");
|
||||||
|
const [editorOpen, setEditorOpen] = useState(false);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [saveResult, setSaveResult] = useState<string | null>(null);
|
||||||
|
const [saveError, setSaveError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!conventionsInput.trim()) return;
|
||||||
|
setSaving(true);
|
||||||
|
setSaveError(null);
|
||||||
|
setSaveResult(null);
|
||||||
|
try {
|
||||||
|
const res = await saveUserConventions(conventionsInput);
|
||||||
|
setSaveResult(`已应用 v${res.version},同步了 ${res.synced_count} 个项目`);
|
||||||
|
setConventionsInput("");
|
||||||
|
setEditorOpen(false);
|
||||||
|
refetchUserConventions();
|
||||||
|
} catch (e) {
|
||||||
|
setSaveError(String(e));
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = async () => {
|
||||||
|
setSaving(true);
|
||||||
|
setSaveError(null);
|
||||||
|
setSaveResult(null);
|
||||||
|
try {
|
||||||
|
const res = await resetConventionsToBuiltin();
|
||||||
|
setSaveResult(`已重置为内置基线 v${res.version},同步了 ${res.synced_count} 个项目`);
|
||||||
|
refetchUserConventions();
|
||||||
|
} catch (e) {
|
||||||
|
setSaveError(String(e));
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 space-y-4 max-w-2xl">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-gray-800">全局治理</h3>
|
||||||
|
<p className="text-[10px] text-gray-400 mt-0.5">CONVENTIONS 规则库——此处的变更同步到所有已接入项目</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 规则来源 */}
|
||||||
|
{userConventions && (
|
||||||
|
<div className="flex items-center justify-between bg-gray-50 rounded-lg px-3 py-2">
|
||||||
|
<span className="text-[11px] text-gray-500">规则来源</span>
|
||||||
|
{userConventions.source === "user" ? (
|
||||||
|
<span className="text-[11px] font-mono text-purple-600 bg-purple-50 px-2 py-0.5 rounded">
|
||||||
|
用户自定义 v{userConventions.version}
|
||||||
|
{userConventions.outdated_from_builtin && (
|
||||||
|
<span className="ml-1 text-yellow-600">⚠️ 低于内置</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-[11px] font-mono text-gray-500 bg-white border border-gray-200 px-2 py-0.5 rounded">
|
||||||
|
内置基线 v{userConventions.builtin_version}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 应用梦核建议 */}
|
||||||
|
<div className="bg-white rounded-lg border border-gray-100 p-3 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-wider">应用梦核建议</p>
|
||||||
|
<button
|
||||||
|
onClick={() => { setEditorOpen((v) => !v); setSaveError(null); }}
|
||||||
|
className="text-[11px] text-blue-500 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
{editorOpen ? "收起" : "展开"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{editorOpen && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-[11px] text-gray-500 leading-relaxed">
|
||||||
|
将梦核 Opus 输出的新 CONVENTIONS 内容粘贴到下方,确认后将写入并同步所有项目。
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
value={conventionsInput}
|
||||||
|
onChange={(e) => setConventionsInput(e.target.value)}
|
||||||
|
placeholder="粘贴完整的 CONVENTIONS.md 内容(须包含 frontmatter version 字段)…"
|
||||||
|
rows={8}
|
||||||
|
className="w-full text-[11px] font-mono border border-gray-200 rounded p-2 resize-none focus:outline-none focus:border-blue-400"
|
||||||
|
/>
|
||||||
|
{saveError && (
|
||||||
|
<p className="text-[11px] text-red-600 font-mono break-all">{saveError}</p>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving || !conventionsInput.trim()}
|
||||||
|
className="flex-1 py-1.5 rounded-lg bg-blue-500 text-white text-xs hover:bg-blue-600 disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
{saving ? "写入中…" : "确认应用"}
|
||||||
|
</button>
|
||||||
|
{userConventions?.source === "user" && (
|
||||||
|
<button
|
||||||
|
onClick={handleReset}
|
||||||
|
disabled={saving}
|
||||||
|
className="px-3 py-1.5 rounded-lg border border-gray-200 text-gray-600 text-xs hover:bg-gray-50 disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
重置为基线
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!editorOpen && userConventions?.source === "user" && (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="text-[11px] text-gray-500">当前使用用户自定义规则</p>
|
||||||
|
<button
|
||||||
|
onClick={handleReset}
|
||||||
|
disabled={saving}
|
||||||
|
className="text-[11px] text-gray-400 hover:text-red-500 disabled:opacity-50 transition-colors"
|
||||||
|
>
|
||||||
|
重置为基线
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{saveResult && (
|
||||||
|
<p className="text-[11px] text-green-600 bg-green-50 rounded px-2 py-1">✓ {saveResult}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[10px] text-gray-300 leading-relaxed">
|
||||||
|
项目级操作(同步规则到某项目、接入包、蓝图提示词)在各项目蓝图 → ⚙️ 治理面板中进行。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
52
src/components/insight/InsightPage.tsx
Normal file
52
src/components/insight/InsightPage.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
// 洞察页:飞轮(跨项目数据反哺)/ 健康中心 / 全局治理
|
||||||
|
// ui-restructure N3:跨项目内容的统一去处——单项目内容留在项目蓝图弹窗
|
||||||
|
import { useState } from "react";
|
||||||
|
import { FlywheelPanel } from "./FlywheelPanel";
|
||||||
|
import { GlobalGovernancePanel } from "./GlobalGovernancePanel";
|
||||||
|
import { HealthPage } from "../health/HealthPage";
|
||||||
|
|
||||||
|
type InsightTab = "flywheel" | "health" | "governance";
|
||||||
|
|
||||||
|
const TABS: { id: InsightTab; label: string }[] = [
|
||||||
|
{ id: "flywheel", label: "♻️ 飞轮" },
|
||||||
|
{ id: "health", label: "健康中心" },
|
||||||
|
{ id: "governance", label: "全局治理" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function InsightPage() {
|
||||||
|
const [tab, setTab] = useState<InsightTab>("flywheel");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full min-h-0">
|
||||||
|
<div className="shrink-0 border-b border-gray-100 bg-white px-8 flex items-center gap-1">
|
||||||
|
{TABS.map(({ id, label }) => (
|
||||||
|
<button
|
||||||
|
key={id}
|
||||||
|
onClick={() => setTab(id)}
|
||||||
|
className={`px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors ${
|
||||||
|
tab === id
|
||||||
|
? "border-blue-500 text-blue-600"
|
||||||
|
: "border-transparent text-gray-400 hover:text-gray-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto min-h-0">
|
||||||
|
{tab === "flywheel" && (
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<FlywheelPanel />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{tab === "health" && <HealthPage />}
|
||||||
|
{tab === "governance" && (
|
||||||
|
<div className="max-w-2xl mx-auto">
|
||||||
|
<GlobalGovernancePanel />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -5,7 +5,6 @@ import {
|
|||||||
type Space,
|
type Space,
|
||||||
} from "../../lib/commands";
|
} from "../../lib/commands";
|
||||||
import { useUIStore } from "../../store/ui";
|
import { useUIStore } from "../../store/ui";
|
||||||
import { RepoRegistryModal } from "../dashboard/RepoRegistryModal";
|
|
||||||
|
|
||||||
export type Page = "overview" | "dashboard" | "resources" | "insight" | "tools" | "settings";
|
export type Page = "overview" | "dashboard" | "resources" | "insight" | "tools" | "settings";
|
||||||
|
|
||||||
@ -27,7 +26,6 @@ export function Sidebar({ page, onPageChange }: Props) {
|
|||||||
const [spaceOpen, setSpaceOpen] = useState(true);
|
const [spaceOpen, setSpaceOpen] = useState(true);
|
||||||
const [showForm, setShowForm] = useState<Space | null | undefined>(undefined);
|
const [showForm, setShowForm] = useState<Space | null | undefined>(undefined);
|
||||||
const [deleteTarget, setDeleteTarget] = useState<Space | null>(null);
|
const [deleteTarget, setDeleteTarget] = useState<Space | null>(null);
|
||||||
const [showRepoRegistry, setShowRepoRegistry] = useState(false);
|
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const showToast = useUIStore((s) => s.showToast);
|
const showToast = useUIStore((s) => s.showToast);
|
||||||
|
|
||||||
@ -154,19 +152,6 @@ export function Sidebar({ page, onPageChange }: Props) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 仓库管理 */}
|
|
||||||
<div className="border-t border-slate-700 px-3 py-2 shrink-0">
|
|
||||||
<button
|
|
||||||
onClick={() => setShowRepoRegistry(true)}
|
|
||||||
className="w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-xs text-slate-400 hover:bg-slate-800 hover:text-slate-200 transition-colors"
|
|
||||||
>
|
|
||||||
<svg className="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="currentColor">
|
|
||||||
<path d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8z"/>
|
|
||||||
</svg>
|
|
||||||
<span>仓库管理</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* 新建/编辑模态框 */}
|
{/* 新建/编辑模态框 */}
|
||||||
@ -182,11 +167,6 @@ export function Sidebar({ page, onPageChange }: Props) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 仓库管理弹窗 */}
|
|
||||||
{showRepoRegistry && (
|
|
||||||
<RepoRegistryModal onClose={() => setShowRepoRegistry(false)} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 删除确认 */}
|
{/* 删除确认 */}
|
||||||
{deleteTarget && (
|
{deleteTarget && (
|
||||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
|
|||||||
@ -13,18 +13,19 @@ import { Dialog } from "../ui/Dialog";
|
|||||||
import { TemplateEditor } from "./TemplateEditor";
|
import { TemplateEditor } from "./TemplateEditor";
|
||||||
import { NewProjectModal } from "./NewProjectModal";
|
import { NewProjectModal } from "./NewProjectModal";
|
||||||
import { TagsManager } from "./TagsManager";
|
import { TagsManager } from "./TagsManager";
|
||||||
|
import { RegistryTab } from "../dashboard/RepoRegistryModal";
|
||||||
|
|
||||||
export function ManagePage() {
|
export function ManagePage() {
|
||||||
const [tab, setTab] = useState<"projects" | "groups" | "templates" | "tags">("projects");
|
const [tab, setTab] = useState<"projects" | "groups" | "templates" | "tags" | "repos">("projects");
|
||||||
const { data: spaces = [] } = useQuery({ queryKey: ["spaces"], queryFn: getSpaces });
|
const { data: spaces = [] } = useQuery({ queryKey: ["spaces"], queryFn: getSpaces });
|
||||||
const currentSpaceId = spaces.find((s) => s.is_current)?.id;
|
const currentSpaceId = spaces.find((s) => s.is_current)?.id;
|
||||||
|
|
||||||
const TAB_LABELS = { projects: "项目", groups: "产品组", templates: "项目模板", tags: "标签" };
|
const TAB_LABELS = { projects: "项目", groups: "产品组", templates: "项目模板", tags: "标签", repos: "仓库" };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full min-h-0">
|
<div className="flex flex-col h-full min-h-0">
|
||||||
<div className="shrink-0 border-b border-gray-200 bg-white px-8 flex gap-1">
|
<div className="shrink-0 border-b border-gray-200 bg-white px-8 flex gap-1">
|
||||||
{(["projects", "groups", "templates", "tags"] as const).map((t) => (
|
{(["projects", "groups", "templates", "tags", "repos"] as const).map((t) => (
|
||||||
<button
|
<button
|
||||||
key={t}
|
key={t}
|
||||||
onClick={() => setTab(t)}
|
onClick={() => setTab(t)}
|
||||||
@ -43,6 +44,11 @@ export function ManagePage() {
|
|||||||
{tab === "groups" && <GroupsTab spaceId={currentSpaceId} />}
|
{tab === "groups" && <GroupsTab spaceId={currentSpaceId} />}
|
||||||
{tab === "templates" && <TemplatesTab spaceId={currentSpaceId} />}
|
{tab === "templates" && <TemplatesTab spaceId={currentSpaceId} />}
|
||||||
{tab === "tags" && <TagsManager />}
|
{tab === "tags" && <TagsManager />}
|
||||||
|
{tab === "repos" && (
|
||||||
|
<div className="bg-white rounded-xl border border-gray-200 shadow-sm flex flex-col max-h-[70vh]">
|
||||||
|
<RegistryTab />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -148,8 +148,8 @@ export function OverviewPage() {
|
|||||||
<div className="h-full overflow-y-auto">
|
<div className="h-full overflow-y-auto">
|
||||||
<div className="max-w-2xl mx-auto px-6 py-6 space-y-6">
|
<div className="max-w-2xl mx-auto px-6 py-6 space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-base font-semibold text-gray-800">仪表盘</h1>
|
<h1 className="text-base font-semibold text-gray-800">总览</h1>
|
||||||
<p className="text-xs text-gray-400 mt-0.5">{buffs.length} 个项目已施加蓝图 Buff</p>
|
<p className="text-xs text-gray-400 mt-0.5">{buffs.length} 个项目已施加蓝图 Buff · 跨项目行动项一览</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 跨项目进行中任务 */}
|
{/* 跨项目进行中任务 */}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
// GitHub OAuth 配置已移至侧边栏「应用设置」
|
// GitHub OAuth 配置已移至侧边栏「应用设置」
|
||||||
import { useUIStore } from "../../store/ui";
|
import { useUIStore } from "../../store/ui";
|
||||||
import { Dialog } from "../ui/Dialog";
|
import { Dialog } from "../ui/Dialog";
|
||||||
|
import { GiteaInstancesSection } from "../dashboard/GiteaPanel";
|
||||||
|
|
||||||
// 编辑器条目数据结构(存为 JSON 保存到 settings 表)
|
// 编辑器条目数据结构(存为 JSON 保存到 settings 表)
|
||||||
interface EditorEntry {
|
interface EditorEntry {
|
||||||
@ -549,6 +550,7 @@ export function SettingsPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<BatchSyncSection />
|
<BatchSyncSection />
|
||||||
|
<GiteaInstancesSection />
|
||||||
<UpdateSection />
|
<UpdateSection />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user