Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/doctoring/dashboard_customer_copy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Dashboard customer-facing status copy

## Evidence and scope

The scoped visual inspection of Naruon #1570 at
`c298e4d93dfa5dcdefeac3a9312f92804b234584` found two implementation terms
on the Today dashboard: `source-linked` and `충돌 토큰 있음`.
See [the independent visual receipt](https://github.com/ContextualWisdomLab/naruon/pull/1570#issuecomment-5560458888)
and Gap baseline 1.52. This follow-up changes existing copy, not the parent
response guards, request lifecycle, API fields, or translation infrastructure.

The pending-task card counts tasks whose status is not done. Its caption is
therefore `미완료`, without implying that the user must understand source linkage.
An ETag provides a comparison baseline, not proof of freshness, write permission,
or absence of a conflict. The existing truthy branch becomes `원본 변경 비교 가능`;
the absent branch becomes `변경 전 원본 확인 필요`. Do not say changes were checked
or writes completed merely because an ETag is present. The calendar action
remains available and provider enforcement is unchanged.

The existing dashboard source-evidence test covers both ETag states, unchanged
counts, source scope and request headers. Its first new expectation failed on
the parent text before the copy repair. Existing browser recovery cases also
assert the customer-facing labels and absence of the two internal terms.
Record fresh browser screenshots before claiming visual acceptance. This is
not delivery of all eight locales or a versioned database translation owner;
those remain separate product requirements. No static catalog or new dependency
is introduced.
12 changes: 9 additions & 3 deletions frontend/src/components/WorkspaceHome.dashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,10 @@ describe("WorkspaceHome Today dashboard", () => {
));
});

