fix: tighten dashboard copy and freshness labels
This commit is contained in:
@@ -23,6 +23,13 @@ interface OpsBoardItem {
|
||||
source: string;
|
||||
}
|
||||
|
||||
interface FreshnessMeta {
|
||||
generatedAt: string;
|
||||
activityLatestAt: string | null;
|
||||
sistersLatestAt: string | null;
|
||||
qaDocLatestAt: string | null;
|
||||
}
|
||||
|
||||
const QA_PATH_CANDIDATES = ['.plans/qa/', '.qa/'];
|
||||
const SISTER_ROLES: Record<string, string> = {
|
||||
harang: 'Plan & Assign',
|
||||
@@ -64,7 +71,7 @@ function nodeStateFromEvidence(params: {
|
||||
|
||||
function summarizeActivity(
|
||||
activity?: ActivityRecord | null,
|
||||
fallback = '최근 이벤트 없음',
|
||||
fallback = 'NO EVENT',
|
||||
) {
|
||||
return (
|
||||
activity?.detail?.trim() || activity?.action?.replace(/_/g, ' ') || fallback
|
||||
@@ -149,8 +156,8 @@ export class DashboardService {
|
||||
activity: latestProjectActivity,
|
||||
}),
|
||||
detail: latestProjectActivity
|
||||
? `${latestProjectActivity.project?.name ?? 'project'} · ${summarizeActivity(latestProjectActivity, '최근 프로젝트 이벤트 없음')}`
|
||||
: '명시적으로 기록된 handoff / request 로그가 아직 없어.',
|
||||
? `${latestProjectActivity.project?.name ?? 'project'} · ${summarizeActivity(latestProjectActivity, 'NO EVENT')}`
|
||||
: 'NO ACTIVE HANDOFF',
|
||||
},
|
||||
...sisters.map((sister) => {
|
||||
const activity = latestBySister.get(sister.name) ?? null;
|
||||
@@ -169,10 +176,10 @@ export class DashboardService {
|
||||
detail: activity
|
||||
? summarizeActivity(activity)
|
||||
: sister.status === 'offline'
|
||||
? '최근 런타임 응답이 없어서 offline 상태야.'
|
||||
? 'RUNTIME OFFLINE'
|
||||
: sister.lastSeen
|
||||
? `최근 활동 기록은 ${new Date(sister.lastSeen).toISOString()} 체크 기준이야.`
|
||||
: '최근 활동 로그가 아직 없어.',
|
||||
? 'SNAPSHOT ONLY'
|
||||
: 'NO EVENT',
|
||||
};
|
||||
}),
|
||||
];
|
||||
@@ -184,8 +191,8 @@ export class DashboardService {
|
||||
(item) =>
|
||||
({
|
||||
id: `harness-${item.id}`,
|
||||
title: item.detail ?? 'Harness updated',
|
||||
body: '관리자 harness 편집 로그에서 직접 가져온 기록이야.',
|
||||
title: item.detail ?? 'HARNESS UPDATED',
|
||||
body: item.detail ?? 'HARNESS UPDATED',
|
||||
tone: toneFromText(`${item.action} ${item.detail ?? ''}`),
|
||||
author: item.sister?.name
|
||||
? this.toDisplayName(item.sister.name)
|
||||
@@ -208,6 +215,20 @@ export class DashboardService {
|
||||
.sort((a, b) => new Date(b.time).getTime() - new Date(a.time).getTime())
|
||||
.slice(0, 6);
|
||||
|
||||
const freshness: FreshnessMeta = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
activityLatestAt: activities[0]?.createdAt?.toISOString() ?? null,
|
||||
sistersLatestAt: sisters
|
||||
.map((sister) => sister.lastSeen)
|
||||
.filter((value): value is Date => value instanceof Date)
|
||||
.sort((a, b) => b.getTime() - a.getTime())[0]
|
||||
?.toISOString() ?? null,
|
||||
qaDocLatestAt: board
|
||||
.filter((item) => item.category === 'qa-log')
|
||||
.map((item) => item.time)
|
||||
.sort((a, b) => new Date(b).getTime() - new Date(a).getTime())[0] ?? null,
|
||||
};
|
||||
|
||||
return {
|
||||
focusProject: focusProject
|
||||
? {
|
||||
@@ -222,19 +243,17 @@ export class DashboardService {
|
||||
pipeline: {
|
||||
activeTask: focusProject
|
||||
? `${focusProject.name} · ${focusProject.currentSprint ?? focusProject.phase}`
|
||||
: 'No active pipeline',
|
||||
: 'NO ACTIVE PIPELINE',
|
||||
focus: latestProjectActivity
|
||||
? summarizeActivity(
|
||||
latestProjectActivity,
|
||||
'최근 프로젝트 이벤트 없음',
|
||||
)
|
||||
: '최근 프로젝트 activity 기준으로 확정된 handoff가 아직 없어.',
|
||||
? summarizeActivity(latestProjectActivity, 'NO EVENT')
|
||||
: 'SNAPSHOT / NO ACTIVE HANDOFF',
|
||||
reviewLoopCount,
|
||||
escalationCount,
|
||||
deployState: focusProject?.deployStatus ?? 'standby',
|
||||
deployState: focusProject?.deployStatus ?? 'STANDBY',
|
||||
nodes,
|
||||
},
|
||||
board,
|
||||
freshness,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -283,7 +302,7 @@ export class DashboardService {
|
||||
tone,
|
||||
author: inferQaAuthor(latestPath, content),
|
||||
time: project.updatedAt,
|
||||
category: 'qa-log',
|
||||
category: 'DOC-DERIVED',
|
||||
source: `${project.repoName}:${latestPath}`,
|
||||
} satisfies OpsBoardItem,
|
||||
];
|
||||
|
||||
@@ -79,6 +79,12 @@ interface DashboardOpsData {
|
||||
nodes: PipelineNode[];
|
||||
};
|
||||
board: BoardItem[];
|
||||
freshness: {
|
||||
generatedAt: string;
|
||||
activityLatestAt: string | null;
|
||||
sistersLatestAt: string | null;
|
||||
qaDocLatestAt: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
const pulse = keyframes`0%{opacity:.35}50%{opacity:.7}100%{opacity:.35}`;
|
||||
@@ -724,6 +730,18 @@ function formatTime(iso?: string | null): string {
|
||||
return date.toLocaleTimeString('ko-KR', { hour: '2-digit', minute: '2-digit', hour12: false });
|
||||
}
|
||||
|
||||
function formatSyncLabel(iso?: string | null): string {
|
||||
if (!iso) return 'NO SYNC';
|
||||
const diff = Date.now() - new Date(iso).getTime();
|
||||
if (!Number.isFinite(diff) || diff < 0) return 'SYNC UNKNOWN';
|
||||
const sec = Math.floor(diff / 1000);
|
||||
if (sec < 60) return `SYNC ${sec}s AGO`;
|
||||
const min = Math.floor(sec / 60);
|
||||
if (min < 60) return `SYNC ${min}m AGO`;
|
||||
const hr = Math.floor(min / 60);
|
||||
return `SYNC ${hr}h AGO`;
|
||||
}
|
||||
|
||||
function normalizePercent(value?: number | null): number {
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) return 0;
|
||||
return Math.max(0, Math.min(100, value));
|
||||
@@ -749,10 +767,10 @@ function buildSisterSummary(sister: SisterItem, activityItems: ActivityItem[], p
|
||||
if (latestActivity?.detail) return latestActivity.detail;
|
||||
const ownerProject = projects.find((project) => project.ownerSister === sister.name && project.phase !== 'DEPLOYED');
|
||||
if (ownerProject) return `${ownerProject.name} · ${ownerProject.phase}`;
|
||||
if (sister.status === 'offline') return '최근 런타임 응답 없음';
|
||||
if (sister.status === 'working') return 'active pipeline 처리 중';
|
||||
if (sister.status === 'online') return '새 handoff 대기 중';
|
||||
return '운영 상태 확인 중';
|
||||
if (sister.status === 'offline') return 'RUNTIME OFFLINE';
|
||||
if (sister.status === 'working') return 'ACTIVE SNAPSHOT';
|
||||
if (sister.status === 'online') return 'RUNTIME READY';
|
||||
return 'SNAPSHOT ONLY';
|
||||
}
|
||||
|
||||
function actionTone(action?: string, detail?: string | null): 'default' | 'warn' | 'ok' | 'active' {
|
||||
@@ -837,6 +855,12 @@ export default function DashboardPage() {
|
||||
}, []);
|
||||
|
||||
const derived = useMemo(() => {
|
||||
const freshness = opsData?.freshness ?? {
|
||||
generatedAt: null,
|
||||
activityLatestAt: null,
|
||||
sistersLatestAt: null,
|
||||
qaDocLatestAt: null,
|
||||
};
|
||||
const onlineCount = sisters.filter((item) => item.status === 'online' || item.status === 'working').length;
|
||||
const offlineCount = sisters.filter((item) => item.status === 'offline').length;
|
||||
const activeProject = opsData?.focusProject
|
||||
@@ -878,24 +902,24 @@ export default function DashboardPage() {
|
||||
|
||||
const infraCards = [
|
||||
{
|
||||
title: 'Gateway / Runtime',
|
||||
meta: connected ? 'Socket live · runtime heartbeat 수신 중' : 'Polling mode · websocket 연결 재시도 중',
|
||||
detail: `${onlineCount}/${Math.max(sisters.length, 4)} sisters active`,
|
||||
title: 'Runtime',
|
||||
meta: connected ? 'SOCKET / EVENT MIRRORED' : 'POLLING SNAPSHOT',
|
||||
detail: `${onlineCount}/${Math.max(sisters.length, 4)} ACTIVE · ${formatSyncLabel(freshness.sistersLatestAt)}`,
|
||||
},
|
||||
{
|
||||
title: 'Project Surface',
|
||||
meta: `${projects.length} projects tracked · ${totalSprints} sprints indexed`,
|
||||
detail: readyDeploy > 0 ? `${readyDeploy} ready for deploy` : 'deploy-ready backlog 없음',
|
||||
title: 'Projects',
|
||||
meta: `SNAPSHOT · ${projects.length} PROJECTS`,
|
||||
detail: readyDeploy > 0 ? `${readyDeploy} READY FOR DEPLOY` : 'NO DEPLOY QUEUE',
|
||||
},
|
||||
{
|
||||
title: 'Review Loop',
|
||||
meta: `${reviewProjects} QA queue · first-pass ${firstPassRate}%`,
|
||||
detail: escalationCount > 0 ? `blocker project ${escalationCount}건` : 'escalation 없음',
|
||||
title: 'QA',
|
||||
meta: `DOC-DERIVED · ${firstPassRate}%`,
|
||||
detail: escalationCount > 0 ? `${escalationCount} BLOCKER PROJECT` : 'NO BLOCKER',
|
||||
},
|
||||
{
|
||||
title: 'Activity Feed',
|
||||
meta: `${activityItems.length} recent events mirrored`,
|
||||
detail: activityItems[0]?.detail ?? activityItems[0]?.action ?? '최근 이벤트 없음',
|
||||
title: 'Events',
|
||||
meta: `EVENTS ${activityItems.length}`,
|
||||
detail: formatSyncLabel(freshness.activityLatestAt),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -916,8 +940,9 @@ export default function DashboardPage() {
|
||||
pipelineNodes,
|
||||
infraCards,
|
||||
board: opsData?.board ?? [],
|
||||
activeTask: opsData?.pipeline.activeTask ?? (activeProject ? `${activeProject.name} · ${activeProject.currentSprint ?? activeProject.phase}` : 'No active pipeline'),
|
||||
deployState: opsData?.pipeline.deployState ?? (activeProject?.deployStatus ?? 'standby'),
|
||||
activeTask: opsData?.pipeline.activeTask ?? (activeProject ? `${activeProject.name} · ${activeProject.currentSprint ?? activeProject.phase}` : 'NO ACTIVE PIPELINE'),
|
||||
deployState: opsData?.pipeline.deployState ?? (activeProject?.deployStatus ?? 'STANDBY'),
|
||||
freshness,
|
||||
};
|
||||
}, [activityItems, connected, opsData, projects, sisters]);
|
||||
|
||||
@@ -928,24 +953,22 @@ export default function DashboardPage() {
|
||||
<TopBar>
|
||||
<TopBlock>
|
||||
<div>
|
||||
<Eyebrow>Hanarang / master dashboard v3</Eyebrow>
|
||||
<Headline>지금 어디가 움직이고 있는지, 첫 화면에서 바로 읽히게 만들었어.</Headline>
|
||||
<Eyebrow>Master dashboard</Eyebrow>
|
||||
<Headline>MASTER PIPELINE BOARD</Headline>
|
||||
</div>
|
||||
<Summary>
|
||||
4자매 운영 상태, active pipeline, activity, metrics, infra, 운영 학습 자산까지 한 화면에 모았어. 장식보다 handoff 판단이 먼저 읽히게 정리했어.
|
||||
</Summary>
|
||||
<Summary>RUNTIME / EVENTS / DOCS</Summary>
|
||||
<InlineStats>
|
||||
<Chip $tone={connected ? 'active' : 'muted'}>{connected ? 'socket live' : 'polling mode'}</Chip>
|
||||
<Chip $tone="default">projects {String(projects.length).padStart(2, '0')}</Chip>
|
||||
<Chip $tone={derived.reviewProjects > 0 ? 'warning' : 'default'}>qa queue {String(derived.reviewProjects).padStart(2, '0')}</Chip>
|
||||
<Chip $tone={derived.readyDeploy > 0 ? 'active' : 'muted'}>ready deploy {String(derived.readyDeploy).padStart(2, '0')}</Chip>
|
||||
<Chip $tone={connected ? 'active' : 'muted'}>{connected ? 'socket on' : 'socket off'}</Chip>
|
||||
<Chip $tone="default">snapshot {formatSyncLabel(derived.freshness.generatedAt)}</Chip>
|
||||
<Chip $tone={derived.reviewProjects > 0 ? 'warning' : 'default'}>qa {String(derived.reviewProjects).padStart(2, '0')}</Chip>
|
||||
<Chip $tone={derived.readyDeploy > 0 ? 'active' : 'muted'}>deploy {String(derived.readyDeploy).padStart(2, '0')}</Chip>
|
||||
</InlineStats>
|
||||
</TopBlock>
|
||||
|
||||
<FocusPanel>
|
||||
<div>
|
||||
<Eyebrow>Current operating focus</Eyebrow>
|
||||
<FocusTitle>{derived.activeProject?.name ?? 'No active sprint'}</FocusTitle>
|
||||
<Eyebrow>Current focus</Eyebrow>
|
||||
<FocusTitle>{derived.activeProject?.name ?? 'NO ACTIVE SPRINT'}</FocusTitle>
|
||||
</div>
|
||||
<FocusBody>{derived.focusSummary}</FocusBody>
|
||||
<TechBar>
|
||||
@@ -954,11 +977,11 @@ export default function DashboardPage() {
|
||||
<FocusMetaGrid>
|
||||
<MetaCard>
|
||||
<MetaLabel>current sprint</MetaLabel>
|
||||
<MetaValue>{derived.activeProject?.currentSprint ?? 'backlog watch'}</MetaValue>
|
||||
<MetaValue>{derived.activeProject?.currentSprint ?? 'SNAPSHOT'}</MetaValue>
|
||||
</MetaCard>
|
||||
<MetaCard>
|
||||
<MetaLabel>phase</MetaLabel>
|
||||
<MetaValue>{derived.activeProject?.phase ?? 'monitoring'}</MetaValue>
|
||||
<MetaValue>{derived.activeProject?.phase ?? 'SNAPSHOT'}</MetaValue>
|
||||
</MetaCard>
|
||||
<MetaCard>
|
||||
<MetaLabel>sprint progress</MetaLabel>
|
||||
@@ -966,7 +989,7 @@ export default function DashboardPage() {
|
||||
</MetaCard>
|
||||
<MetaCard>
|
||||
<MetaLabel>deploy state</MetaLabel>
|
||||
<MetaValue>{derived.activeProject?.deployStatus ?? 'no deploy signal'}</MetaValue>
|
||||
<MetaValue>{derived.activeProject?.deployStatus ?? 'NO SIGNAL'}</MetaValue>
|
||||
</MetaCard>
|
||||
</FocusMetaGrid>
|
||||
</FocusPanel>
|
||||
@@ -976,11 +999,11 @@ export default function DashboardPage() {
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
|
||||
<ConnectionDot $connected={connected} />
|
||||
<div>
|
||||
<Eyebrow>Runtime link</Eyebrow>
|
||||
<div style={{ fontSize: 14, color: 'var(--text-primary)', marginTop: 4 }}>{connected ? 'Discord / runtime connected' : 'Polling fallback active'}</div>
|
||||
<Eyebrow>Event stream</Eyebrow>
|
||||
<div style={{ fontSize: 14, color: 'var(--text-primary)', marginTop: 4 }}>{connected ? 'SOCKET ON / EVENT MIRRORED' : 'SOCKET OFF / POLLING'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<LabelMeta><span>LIVE:</span>{connected ? 'ON' : 'OFF'}</LabelMeta>
|
||||
<LabelMeta><span>SYNC:</span>{formatSyncLabel(derived.freshness.activityLatestAt)}</LabelMeta>
|
||||
</ConnectionCard>
|
||||
|
||||
<OperatorRow>
|
||||
@@ -988,7 +1011,7 @@ export default function DashboardPage() {
|
||||
<OperatorText>
|
||||
<OperatorName>{SISTER_META[derived.activeProject?.ownerSister ?? 'narang']?.label ?? '나랑이'}</OperatorName>
|
||||
<OperatorMeta>
|
||||
현재 오너 기준은 {SISTER_META[derived.activeProject?.ownerSister ?? 'narang']?.role ?? 'Implementation'}야. 최근 handoff와 phase를 같이 보고 있어.
|
||||
{SISTER_META[derived.activeProject?.ownerSister ?? 'narang']?.role ?? 'Implementation'} / SNAPSHOT OWNER
|
||||
</OperatorMeta>
|
||||
</OperatorText>
|
||||
</OperatorRow>
|
||||
@@ -1018,7 +1041,7 @@ export default function DashboardPage() {
|
||||
{loading
|
||||
? Array.from({ length: 4 }).map((_, index) => <SkeletonCard key={index} />)
|
||||
: sisters.length === 0
|
||||
? <EmptyState>자매 런타임 데이터가 아직 없어.</EmptyState>
|
||||
? <EmptyState>NO RUNTIME DATA.</EmptyState>
|
||||
: sisters.map((sister) => {
|
||||
const sisterMeta = SISTER_META[sister.name] ?? { label: sister.name, role: sister.role ?? 'unknown', accent: '#888888' };
|
||||
return (
|
||||
@@ -1080,8 +1103,8 @@ export default function DashboardPage() {
|
||||
<PanelHeader>
|
||||
<PanelTitleBlock>
|
||||
<Eyebrow>Activity feed</Eyebrow>
|
||||
<PanelTitle>최근 handoff / review / deploy 흐름</PanelTitle>
|
||||
<PanelDesc>이벤트 타입, 주체, 대상 문맥이 바로 읽히게 텍스트 로그 대신 운영 카드로 정리했어.</PanelDesc>
|
||||
<PanelTitle>EVENT MIRROR</PanelTitle>
|
||||
<PanelDesc>WEBSOCKET + POLLING SNAPSHOT</PanelDesc>
|
||||
</PanelTitleBlock>
|
||||
<LabelMeta><span>EVENT:</span>{String(activityItems.length).padStart(2, '0')}</LabelMeta>
|
||||
</PanelHeader>
|
||||
@@ -1089,7 +1112,7 @@ export default function DashboardPage() {
|
||||
{loading ? (
|
||||
<SkeletonCard />
|
||||
) : activityItems.length === 0 ? (
|
||||
<EmptyState>No recent activity.</EmptyState>
|
||||
<EmptyState>NO RECENT EVENT.</EmptyState>
|
||||
) : (
|
||||
<ActivityList>
|
||||
{activityItems.slice(0, 6).map((item) => (
|
||||
@@ -1117,8 +1140,8 @@ export default function DashboardPage() {
|
||||
<PanelHeader>
|
||||
<PanelTitleBlock>
|
||||
<Eyebrow>Sprint metrics</Eyebrow>
|
||||
<PanelTitle>정량 지표로 지금 속도를 확인</PanelTitle>
|
||||
<PanelDesc>fake 숫자처럼 보이지 않게 프로젝트/QA/activity에서 바로 읽을 수 있는 값만 남겼어.</PanelDesc>
|
||||
<PanelTitle>SPRINT METRICS</PanelTitle>
|
||||
<PanelDesc>POLLING SNAPSHOT</PanelDesc>
|
||||
</PanelTitleBlock>
|
||||
<LabelMeta><span>SPRINT:</span>{String(derived.totalSprints).padStart(2, '0')}</LabelMeta>
|
||||
</PanelHeader>
|
||||
@@ -1127,22 +1150,22 @@ export default function DashboardPage() {
|
||||
<MetricCard>
|
||||
<MetaLabel>total tasks view</MetaLabel>
|
||||
<MetricValue>{derived.totalSprints}</MetricValue>
|
||||
<MetricHelp>현재 인덱싱된 sprint 기준 총 작업량이야.</MetricHelp>
|
||||
<MetricHelp>INDEXED SPRINT COUNT</MetricHelp>
|
||||
</MetricCard>
|
||||
<MetricCard>
|
||||
<MetaLabel>done sprint</MetaLabel>
|
||||
<MetricValue>{derived.doneSprints}</MetricValue>
|
||||
<MetricHelp>완료된 sprint 누적 수치야.</MetricHelp>
|
||||
<MetricHelp>DONE SPRINTS</MetricHelp>
|
||||
</MetricCard>
|
||||
<MetricCard>
|
||||
<MetaLabel>first-pass rate</MetaLabel>
|
||||
<MetricValue>{derived.firstPassRate}%</MetricValue>
|
||||
<MetricHelp>최근 QA 결과 중 blocker 없이 통과한 비율이야.</MetricHelp>
|
||||
<MetricHelp>QA DOC DERIVED</MetricHelp>
|
||||
</MetricCard>
|
||||
<MetricCard>
|
||||
<MetaLabel>escalations</MetaLabel>
|
||||
<MetricValue>{derived.escalationCount}</MetricValue>
|
||||
<MetricHelp>blocker 존재 프로젝트 수를 escalation 신호로 잡았어.</MetricHelp>
|
||||
<MetricHelp>BLOCKER PROJECTS</MetricHelp>
|
||||
</MetricCard>
|
||||
</MetricsGrid>
|
||||
|
||||
@@ -1163,8 +1186,8 @@ export default function DashboardPage() {
|
||||
<PanelHeader>
|
||||
<PanelTitleBlock>
|
||||
<Eyebrow>Infrastructure overview</Eyebrow>
|
||||
<PanelTitle>gateway / sisters / deploy surface</PanelTitle>
|
||||
<PanelDesc>과장된 네트워크 다이어그램 대신 실제 상태 요약을 운영 맥락으로 묶었어.</PanelDesc>
|
||||
<PanelTitle>RUNTIME SURFACE</PanelTitle>
|
||||
<PanelDesc>LIVE / SNAPSHOT / DOC-DERIVED</PanelDesc>
|
||||
</PanelTitleBlock>
|
||||
<LabelMeta><span>NODE:</span>{String(sisters.length).padStart(2, '0')}</LabelMeta>
|
||||
</PanelHeader>
|
||||
@@ -1184,14 +1207,14 @@ export default function DashboardPage() {
|
||||
<PanelHeader>
|
||||
<PanelTitleBlock>
|
||||
<Eyebrow>Mistake log & harness</Eyebrow>
|
||||
<PanelTitle>실수와 운영 규칙도 홈에서 읽히게</PanelTitle>
|
||||
<PanelDesc>합성 요약 대신 QA 문서와 harness 변경 로그에서 직접 읽은 항목만 보드로 보여줘.</PanelDesc>
|
||||
<PanelTitle>RULE / LOG BOARD</PanelTitle>
|
||||
<PanelDesc>DOC-DERIVED / ACTIVITY</PanelDesc>
|
||||
</PanelTitleBlock>
|
||||
<LabelMeta><span>RULE:</span>{String(derived.board.length).padStart(2, '0')}</LabelMeta>
|
||||
</PanelHeader>
|
||||
|
||||
{derived.board.length === 0 ? (
|
||||
<EmptyState>아직 표시할 QA / harness 원본 기록이 없어.</EmptyState>
|
||||
<EmptyState>NO RULE LOG.</EmptyState>
|
||||
) : (
|
||||
<LessonList>
|
||||
{derived.board.map((item) => (
|
||||
@@ -1203,7 +1226,11 @@ export default function DashboardPage() {
|
||||
<EventTag>{formatRelativeTime(item.time)}</EventTag>
|
||||
</LessonMeta>
|
||||
<LessonBody>{item.body}</LessonBody>
|
||||
<LessonBody>Source · {item.source}</LessonBody>
|
||||
<LessonMeta>
|
||||
<EventTag>SRC</EventTag>
|
||||
<EventTag>{item.source}</EventTag>
|
||||
<EventTag>{formatTime(item.time)}</EventTag>
|
||||
</LessonMeta>
|
||||
</LessonCard>
|
||||
))}
|
||||
</LessonList>
|
||||
|
||||
@@ -64,6 +64,7 @@ const Summary = styled.p`
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
max-width: 780px;
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
const HeaderStats = styled.div`
|
||||
@@ -248,7 +249,7 @@ export default function ActivePipeline({
|
||||
<PipelineCard>
|
||||
<Header>
|
||||
<div>
|
||||
<Eyebrow>Master flow / active pipeline</Eyebrow>
|
||||
<Eyebrow>Active pipeline / snapshot</Eyebrow>
|
||||
<Title>{activeTask}</Title>
|
||||
<Summary>{focus}</Summary>
|
||||
</div>
|
||||
@@ -282,7 +283,7 @@ export default function ActivePipeline({
|
||||
<StateBadge $state={node.state}>{stateText[node.state]}</StateBadge>
|
||||
</NodeTop>
|
||||
<NodeDetail>{node.detail}</NodeDetail>
|
||||
<NodeHint>stage {String(index + 1).padStart(2, '0')}</NodeHint>
|
||||
<NodeHint>snapshot {String(index + 1).padStart(2, '0')}</NodeHint>
|
||||
</NodeCard>
|
||||
{index < nodes.length - 1 && <Connector $active={connectorActive} />}
|
||||
</NodeWrap>
|
||||
|
||||
Reference in New Issue
Block a user