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 健康度后端——全量历史导入 + 跨项目聚合
|
### ✅ F1. Git 健康度后端——全量历史导入 + 跨项目聚合
|
||||||
- status: todo
|
- status: done
|
||||||
- complexity: M
|
- complexity: M
|
||||||
- files: src-tauri/src/commands/commit_metrics.rs, src-tauri/src/lib.rs, src/lib/commands.ts
|
- 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 聚合
|
- 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 健康度区块 + 全量导入按钮
|
### ✅ F2. 飞轮面板——Git 健康度区块 + 全量导入按钮
|
||||||
- status: todo
|
- status: done
|
||||||
- complexity: M
|
- complexity: M
|
||||||
- depends: F1
|
- depends: F1
|
||||||
- files: src/lib/commands.ts, src/components/dashboard/BlueprintModal.tsx
|
- files: src/lib/commands.ts, src/components/dashboard/BlueprintModal.tsx
|
||||||
|
|||||||
@ -43,6 +43,9 @@ import {
|
|||||||
type TemplateResult,
|
type TemplateResult,
|
||||||
type CommitStats,
|
type CommitStats,
|
||||||
type UserConventionsStatus,
|
type UserConventionsStatus,
|
||||||
|
type ProjectCommitHealth,
|
||||||
|
ingestFullGitHistory,
|
||||||
|
getProjectCommitHealth,
|
||||||
} from "../../lib/commands";
|
} from "../../lib/commands";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -1660,23 +1663,34 @@ function FlywheelPanel({ onClose }: { onClose: () => void }) {
|
|||||||
queryFn: () => getProjects(),
|
queryFn: () => getProjects(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { projectPaths, projectsMap } = useMemo(() => {
|
const { projectPaths, projectsMap, projectsWithPath } = useMemo(() => {
|
||||||
const paths = (projects ?? [])
|
const paths = (projects ?? [])
|
||||||
.map((p) => p.win_path || p.wsl_path || "")
|
.map((p) => p.win_path || p.wsl_path || "")
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
const map: Record<string, string> = {};
|
const map: Record<string, string> = {};
|
||||||
|
const withPath: Array<{ id: string; name: string; path: string }> = [];
|
||||||
for (const p of (projects ?? [])) {
|
for (const p of (projects ?? [])) {
|
||||||
const path = p.win_path || p.wsl_path || "";
|
const path = p.win_path || p.wsl_path || "";
|
||||||
if (!path) continue;
|
if (!path) continue;
|
||||||
// 用炼境项目名做 key
|
withPath.push({ id: p.id, name: p.name, path });
|
||||||
map[p.name] = path;
|
map[p.name] = path;
|
||||||
// 同时用路径末尾目录名做备用 key(usage.json 里存的是目录名)
|
|
||||||
const dirName = path.replace(/\\/g, "/").split("/").filter(Boolean).pop() ?? "";
|
const dirName = path.replace(/\\/g, "/").split("/").filter(Boolean).pop() ?? "";
|
||||||
if (dirName && !map[dirName]) map[dirName] = path;
|
if (dirName && !map[dirName]) map[dirName] = path;
|
||||||
}
|
}
|
||||||
return { projectPaths: paths, projectsMap: map };
|
return { projectPaths: paths, projectsMap: map, projectsWithPath: withPath };
|
||||||
}, [projects]);
|
}, [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>({
|
const { data: stats, isLoading } = useQuery<FlywheelStats>({
|
||||||
queryKey: ["flywheel-stats", projectPaths],
|
queryKey: ["flywheel-stats", projectPaths],
|
||||||
queryFn: () => getFlywheelStats(projectPaths),
|
queryFn: () => getFlywheelStats(projectPaths),
|
||||||
@ -1690,6 +1704,25 @@ function FlywheelPanel({ onClose }: { onClose: () => void }) {
|
|||||||
const [archiving, setArchiving] = useState<string | null>(null);
|
const [archiving, setArchiving] = useState<string | null>(null);
|
||||||
const [pendingArchive, setPendingArchive] = useState<{ path: string; name: 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 () => {
|
const handleGenerateMuhe = async () => {
|
||||||
if (!stats) return;
|
if (!stats) return;
|
||||||
setGenerating(true);
|
setGenerating(true);
|
||||||
@ -1894,6 +1927,64 @@ function FlywheelPanel({ onClose }: { onClose: () => void }) {
|
|||||||
</div>
|
</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">
|
<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-[10px] font-semibold text-gray-400 uppercase tracking-wider">梦核分析</p>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user