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:
lanrtop 2026-07-02 14:51:43 +09:00
parent bc07dd41bf
commit bf697b5e28
15 changed files with 1031 additions and 763 deletions

View File

@ -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:

View File

@ -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

View File

@ -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 质量表、全量导入按钮)迁至飞轮 tabCONVENTIONS 版本管理与应用梦核建议迁至全局治理 tabBlueprintModal 侧板只保留单项目内容typecheck 通过 - acceptance: 洞察页 3 tab飞轮 / 健康 / 全局治理FlywheelPanel 的跨项目内容(梦核提示词、归档候选、全项目 Git 质量表、全量导入按钮)迁至飞轮 tabCONVENTIONS 版本管理与应用梦核建议迁至全局治理 tabBlueprintModal 侧板只保留单项目内容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"] queryKeytypecheck 通过 - acceptance: 实例管理区(添加/测试/删除拆为独立组件挂到设置页BlueprintModal 治理面板只保留项目级绑定与 webhook 区(无实例时提示去设置页添加);两处共用 ["gitea-instances"] queryKeytypecheck 通过
### 📋 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 整体函数级迁移(组件+子组件+查询原样搬家,仅去掉 onCloseBlueprintModal 用脚本按锚点批量切除 414 行避免手工 Edit 匹配失败;全局/项目级拆分以"操作影响范围"为判据(影响所有项目→洞察/设置,影响单项目→弹窗)
遗留风险: ① RegistryTab 在 ManagePage 中的 max-h-[70vh] 容器是估计值,长列表滚动体验待实际验证;② BlueprintModal 删除跨项目内容后侧板宽度 340px 未调整,治理面板可能显空旷
auto_applied_rules: R04
rule_effectiveness: R04=有效(本 session 改动 10+ 文件但全部在预建任务卡范围内,无漂移)

View File

@ -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 CRUDcloud_products.rs",
"cloud-products/前端 PanelCloudProductsPanel.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 · 后端聚合 commandMentorContext",
"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
}
} }
] ]
} }

View File

@ -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>

View File

@ -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">
6090
</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>
);
}

View File

@ -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>
)} )}
</> </>

View File

@ -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>
);
}

View 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">
6090
</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>
);
}

View 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>
);
}

View 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>
);
}

View File

@ -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">

View File

@ -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>

View File

@ -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>
{/* 跨项目进行中任务 */} {/* 跨项目进行中任务 */}

View File

@ -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>
); );