fix: stabilize backend prisma and lint
This commit is contained in:
@@ -33,8 +33,10 @@ const SISTER_ROLES: Record<string, string> = {
|
||||
|
||||
function toneFromText(text: string): 'default' | 'warn' | 'ok' | 'active' {
|
||||
const normalized = text.toLowerCase();
|
||||
if (/(fail|error|blocker|offline|invalid|drift)/.test(normalized)) return 'warn';
|
||||
if (/(passed|deploy|merged|restart_ok|write_ok|successful)/.test(normalized)) return 'ok';
|
||||
if (/(fail|error|blocker|offline|invalid|drift)/.test(normalized))
|
||||
return 'warn';
|
||||
if (/(passed|deploy|merged|restart_ok|write_ok|successful)/.test(normalized))
|
||||
return 'ok';
|
||||
if (/(review|qa|handoff|sync|update)/.test(normalized)) return 'active';
|
||||
return 'default';
|
||||
}
|
||||
@@ -45,18 +47,28 @@ function nodeStateFromEvidence(params: {
|
||||
name: string;
|
||||
}): 'idle' | 'active' | 'review' | 'blocked' | 'ready' {
|
||||
const { sisterStatus, activity, name } = params;
|
||||
const text = `${activity?.action ?? ''} ${activity?.detail ?? ''}`.toLowerCase();
|
||||
const text =
|
||||
`${activity?.action ?? ''} ${activity?.detail ?? ''}`.toLowerCase();
|
||||
|
||||
if (name !== 'user' && sisterStatus === 'offline') return 'blocked';
|
||||
if (/(review|qa)/.test(text)) return 'review';
|
||||
if (name === 'erang' && /(deploy|merged|ready for deploy|deployed)/.test(text)) return 'ready';
|
||||
if (
|
||||
name === 'erang' &&
|
||||
/(deploy|merged|ready for deploy|deployed)/.test(text)
|
||||
)
|
||||
return 'ready';
|
||||
if (sisterStatus === 'working') return 'active';
|
||||
if (activity) return 'ready';
|
||||
return 'idle';
|
||||
}
|
||||
|
||||
function summarizeActivity(activity?: ActivityRecord | null, fallback = '최근 이벤트 없음') {
|
||||
return activity?.detail?.trim() || activity?.action?.replace(/_/g, ' ') || fallback;
|
||||
function summarizeActivity(
|
||||
activity?: ActivityRecord | null,
|
||||
fallback = '최근 이벤트 없음',
|
||||
) {
|
||||
return (
|
||||
activity?.detail?.trim() || activity?.action?.replace(/_/g, ' ') || fallback
|
||||
);
|
||||
}
|
||||
|
||||
function parseQaSummary(content: string): string {
|
||||
@@ -65,7 +77,9 @@ function parseQaSummary(content: string): string {
|
||||
if (taskLine) return taskLine.replace(/^###\s+/, '').trim();
|
||||
const bullet = lines.find((line) => /^-\s+/.test(line));
|
||||
if (bullet) return bullet.replace(/^-\s+/, '').trim();
|
||||
return lines.find((line) => line.length > 0 && !line.startsWith('#')) ?? 'QA note';
|
||||
return (
|
||||
lines.find((line) => line.length > 0 && !line.startsWith('#')) ?? 'QA note'
|
||||
);
|
||||
}
|
||||
|
||||
function inferQaAuthor(path: string, content: string): string {
|
||||
@@ -101,7 +115,8 @@ export class DashboardService {
|
||||
]);
|
||||
|
||||
const activities = rawActivities as ActivityRecord[];
|
||||
const latestProjectActivity = activities.find((item) => item.project?.name) ?? null;
|
||||
const latestProjectActivity =
|
||||
activities.find((item) => item.project?.name) ?? null;
|
||||
const latestBySister = new Map<string, ActivityRecord>();
|
||||
for (const item of activities) {
|
||||
if (item.sister?.name && !latestBySister.has(item.sister.name)) {
|
||||
@@ -110,18 +125,29 @@ export class DashboardService {
|
||||
}
|
||||
|
||||
const focusProject = latestProjectActivity?.project?.name
|
||||
? projects.find((project) => project.name === latestProjectActivity.project?.name) ?? null
|
||||
: projects.find((project) => ['IMPLEMENT', 'QA', 'READY FOR DEPLOY'].includes(project.phase)) ?? null;
|
||||
? (projects.find(
|
||||
(project) => project.name === latestProjectActivity.project?.name,
|
||||
) ?? null)
|
||||
: (projects.find((project) =>
|
||||
['IMPLEMENT', 'QA', 'READY FOR DEPLOY'].includes(project.phase),
|
||||
) ?? null);
|
||||
|
||||
const reviewLoopCount = activities.filter((item) => /(review|qa)/i.test(`${item.action} ${item.detail ?? ''}`)).length;
|
||||
const escalationCount = projects.filter((project) => (project.blockerCount ?? 0) > 0).length;
|
||||
const reviewLoopCount = activities.filter((item) =>
|
||||
/(review|qa)/i.test(`${item.action} ${item.detail ?? ''}`),
|
||||
).length;
|
||||
const escalationCount = projects.filter(
|
||||
(project) => (project.blockerCount ?? 0) > 0,
|
||||
).length;
|
||||
|
||||
const nodes = [
|
||||
{
|
||||
id: 'user',
|
||||
label: 'User',
|
||||
role: 'Request / Approval',
|
||||
state: nodeStateFromEvidence({ name: 'user', activity: latestProjectActivity }),
|
||||
state: nodeStateFromEvidence({
|
||||
name: 'user',
|
||||
activity: latestProjectActivity,
|
||||
}),
|
||||
detail: latestProjectActivity
|
||||
? `${latestProjectActivity.project?.name ?? 'project'} · ${summarizeActivity(latestProjectActivity, '최근 프로젝트 이벤트 없음')}`
|
||||
: '명시적으로 기록된 handoff / request 로그가 아직 없어.',
|
||||
@@ -130,9 +156,16 @@ export class DashboardService {
|
||||
const activity = latestBySister.get(sister.name) ?? null;
|
||||
return {
|
||||
id: sister.name,
|
||||
label: sister.name === 'erang' ? 'Irang' : `${sister.name.slice(0, 1).toUpperCase()}${sister.name.slice(1)}`,
|
||||
label:
|
||||
sister.name === 'erang'
|
||||
? 'Irang'
|
||||
: `${sister.name.slice(0, 1).toUpperCase()}${sister.name.slice(1)}`,
|
||||
role: SISTER_ROLES[sister.name] ?? 'Unknown',
|
||||
state: nodeStateFromEvidence({ name: sister.name, sisterStatus: sister.status, activity }),
|
||||
state: nodeStateFromEvidence({
|
||||
name: sister.name,
|
||||
sisterStatus: sister.status,
|
||||
activity,
|
||||
}),
|
||||
detail: activity
|
||||
? summarizeActivity(activity)
|
||||
: sister.status === 'offline'
|
||||
@@ -147,22 +180,29 @@ export class DashboardService {
|
||||
const harnessItems = activities
|
||||
.filter((item) => item.action === 'harness_updated')
|
||||
.slice(0, 4)
|
||||
.map((item) => ({
|
||||
id: `harness-${item.id}`,
|
||||
title: item.detail ?? 'Harness updated',
|
||||
body: '관리자 harness 편집 로그에서 직접 가져온 기록이야.',
|
||||
tone: toneFromText(`${item.action} ${item.detail ?? ''}`),
|
||||
author: item.sister?.name ? this.toDisplayName(item.sister.name) : 'System',
|
||||
time: item.createdAt.toISOString(),
|
||||
category: 'harness',
|
||||
source: `activity:${item.action}`,
|
||||
} satisfies OpsBoardItem));
|
||||
.map(
|
||||
(item) =>
|
||||
({
|
||||
id: `harness-${item.id}`,
|
||||
title: item.detail ?? 'Harness updated',
|
||||
body: '관리자 harness 편집 로그에서 직접 가져온 기록이야.',
|
||||
tone: toneFromText(`${item.action} ${item.detail ?? ''}`),
|
||||
author: item.sister?.name
|
||||
? this.toDisplayName(item.sister.name)
|
||||
: 'System',
|
||||
time: item.createdAt.toISOString(),
|
||||
category: 'harness',
|
||||
source: `activity:${item.action}`,
|
||||
}) satisfies OpsBoardItem,
|
||||
);
|
||||
|
||||
const qaItems = await this.getLatestQaBoardItems(projects.map((project) => ({
|
||||
name: project.name,
|
||||
repoName: extractRepoName(project.repoUrl),
|
||||
updatedAt: project.updatedAt,
|
||||
})));
|
||||
const qaItems = await this.getLatestQaBoardItems(
|
||||
projects.map((project) => ({
|
||||
name: project.name,
|
||||
repoName: extractRepoName(project.repoUrl),
|
||||
updatedAt: project.updatedAt,
|
||||
})),
|
||||
);
|
||||
|
||||
const board = [...harnessItems, ...qaItems]
|
||||
.sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime())
|
||||
@@ -184,7 +224,10 @@ export class DashboardService {
|
||||
? `${focusProject.name} · ${focusProject.currentSprint ?? focusProject.phase}`
|
||||
: 'No active pipeline',
|
||||
focus: latestProjectActivity
|
||||
? summarizeActivity(latestProjectActivity, '최근 프로젝트 이벤트 없음')
|
||||
? summarizeActivity(
|
||||
latestProjectActivity,
|
||||
'최근 프로젝트 이벤트 없음',
|
||||
)
|
||||
: '최근 프로젝트 activity 기준으로 확정된 handoff가 아직 없어.',
|
||||
reviewLoopCount,
|
||||
escalationCount,
|
||||
@@ -195,45 +238,57 @@ export class DashboardService {
|
||||
};
|
||||
}
|
||||
|
||||
private async getLatestQaBoardItems(projects: { name: string; repoName: string; updatedAt: string }[]) {
|
||||
const docs = await Promise.all(projects.map(async (project) => {
|
||||
if (!project.repoName) return [] as OpsBoardItem[];
|
||||
private async getLatestQaBoardItems(
|
||||
projects: { name: string; repoName: string; updatedAt: string }[],
|
||||
) {
|
||||
const docs = await Promise.all(
|
||||
projects.map(async (project) => {
|
||||
if (!project.repoName) return [] as OpsBoardItem[];
|
||||
|
||||
const pathGroups = await Promise.all(
|
||||
QA_PATH_CANDIDATES.map((path) => this.gitea.getRepoTree(project.repoName, path)),
|
||||
);
|
||||
const pathGroups = await Promise.all(
|
||||
QA_PATH_CANDIDATES.map((path) =>
|
||||
this.gitea.getRepoTree(project.repoName, path),
|
||||
),
|
||||
);
|
||||
|
||||
const qaFiles = Array.from(
|
||||
new Set(
|
||||
pathGroups
|
||||
.flat()
|
||||
.filter((path) => /(?:SPRINT|HOTFIX)-\d+.*(?:review|qa).*\.md$/i.test(path)),
|
||||
),
|
||||
);
|
||||
const qaFiles = Array.from(
|
||||
new Set(
|
||||
pathGroups
|
||||
.flat()
|
||||
.filter((path) =>
|
||||
/(?:SPRINT|HOTFIX)-\d+.*(?:review|qa).*\.md$/i.test(path),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const latestPath = qaFiles.sort().at(-1);
|
||||
if (!latestPath) return [] as OpsBoardItem[];
|
||||
const latestPath = qaFiles.sort().at(-1);
|
||||
if (!latestPath) return [] as OpsBoardItem[];
|
||||
|
||||
const content = await this.gitea.getRawFile(project.repoName, latestPath);
|
||||
if (!content) return [] as OpsBoardItem[];
|
||||
const content = await this.gitea.getRawFile(
|
||||
project.repoName,
|
||||
latestPath,
|
||||
);
|
||||
if (!content) return [] as OpsBoardItem[];
|
||||
|
||||
const summary = parseQaSummary(content);
|
||||
const label = latestPath.split('/').pop()?.replace(/\.md$/i, '') ?? latestPath;
|
||||
const tone = toneFromText(content);
|
||||
const summary = parseQaSummary(content);
|
||||
const label =
|
||||
latestPath.split('/').pop()?.replace(/\.md$/i, '') ?? latestPath;
|
||||
const tone = toneFromText(content);
|
||||
|
||||
return [
|
||||
{
|
||||
id: `qa-${project.repoName}-${label}`,
|
||||
title: label,
|
||||
body: summary,
|
||||
tone,
|
||||
author: inferQaAuthor(latestPath, content),
|
||||
time: project.updatedAt,
|
||||
category: 'qa-log',
|
||||
source: `${project.repoName}:${latestPath}`,
|
||||
} satisfies OpsBoardItem,
|
||||
];
|
||||
}));
|
||||
return [
|
||||
{
|
||||
id: `qa-${project.repoName}-${label}`,
|
||||
title: label,
|
||||
body: summary,
|
||||
tone,
|
||||
author: inferQaAuthor(latestPath, content),
|
||||
time: project.updatedAt,
|
||||
category: 'qa-log',
|
||||
source: `${project.repoName}:${latestPath}`,
|
||||
} satisfies OpsBoardItem,
|
||||
];
|
||||
}),
|
||||
);
|
||||
|
||||
return docs.flat();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user