it("backs Today dashboard operating metrics with source evidence instead of fixed fixture numbers", async () => {
it.each([
["etag-home-1", "원본 변경 비교 가능"],
[null, "변경 전 원본 확인 필요"],
])("backs Today dashboard metrics with source evidence (comparison: %s)", async (sourceEtag, comparisonLabel) => {
vi.stubGlobal("matchMedia", vi.fn((query: string) => ({
matches: false,
media: query,
Expand Down Expand Up @@ -739,7 +742,7 @@ describe("WorkspaceHome Today dashboard", () => {
protocol: "caldav",
capabilities: ["read", "write"],
writeback_enabled: true,
etag: "etag-home-1",
etag: sourceEtag,
},
{
source_id: "calendar-readonly",
Expand Down Expand Up @@ -794,7 +797,10 @@ describe("WorkspaceHome Today dashboard", () => {
expect(container.textContent).toContain("2");
expect(container.textContent).toContain("1개 일정 반영 가능");
expect(container.textContent).toContain("일정 원본 1");
expect(container.textContent).toContain("충돌 토큰 있음");
expect(container.textContent).toContain(comparisonLabel);
expect(container.textContent).not.toContain("충돌 토큰");
expect(container.textContent).not.toContain("source-linked");
expect(container.querySelector('article[aria-label="대기 작업"]')?.textContent).toContain("미완료");
expect(container.textContent).toContain("프로젝트 원본");
expect(container.textContent).toContain("1개 WebDAV 폴더");
expect(container.textContent).toContain("작업 완료율");
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/WorkspaceHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ function getCalendarCapabilityLabel(capability: string) {
}

function getCalendarConflictLabel(source: CalendarWritebackSource) {
return source.etag ? '충돌 토큰 있음' : '충돌 토큰 대기';
return source.etag ? '원본 변경 비교 가능' : '변경 전 원본 확인 필요';
}

function buildCompletionRate(tasks: TaskItem[]) {
Expand Down Expand Up @@ -406,7 +406,7 @@ function StartupDashboard({ onOpenView }: { onOpenView: (view: WorkspaceStartupV
{ title: '받은 메일', value: emailUnavailable ? '오류' : emailLoading ? '-' : emails.length.toString(), diff: emailUnavailable ? '확인 필요' : unreadCount > 0 ? `+${unreadCount}` : '-', diffText: '안 읽음', icon: Inbox, color: emailUnavailable ? 'text-red-500' : 'text-primary' },
{ title: '답변 대기', value: pendingReplyUnavailable ? '오류' : pendingReplyLoading ? '-' : pendingReplyCount.toString(), diff: pendingReplyUnavailable ? '확인 필요' : pendingReplyCount > 0 ? `${pendingReplyCount}건` : '-', diffText: '보낸 메일', icon: Send, color: pendingReplyUnavailable ? 'text-red-500' : 'text-rose-500' },
{ title: '일정 원본', value: calendarSourceError ? '오류' : calendarSourceLoading ? '-' : calendarSources.length.toString(), diff: calendarSourceError ? '확인 필요' : calendarSourceLoading ? '-' : `${writableCalendarSourceCount}개`, diffText: calendarSourceError ? '원본 확인' : '반영 가능', icon: CalendarDays, color: calendarSourceError ? 'text-red-500' : 'text-blue-500' },
{ title: '대기 작업', value: taskUnavailable ? '오류' : taskLoading ? '-' : pendingTasks.length.toString(), diff: taskUnavailable ? '확인 필요' : '-', diffText: 'source-linked', icon: CheckCircle2, color: taskUnavailable ? 'text-red-500' : 'text-green-500' },
{ title: '대기 작업', value: taskUnavailable ? '오류' : taskLoading ? '-' : pendingTasks.length.toString(), diff: taskUnavailable ? '확인 필요' : '-', diffText: '미완료', icon: CheckCircle2, color: taskUnavailable ? 'text-red-500' : 'text-green-500' },
{ title: '프로젝트 원본', value: projectFolderError ? '오류' : projectFolderLoading ? '-' : projectFolders.length.toString(), diff: projectFolderError ? '확인 필요' : projectFolderLoading ? '-' : `${projectFolders.length}개`, diffText: 'WebDAV 폴더', icon: Network, color: projectFolderError ? 'text-red-500' : 'text-purple-500' },
{ title: '작업 완료율', value: taskUnavailable ? '오류' : taskLoading ? '-' : `${taskCompletionRate}%`, diff: taskUnavailable ? '확인 필요' : taskLoading ? '-' : `${completedTaskCount}/${tasks.length}`, diffText: '완료', icon: CheckCircle2, color: taskUnavailable ? 'text-red-500' : 'text-emerald-500' },
]), [
Expand Down
33 changes: 33 additions & 0 deletions frontend/tests/e2e/dashboard-branding.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,35 @@ test('renders Today dashboard pending reply lane with signed API headers', async
await page.screenshot({ path: testInfo.outputPath('today-pending-replies-mobile-scroll.png'), fullPage: false });
});

for (const comparisonState of [
{ stateName: 'present', sourceEtag: 'calendar-comparison-baseline', label: '원본 변경 비교 가능' },
{ stateName: 'absent', sourceEtag: null, label: '변경 전 원본 확인 필요' },
]) {
test(`shows customer calendar comparison copy with ${comparisonState.stateName} baseline`, async ({ page }, testInfo) => {
const pageErrors: string[] = [];
page.on('pageerror', (pageError) => pageErrors.push(pageError.message));
await mockDashboardApi(page);
await page.route('**/api/calendar/writeback-sources', (route) => route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{
source_id: 'calendar-copy-source', provider: 'Calendar', protocol: 'caldav',
capabilities: ['read', 'write'], writeback_enabled: true, etag: comparisonState.sourceEtag,
}]),
}));
await page.goto('/');
const dashboard = page.locator('section[aria-label="홈 개요"]:visible').first();
const comparisonLabel = dashboard.getByText(`일정 원본 1 · ${comparisonState.label}`, { exact: true });
await expect(comparisonLabel).toBeVisible();
await comparisonLabel.scrollIntoViewIfNeeded();
await expect(comparisonLabel).toBeInViewport();
await expect(dashboard).not.toContainText('충돌 토큰');
await expect(dashboard).not.toContainText('source-linked');
await page.screenshot({ path: testInfo.outputPath('calendar-comparison-focused.png') });
expect(pageErrors).toEqual([]);
});
}

for (const failureResponse of [
{ name: 'source request fails', status: 503, body: '{"error_code":"source_unavailable"}' },
{ name: 'source returns malformed members', status: 200, body: '{"emails":[null]}' },
Expand Down Expand Up @@ -196,6 +225,10 @@ test(`recovers the Today dashboard after a ${failureResponse.name}`, async ({ pa
const recoveryAlert = dashboard.getByRole('alert');
await expect(recoveryAlert).toContainText('업무 현황을 모두 불러오지 못했습니다.');
await expect(dashboard.getByRole('article', { name: '받은 메일' })).toContainText('오류');
await expect(dashboard.getByRole('article', { name: '대기 작업' })).toContainText('미완료');
await expect(dashboard).toContainText('원본 변경 비교 가능');
await expect(dashboard).not.toContainText('source-linked');
await expect(dashboard).not.toContainText('충돌 토큰');
await page.screenshot({ path: testInfo.outputPath('dashboard-source-unavailable.png') });

inboxAvailable = true;
Expand Down