feat(flywheel-L0): F2 飞轮面板 Git 健康度区块 + 全量导入按钮
- FlywheelPanel 新增「Git 执行质量」卡片:表格展示 commits/规范化率/返工率/TOP scope - 「全量导入」按钮对所有接入项目触发 ingest_full_git_history,完成后刷新健康度数据 - 颜色编码:规范化率 ≥80% 绿/≥50% 黄/其余红,返工率 >20% 红/>10% 黄
This commit is contained in:
parent
9c2064e3d0
commit
532e28c421
@ -110,14 +110,14 @@
|
||||
|
||||
---
|
||||
|
||||
### 📋 F1. Git 健康度后端——全量历史导入 + 跨项目聚合
|
||||
- status: todo
|
||||
### ✅ F1. Git 健康度后端——全量历史导入 + 跨项目聚合
|
||||
- status: done
|
||||
- complexity: M
|
||||
- files: src-tauri/src/commands/commit_metrics.rs, src-tauri/src/lib.rs, src/lib/commands.ts
|
||||
- acceptance: 新增 `ingest_full_git_history(project_id, project_path)` 命令(最多 500 条,INSERT OR IGNORE 幂等);新增 `get_project_commit_health(project_ids)` 命令返回各项目 conventional_rate/rework_rate/top_scopes;单元测试覆盖 ingest 幂等性和 health 聚合
|
||||
|
||||
### 📋 F2. 飞轮面板——Git 健康度区块 + 全量导入按钮
|
||||
- status: todo
|
||||
### ✅ F2. 飞轮面板——Git 健康度区块 + 全量导入按钮
|
||||
- status: done
|
||||
- complexity: M
|
||||
- depends: F1
|
||||
- files: src/lib/commands.ts, src/components/dashboard/BlueprintModal.tsx
|
||||
|
||||
@ -43,6 +43,9 @@ import {
|
||||
type TemplateResult,
|
||||
type CommitStats,
|
||||
type UserConventionsStatus,
|
||||
type ProjectCommitHealth,
|
||||
ingestFullGitHistory,
|
||||
getProjectCommitHealth,
|
||||
} from "../../lib/commands";
|
||||
|
||||
interface Props {
|
||||
@ -1660,23 +1663,34 @@ function FlywheelPanel({ onClose }: { onClose: () => void }) {
|
||||
queryFn: () => getProjects(),
|
||||
});
|
||||
|
||||
const { projectPaths, projectsMap } = useMemo(() => {
|
||||
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;
|
||||
// 用炼境项目名做 key
|
||||
withPath.push({ id: p.id, name: p.name, path });
|
||||
map[p.name] = path;
|
||||
// 同时用路径末尾目录名做备用 key(usage.json 里存的是目录名)
|
||||
const dirName = path.replace(/\\/g, "/").split("/").filter(Boolean).pop() ?? "";
|
||||
if (dirName && !map[dirName]) map[dirName] = path;
|
||||
}
|
||||
return { projectPaths: paths, projectsMap: map };
|
||||
return { projectPaths: paths, projectsMap: map, projectsWithPath: withPath };
|
||||
}, [projects]);
|
||||
|
||||
const projectIds = useMemo(() => projectsWithPath.map((p) => p.id), [projectsWithPath]);
|
||||
|
||||
const { data: commitHealth, refetch: refetchHealth } = useQuery<ProjectCommitHealth[]>({
|
||||
queryKey: ["commit-health", projectIds],
|
||||
queryFn: () => getProjectCommitHealth(projectIds),
|
||||
enabled: projectIds.length > 0,
|
||||
});
|
||||
|
||||
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),
|
||||
@ -1690,6 +1704,25 @@ function FlywheelPanel({ onClose }: { onClose: () => void }) {
|
||||
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);
|
||||
@ -1894,6 +1927,64 @@ function FlywheelPanel({ onClose }: { onClose: () => void }) {
|
||||
</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) => {
|
||||
const proj = projectsWithPath.find((p) => p.id === h.project_id);
|
||||
return (
|
||||
<tr key={h.project_id} className="hover:bg-gray-50">
|
||||
<td className="py-1 text-gray-700 truncate max-w-[80px]" title={proj?.name}>
|
||||
{proj?.name ?? h.project_id}
|
||||
</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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user