{
  "config": {
    "argv": [
      "/opt/hostedtoolcache/node/20.20.2/x64/bin/node",
      "/home/runner/work/dr-card-web/dr-card-web/node_modules/.bin/playwright",
      "test",
      "tests/e2e/test-gate.spec.ts"
    ],
    "configFile": "/home/runner/work/dr-card-web/dr-card-web/playwright.config.ts",
    "rootDir": "/home/runner/work/dr-card-web/dr-card-web/tests/e2e",
    "failOnFlakyTests": false,
    "forbidOnly": true,
    "fullyParallel": true,
    "globalSetup": null,
    "globalTeardown": null,
    "globalTimeout": 0,
    "grep": {},
    "grepInvert": null,
    "maxFailures": 0,
    "metadata": {
      "ci": {
        "commitHref": "https://github.com/sanchuang-dev/dr-card-web/commit/7c3a337e0dcbf7c39431d3a8d3f7c55d07735666",
        "commitHash": "7c3a337e0dcbf7c39431d3a8d3f7c55d07735666",
        "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/223",
        "prTitle": "release(test): 晋级 #220 首页自然周签到",
        "prBaseHash": "cbff303792dfcc2d6eca29c81f5ce67b46a528a3",
        "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/38380467349"
      },
      "gitCommit": {
        "shortHash": "7c3a337",
        "hash": "7c3a337e0dcbf7c39431d3a8d3f7c55d07735666",
        "subject": "Merge 06d9e1064ad7079591cbb7d8d97a41d8e32cae30 into cbff303792dfcc2d6eca29c81f5ce67b46a528a3",
        "body": "Merge 06d9e1064ad7079591cbb7d8d97a41d8e32cae30 into cbff303792dfcc2d6eca29c81f5ce67b46a528a3\n",
        "author": {
          "name": "Tomz Dang",
          "email": "dangjingtao@163.com",
          "time": 1791885670000
        },
        "committer": {
          "name": "GitHub",
          "email": "noreply@github.com",
          "time": 1791885670000
        },
        "branch": "HEAD"
      },
      "gitDiff": "diff --git a/src/pages/Home.signRecords.test.tsx b/src/pages/Home.signRecords.test.tsx\nindex 0c04ec9..10e88aa 100644\n--- a/src/pages/Home.signRecords.test.tsx\n+++ b/src/pages/Home.signRecords.test.tsx\n@@ -1,8 +1,8 @@\n import { afterEach, describe, expect, it, vi } from 'vitest'\n-import { cleanup, render, screen } from '@testing-library/react'\n+import { cleanup, fireEvent, render, screen } from '@testing-library/react'\n import type { ReactNode } from 'react'\n \n-const mocks = vi.hoisted(() => ({ useSignRecords: vi.fn(), navigate: vi.fn() }))\n+const mocks = vi.hoisted(() => ({ useSignRecords: vi.fn(), navigate: vi.fn(), reloadSignStatus: vi.fn() }))\n \n vi.mock('react-router-dom', async (importOriginal) => {\n   const original = await importOriginal<typeof import('react-router-dom')>()\n@@ -15,7 +15,7 @@ vi.mock('react-router-dom', async (importOriginal) => {\n vi.mock('./checkin/useCheckinFeed', () => ({ useSignRecords: mocks.useSignRecords }))\n vi.mock('./home/useHomeFeed', () => ({\n   useHomeBanners: () => ({ state: 'success', data: [] }),\n-  useSignStatus: () => ({ state: 'loading' }),\n+  useSignStatus: () => ({ state: 'loading', reload: mocks.reloadSignStatus }),\n }))\n vi.mock('../app/fixtures/useFixture', () => ({\n   useFixtureState: () => null,\n@@ -38,6 +38,8 @@ afterEach(() => {\n   cleanup()\n   mocks.useSignRecords.mockReset()\n   mocks.navigate.mockReset()\n+  mocks.reloadSignStatus.mockReset()\n+  vi.useRealTimers()\n })\n \n describe('Issue #89: home check-in scope', () => {\n@@ -48,3 +50,21 @@ describe('Issue #89: home check-in scope', () => {\n     expect(mocks.useSignRecords).toHaveBeenCalledWith('week')\n   })\n })\n+\n+describe('Issue #220: cross-day refresh', () => {\n+  it('reloads week records on returning to the visible app after the local day changes', () => {\n+    vi.useFakeTimers()\n+    vi.setSystemTime(new Date(2026, 9, 11, 23, 30))\n+    const reload = vi.fn()\n+    mocks.useSignRecords.mockReturnValue({ remote: { state: 'success', data: [] }, reload })\n+    render(<Home />)\n+\n+    vi.setSystemTime(new Date(2026, 9, 12, 8))\n+    fireEvent(document, new Event('visibilitychange'))\n+    expect(reload).toHaveBeenCalledTimes(1)\n+    expect(mocks.reloadSignStatus).toHaveBeenCalledTimes(1)\n+    fireEvent(document, new Event('visibilitychange'))\n+    expect(reload).toHaveBeenCalledTimes(1)\n+    expect(mocks.reloadSignStatus).toHaveBeenCalledTimes(1)\n+  })\n+})\ndiff --git a/src/pages/Home.tsx b/src/pages/Home.tsx\nindex cd987d5..7998359 100644\n--- a/src/pages/Home.tsx\n+++ b/src/pages/Home.tsx\n@@ -1,4 +1,4 @@\n-import { useMemo } from 'react'\n+import { useEffect, useMemo, useRef } from 'react'\n import { useNavigate, useSearchParams } from 'react-router-dom'\n import { BookOpen, ChevronLeft, ChevronRight, Gift, Heart, Search } from 'lucide-react'\n import BannerCarousel from '../components/mobile/BannerCarousel'\n@@ -46,6 +46,21 @@ export default function Home() {\n   const signStatus = useSignStatus()\n   // 首页只需要本周七日记录，不读取完整月份。\n   const signRecords = useSignRecords(SIGN_RECORDS_RANGE_WEEK)\n+  const lastSeenDay = useRef(new Date().toDateString())\n+\n+  // App 跨日恢复时同步刷新本周记录与今日签到状态，避免两者显示不同日期。\n+  useEffect(() => {\n+    const onVisible = () => {\n+      if (document.visibilityState !== 'visible') return\n+      const todayKey = new Date().toDateString()\n+      if (todayKey === lastSeenDay.current) return\n+      lastSeenDay.current = todayKey\n+      signRecords.reload()\n+      signStatus.reload()\n+    }\n+    document.addEventListener('visibilitychange', onVisible)\n+    return () => document.removeEventListener('visibilitychange', onVisible)\n+  }, [signRecords.reload, signStatus.reload])\n \n   const bannerSlides = useMemo(\n     () =>\n@@ -129,6 +144,7 @@ export default function Home() {\n           debug={debug}\n           loading={signStatus.state === 'loading' || signRecords.remote.state === 'loading'}\n           records={signRecords.remote.state === 'success' ? signRecords.remote.data : null}\n+          weekRecordsState={signRecords.remote.state}\n           signStatus={\n             signStatus.state === 'success'\n               ? {\ndiff --git a/src/pages/checkin/CheckinCalendar.test.ts b/src/pages/checkin/CheckinCalendar.test.ts\nindex 24822cb..67efe73 100644\n--- a/src/pages/checkin/CheckinCalendar.test.ts\n+++ b/src/pages/checkin/CheckinCalendar.test.ts\n@@ -1,6 +1,6 @@\n import { describe, expect, it } from 'vitest'\n \n-import { buildCheckinCalendar, buildCycleDays, resolveChallengeCompletedDays } from './components/CheckinBoard'\n+import { buildCheckinCalendar, buildCycleDays, buildRollingCycleDays, resolveChallengeCompletedDays, resolveWeeklyCompletedDays } from './components/CheckinBoard'\n \n const record = (id: number, createTime: string, status: number) => {\n   const [year, month, day] = createTime.slice(0, 10).split('-')\n@@ -169,49 +169,78 @@ describe('buildCheckinCalendar', () => {\n })\n \n \n-describe('7-day challenge model', () => {\n-  it('shows the previous six days plus today instead of future dates', () => {\n+describe('Issue #220: natural-week model', () => {\n+  it('shows Monday through Sunday with future dates, never a rolling last-seven-days window', () => {\n     const days = buildCycleDays(\n       [record(1, '2026-09-26 09:00:00', 10), record(2, '2026-09-27 09:00:00', 10)],\n-      new Date(2026, 8, 28),\n+      new Date(2026, 8, 28), // Monday\n     )\n-\n     expect(days.map((item) => item.dateKey)).toEqual([\n-      '2026-09-22',\n-      '2026-09-23',\n-      '2026-09-24',\n-      '2026-09-25',\n-      '2026-09-26',\n-      '2026-09-27',\n-      '2026-09-28',\n+      '2026-09-28', '2026-09-29', '2026-09-30',\n+      '2026-10-01', '2026-10-02', '2026-10-03', '2026-10-04',\n     ])\n-    expect(days.filter((item) => item.signed).map((item) => item.dateKey)).toEqual([\n-      '2026-09-26',\n-      '2026-09-27',\n+    expect(days[0].state).toBe('today')\n+    expect(days.slice(1).every((item) => item.state === 'upcoming')).toBe(true)\n+    expect(resolveWeeklyCompletedDays(days)).toBe(0)\n+  })\n+\n+  it('includes seven completed days on Sunday and resets the display on Monday', () => {\n+    const records = Array.from({ length: 7 }, (_, index) =>\n+      record(index + 1, `2026-10-${String(5 + index).padStart(2, '0')} 09:00:00`, 10),\n+    )\n+    const sunday = buildCycleDays(records, new Date(2026, 9, 11))\n+    expect(sunday[0].dateKey).toBe('2026-10-05')\n+    expect(sunday[6].dateKey).toBe('2026-10-11')\n+    expect(resolveWeeklyCompletedDays(sunday)).toBe(7)\n+\n+    const monday = buildCycleDays(records, new Date(2026, 9, 12))\n+    expect(monday[0].dateKey).toBe('2026-10-12')\n+    expect(monday[6].dateKey).toBe('2026-10-18')\n+    expect(resolveWeeklyCompletedDays(monday)).toBe(0)\n+  })\n+\n+  it('keeps a cross-year natural week and counts valid makeup exactly once', () => {\n+    const days = buildCycleDays(\n+      [record(1, '2026-12-31 09:00:00', 20), record(2, '2027-01-01 09:00:00', 10),\n+        record(3, '2026-12-31 18:00:00', 20)],\n+      new Date(2027, 0, 2),\n+    )\n+    expect(days.map((item) => item.dateKey)).toEqual([\n+      '2026-12-28', '2026-12-29', '2026-12-30', '2026-12-31',\n+      '2027-01-01', '2027-01-02', '2027-01-03',\n     ])\n-    expect(days[6].state).toBe('today')\n-  })\n-\n-  it('uses current consecutive-day truth and adjusts the unsigned projected value', () => {\n-    const days = buildCycleDays([], new Date(2026, 8, 28))\n-\n-    expect(\n-      resolveChallengeCompletedDays(\n-        { signed: true, consecutiveDays: 5, points: 5, rewardDesc: '' },\n-        days,\n-      ),\n-    ).toBe(5)\n-    expect(\n-      resolveChallengeCompletedDays(\n-        { signed: false, consecutiveDays: 5, points: 5, rewardDesc: '连续签到5天' },\n-        days,\n-      ),\n-    ).toBe(4)\n-    expect(\n-      resolveChallengeCompletedDays(\n-        { signed: true, consecutiveDays: 99, points: 5, rewardDesc: '' },\n-        days,\n-      ),\n-    ).toBe(7)\n+    expect(days[3].makeup).toBe(true)\n+    expect(resolveWeeklyCompletedDays(days)).toBe(2)\n+  })\n+\n+  it('ignores future records and includes an optimistic makeup only for a past day', () => {\n+    const days = buildCycleDays(\n+      [record(1, '2026-10-17 09:00:00', 10)],\n+      new Date(2026, 9, 14),\n+      ['2026-10-13', '2026-10-16'],\n+    )\n+    expect(days[1].makeupApplied).toBe(true)\n+    expect(days[4].state).toBe('upcoming')\n+    expect(days[5].signed).toBe(false)\n+    expect(resolveWeeklyCompletedDays(days)).toBe(1)\n+  })\n+\n+  it('does not mistake a 20-day account streak for natural-week progress', () => {\n+    const days = buildCycleDays([record(1, '2026-10-12 09:00:00', 10)], new Date(2026, 9, 14))\n+    expect(resolveWeeklyCompletedDays(days)).toBe(1)\n+    // Old streak resolver remains exclusive to the existing /checkin Hero until #221.\n+    expect(resolveChallengeCompletedDays({ signed: true, consecutiveDays: 20, points: 5, rewardDesc: '' }, days)).toBe(7)\n+  })\n+})\n+\n+describe('Issue #220: keep detail-page fallback unchanged', () => {\n+  it('still shows the prior rolling 7-day streak on Monday if status is unavailable', () => {\n+    const today = new Date(2026, 9, 12)\n+    const lastSeven = Array.from({ length: 7 }, (_, index) => {\n+      const day = new Date(2026, 9, 6 + index)\n+      return record(index + 1, `${day.getFullYear()}-${String(day.getMonth() + 1).padStart(2, '0')}-${String(day.getDate()).padStart(2, '0')} 09:00:00`, 10)\n+    })\n+    expect(resolveWeeklyCompletedDays(buildCycleDays(lastSeven, today))).toBe(1)\n+    expect(resolveChallengeCompletedDays(null, buildRollingCycleDays(lastSeven, today))).toBe(7)\n   })\n })\ndiff --git a/src/pages/checkin/components/CheckinBoard.test.tsx b/src/pages/checkin/components/CheckinBoard.test.tsx\nindex 241977e..e4062c3 100644\n--- a/src/pages/checkin/components/CheckinBoard.test.tsx\n+++ b/src/pages/checkin/components/CheckinBoard.test.tsx\n@@ -1,10 +1,10 @@\n-import { afterEach, describe, expect, it } from 'vitest'\n-import { cleanup, render, screen, within } from '@testing-library/react'\n-import { MemoryRouter } from 'react-router-dom'\n+import { afterEach, describe, expect, it, vi } from 'vitest'\n+import { cleanup, fireEvent, render, screen, within } from '@testing-library/react'\n+import { MemoryRouter, Route, Routes } from 'react-router-dom'\n \n import CheckinBoard, { type HomeSignStatusView } from './CheckinBoard'\n \n-afterEach(cleanup)\n+afterEach(() => { cleanup(); vi.useRealTimers() })\n \n function renderBoard(signStatus?: HomeSignStatusView) {\n   render(\n@@ -44,3 +44,55 @@ describe('每日打卡奖励', () => {\n     expect(mission.queryByText(/\\+\\d+ 泡泡值/)).toBeNull()\n   })\n })\n+\n+describe('Issue #220: homepage natural week entry', () => {\n+  it('shows the actual week count instead of the account streak and navigates straight to the month', () => {\n+    vi.useFakeTimers()\n+    vi.setSystemTime(new Date(2026, 9, 14, 12))\n+    render(\n+      <MemoryRouter initialEntries={['/']}>\n+        <Routes>\n+          <Route path=\"/\" element={\n+            <CheckinBoard\n+              mode=\"home\"\n+              weekRecordsState=\"success\"\n+              records={[\n+                { id: 1, user_id: 3, points: 10, consecutive_days: 20, status: 10, year: '2026', month: '10', day: '12' },\n+                { id: 2, user_id: 3, points: 10, consecutive_days: 21, status: 20, year: '2026', month: '10', day: '13' },\n+              ]}\n+              signStatus={{ signed: false, consecutiveDays: 22, points: 10, rewardDesc: '' }}\n+            />\n+          } />\n+          <Route path=\"/checkin\" element={<div>自然月签到详情</div>} />\n+        </Routes>\n+      </MemoryRouter>,\n+    )\n+    const entry = screen.getByRole('button', { name: /已签到 2 \\/ 7 天，进入本月签到日历/ })\n+    expect(entry.textContent).toContain('2/7')\n+    expect(entry.textContent).not.toContain('7/7')\n+    expect(screen.getByText('周一至周日 · 10/12—10/18')).toBeTruthy()\n+    fireEvent.click(entry)\n+    expect(screen.getByText('自然月签到详情')).toBeTruthy()\n+  })\n+\n+  it('does not fabricate zero progress or weekly reward when API data is unavailable', () => {\n+    vi.useFakeTimers()\n+    vi.setSystemTime(new Date(2026, 9, 14, 12))\n+    render(\n+      <MemoryRouter>\n+        <CheckinBoard\n+          mode=\"home\"\n+          weekRecordsState=\"error\"\n+          records={null}\n+          signStatus={{ signed: true, consecutiveDays: 99, points: 10, rewardDesc: '' }}\n+        />\n+      </MemoryRouter>,\n+    )\n+    const entry = screen.getByRole('button', { name: '查看本周签到，进入本月签到日历' })\n+    expect(entry.getAttribute('data-checkin-week-state')).toBe('error')\n+    expect(entry.textContent).toContain('—/7')\n+    expect(entry.textContent).toContain('记录暂不可用')\n+    expect(entry.textContent).not.toContain('已满勤')\n+    expect(entry.textContent).toContain('周奖励以后台活动规则为准')\n+  })\n+})\ndiff --git a/src/pages/checkin/components/CheckinBoard.tsx b/src/pages/checkin/components/CheckinBoard.tsx\nindex ea410d2..1b686b7 100644\n--- a/src/pages/checkin/components/CheckinBoard.tsx\n+++ b/src/pages/checkin/components/CheckinBoard.tsx\n@@ -1,7 +1,7 @@\n import { useEffect, useMemo, useState } from 'react'\n import { useNavigate } from 'react-router-dom'\n import { CalendarCheck, CalendarDays, Check, CheckCircle2, ChevronRight, Gift, Sparkles, X } from 'lucide-react'\n-import { BottomSheet, Button, ProgressIndicator, Skeleton } from '../../../components/ui'\n+import { Button, ProgressIndicator, Skeleton } from '../../../components/ui'\n import {\n   CHECKIN_DAILY_TASK,\n   CHECKIN_RULE_STATUS,\n@@ -13,7 +13,7 @@ import checkinRitualHero from '../../../assets/brand/bubble/checkin-ritual-hero-\n import exchangePromoShampoo from '../../../assets/brand/exchange/exchange-promo-shampoo.webp'\n \n const CHECKIN_CYCLE_TARGET = 7\n-const CHECKIN_WEEK_LABELS = ['日', '一', '二', '三', '四', '五', '六'] as const\n+const CHECKIN_WEEK_LABELS = ['一', '二', '三', '四', '五', '六', '日'] as const\n const CHECKIN_MONTH_WEEK_LABELS = ['日', '一', '二', '三', '四', '五', '六'] as const\n \n /** 一个月的天数（本地时区）。 */\n@@ -128,7 +128,7 @@ export function buildCheckinCalendar(options: BuildCalendarOptions = {}): Checki\n   }\n }\n \n-/** 7 天签到挑战轨道：展示过去 6 天 + 今天，不把未来日期当成可累计进度。 */\n+/** 首页自然周：从周一到周日七天。签到和补签共用业务日期，同一天至多计一次。 */\n export function buildCycleDays(\n   records: SignRecord[] | null | undefined,\n   today: Date,\n@@ -137,17 +137,54 @@ export function buildCycleDays(\n   const dayMap = buildSignRecordDayMap(records ?? [])\n   const optimistic = new Set(optimisticMakeupDays)\n   const todayKey = toLocalDateKey(today)\n+  const daysSinceMonday = (today.getDay() + 6) % 7\n+  const monday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - daysSinceMonday)\n \n   return Array.from({ length: CHECKIN_CYCLE_TARGET }, (_, index) => {\n-    const date = new Date(today)\n-    date.setDate(today.getDate() - (CHECKIN_CYCLE_TARGET - 1 - index))\n+    // 使用本地日历构造日期，避免 UTC 与夏令时换日影响自然周边界。\n+    const date = new Date(monday.getFullYear(), monday.getMonth(), monday.getDate() + index)\n     const dateKey = toLocalDateKey(date)\n+    const record = dayMap.get(dateKey)\n+    const isOptimistic = optimistic.has(dateKey) && dateKey < todayKey\n+    const signed = dateKey <= todayKey && (Boolean(record) || isOptimistic)\n+    const state: CheckinDayState = dateKey === todayKey\n+      ? 'today'\n+      : signed\n+        ? 'done'\n+        : dateKey < todayKey\n+          ? 'makeup'\n+          : 'upcoming'\n+\n+    return {\n+      day: date.getDate(),\n+      dateKey,\n+      state,\n+      signed,\n+      makeup: signed && (record?.status === SIGN_RECORD_STATUS_MAKEUP || isOptimistic),\n+      makeupApplied: isOptimistic && !record,\n+    }\n+  })\n+}\n \n+/**\n+ * 签到详情页既有「7 天挑战」的兼容兜底：当 status 不可用时仍按过去六天＋今天显示。\n+ * 此函数只供 full 模式使用，不能用于首页自然周进度。\n+ */\n+export function buildRollingCycleDays(\n+  records: SignRecord[] | null | undefined,\n+  today: Date,\n+  optimisticMakeupDays: string[] = [],\n+): CheckinCalendarDay[] {\n+  const dayMap = buildSignRecordDayMap(records ?? [])\n+  const optimistic = new Set(optimisticMakeupDays)\n+  const todayKey = toLocalDateKey(today)\n+  return Array.from({ length: CHECKIN_CYCLE_TARGET }, (_, index) => {\n+    const date = new Date(today.getFullYear(), today.getMonth(), today.getDate() - (CHECKIN_CYCLE_TARGET - 1 - index))\n+    const dateKey = toLocalDateKey(date)\n     const record = dayMap.get(dateKey)\n     const isOptimistic = optimistic.has(dateKey)\n     const signed = Boolean(record) || isOptimistic\n     const state: CheckinDayState = dateKey === todayKey ? 'today' : signed ? 'done' : 'makeup'\n-\n     return {\n       day: date.getDate(),\n       dateKey,\n@@ -159,6 +196,11 @@ export function buildCycleDays(\n   })\n }\n \n+/** 自然周进度只由本周有效日期计算，不读取跨周期连续签到天数。 */\n+export function resolveWeeklyCompletedDays(weekDays: CheckinCalendarDay[]): number {\n+  return weekDays.filter((day) => day.signed).length\n+}\n+\n /** 是日任务进度条比例：解析夹具里既有的「1 / 1」文案，不额外引入进度字段。 */\n const CHECKIN_DAILY_TASK_PERCENT = (() => {\n   const [done, total] = CHECKIN_DAILY_TASK.progress.split('/').map((part) => Number(part.trim()))\n@@ -178,7 +220,7 @@ export interface HomeSignStatusView {\n }\n \n /**\n- * 7 天挑战累计值优先使用 status 接口的连续签到口径。\n+ * 仅为既有签到详情页的连续签到 Hero 保留旧口径；首页自然周不得调用此函数。\n  * 未签到时后端 consecutive_days 表示「今天签到后将达到的值」，因此当前值减 1。\n  */\n export function resolveChallengeCompletedDays(\n@@ -209,6 +251,8 @@ export interface CheckinBoardProps {\n   loading?: boolean\n   /** 接口签到记录（打卡日历依据）；缺省时不渲染任何已签格。 */\n   records?: SignRecord[] | null\n+  /** 首页周记录请求状态；仅 success 才可显示确定的周进度。 */\n+  weekRecordsState?: 'loading' | 'success' | 'error'\n   /** 本次会话补签乐观点亮的日期。 */\n   optimisticMakeupDays?: string[]\n   /** 点击日历中的「补签」，参数为该格本地日期 `YYYY-MM-DD`；由宿主页面执行（先过广告闸门）。 */\n@@ -239,6 +283,7 @@ export default function CheckinBoard({\n   signStatus,\n   loading = false,\n   records,\n+  weekRecordsState,\n   optimisticMakeupDays,\n   onMakeupDay,\n   onSignIn,\n@@ -246,7 +291,6 @@ export default function CheckinBoard({\n   debug = false,\n }: CheckinBoardProps) {\n   const navigate = useNavigate()\n-  const [calendarOpen, setCalendarOpen] = useState(false)\n   const [now, setNow] = useState(() => new Date())\n \n   // 「今天」按本地时间计算；跨日（含从后台恢复）时刷新一次，避免停在昨天。\n@@ -267,12 +311,16 @@ export default function CheckinBoard({\n     [records, now, optimisticMakeupDays],\n   )\n \n-  // 7 天挑战累计值以 status 接口的连续签到口径为准；接口不可用时才回退近 7 日记录。\n-  const completedDays = resolveChallengeCompletedDays(signStatus, cycleDays)\n-  const remainingDays = Math.max(0, CHECKIN_CYCLE_TARGET - completedDays)\n+  // 详情页原有 Hero 暂不纳入 #220；首页自然周进度严格使用本周记录。\n+  const completedDays = resolveChallengeCompletedDays(\n+    signStatus,\n+    mode === 'full' ? buildRollingCycleDays(records, now, optimisticMakeupDays) : cycleDays,\n+  )\n+  const weekRecordsReady = weekRecordsState === 'success' || (weekRecordsState === undefined && Array.isArray(records))\n+  const weeklyCompletedDays = weekRecordsReady ? resolveWeeklyCompletedDays(cycleDays) : null\n+  const weekLabel = `${cycleDays[0].dateKey.slice(5).replace('-', '/')}—${cycleDays[6].dateKey.slice(5).replace('-', '/')}`\n \n   const handleMakeupDay = (dateKey: string) => {\n-    setCalendarOpen(false)\n     onMakeupDay?.(dateKey)\n   }\n \n@@ -325,133 +373,88 @@ export default function CheckinBoard({\n       )}\n \n       {mode === 'home' ? (\n-        <>\n-          <button\n-            type=\"button\"\n-            onClick={() => navigate('/checkin')}\n-            aria-label={`查看 7 天签到日历，当前 ${completedDays} / ${CHECKIN_CYCLE_TARGET}`}\n-            className=\"mx-4 block w-[calc(100%-2rem)] rounded-[16px] bg-checkin-home-surface px-3.5 pb-3.5 pt-3.5 text-left shadow-bubble transition active:scale-[0.995] active:bg-surface-pressed\"\n-          >\n-            <div className=\"flex items-center justify-between gap-3\">\n-              <span className=\"flex min-w-0 items-center gap-2 text-[14px] font-semibold leading-5 text-text-primary\">\n-                <span className=\"flex h-7 w-7 flex-none items-center justify-center rounded-[8px] bg-secondary text-reward-strong\" aria-hidden>\n-                  <CalendarDays className=\"h-4 w-4\" />\n-                </span>\n-                <span>7天签到</span>\n-                <span className=\"text-[12px] font-semibold text-reward-text\">{completedDays}/{CHECKIN_CYCLE_TARGET}</span>\n+        <button\n+          type=\"button\"\n+          onClick={() => navigate('/checkin')}\n+          data-checkin-week-state={weekRecordsReady ? 'success' : weekRecordsState ?? 'loading'}\n+          aria-label={weeklyCompletedDays === null\n+            ? '查看本周签到，进入本月签到日历'\n+            : `查看本周签到，已签到 ${weeklyCompletedDays} / 7 天，进入本月签到日历`}\n+          className=\"mx-4 block w-[calc(100%-2rem)] rounded-[16px] bg-checkin-home-surface px-3.5 pb-3.5 pt-3.5 text-left shadow-bubble transition active:scale-[0.995] active:bg-surface-pressed\"\n+        >\n+          <div className=\"flex items-center justify-between gap-3\">\n+            <span className=\"flex min-w-0 items-center gap-2 text-[14px] font-semibold leading-5 text-text-primary\">\n+              <span className=\"flex h-7 w-7 flex-none items-center justify-center rounded-[8px] bg-secondary text-reward-strong\" aria-hidden>\n+                <CalendarDays className=\"h-4 w-4\" />\n               </span>\n-              <span className=\"flex flex-none items-center gap-0.5 rounded-pill bg-surface-subtle px-2.5 py-1 text-[11px] font-medium leading-4 text-text-secondary\">\n-                {remainingDays === 0 ? '已完成' : `还差 ${remainingDays} 天`}\n-                <ChevronRight className=\"h-3 w-3\" aria-hidden />\n+              <span>本周签到</span>\n+              <span className=\"text-[12px] font-semibold text-reward-text\">\n+                {weeklyCompletedDays === null ? '—/7' : `${weeklyCompletedDays}/7`}\n               </span>\n-            </div>\n+            </span>\n+            <span className=\"flex flex-none items-center gap-0.5 rounded-pill bg-surface-subtle px-2.5 py-1 text-[11px] font-medium leading-4 text-text-secondary\">\n+              {weeklyCompletedDays === null\n+                ? weekRecordsState === 'error' ? '记录暂不可用' : '记录加载中'\n+                : weeklyCompletedDays === 7 ? '本周已满勤' : `已签到 ${weeklyCompletedDays} 天`}\n+              <ChevronRight className=\"h-3 w-3\" aria-hidden />\n+            </span>\n+          </div>\n \n-            <div className=\"mt-3 grid grid-cols-7 gap-1\" aria-hidden>\n-              {cycleDays.map((item) => {\n-                const completed = item.signed\n-                return (\n-                  <span key={item.dateKey} className=\"flex min-w-0 flex-col items-center gap-1.5\">\n-                    <span className=\"text-[9px] leading-none text-checkin-home-muted\">\n-                      {CHECKIN_WEEK_LABELS[new Date(`${item.dateKey}T00:00:00`).getDay()]}\n-                    </span>\n-                    <span\n-                      className={`flex h-7 w-7 items-center justify-center text-[10px] font-semibold ${\n-                        item.state === 'today' && !completed\n-                          ? 'flex-col rounded-[10px] bg-checkin-home-today text-checkin-home-today-text shadow-sm'\n-                          : completed\n-                            ? 'flex-col rounded-full bg-checkin-home-day-surface text-reward-strong'\n+          <span className=\"mt-2 block text-[10px] text-text-tertiary\">周一至周日 · {weekLabel}</span>\n+          <div className=\"mt-3 grid grid-cols-7 gap-1\" aria-hidden>\n+            {cycleDays.map((item, index) => {\n+              const completed = weekRecordsReady && item.signed\n+              return (\n+                <span key={item.dateKey} className=\"flex min-w-0 flex-col items-center gap-1.5\">\n+                  <span className=\"text-[9px] leading-none text-checkin-home-muted\">\n+                    {CHECKIN_WEEK_LABELS[index]}\n+                  </span>\n+                  <span\n+                    className={`flex h-7 w-7 items-center justify-center text-[10px] font-semibold ${\n+                      item.state === 'today' && !completed\n+                        ? 'flex-col rounded-[10px] bg-checkin-home-today text-checkin-home-today-text shadow-sm'\n+                        : completed\n+                          ? 'flex-col rounded-full bg-checkin-home-day-surface text-reward-strong'\n+                          : item.state === 'upcoming'\n+                            ? 'rounded-full bg-checkin-home-day-surface text-text-tertiary/60'\n                             : 'rounded-full bg-checkin-home-day-surface text-checkin-home-day-text'\n-                      }`}\n-                    >\n-                      {item.state === 'today' && !completed ? (\n-                        <>\n-                          <span className=\"text-[10px] leading-3\">{item.day}</span>\n-                          <span className=\"mt-0.5 text-[8px] font-medium leading-3\">今天</span>\n-                        </>\n-                      ) : completed ? (\n-                        <>\n-                          <Check className=\"h-2.5 w-2.5\" strokeWidth={3} />\n-                          <span className=\"mt-0.5 text-[8px] leading-none\">{item.day}</span>\n-                        </>\n-                      ) : (\n-                        item.day\n-                      )}\n-                    </span>\n+                    }`}\n+                  >\n+                    {item.state === 'today' && !completed ? (\n+                      <>\n+                        <span className=\"text-[10px] leading-3\">{item.day}</span>\n+                        <span className=\"mt-0.5 text-[8px] font-medium leading-3\">今天</span>\n+                      </>\n+                    ) : completed ? (\n+                      <>\n+                        <Check className=\"h-2.5 w-2.5\" strokeWidth={3} />\n+                        <span className=\"mt-0.5 text-[8px] leading-none\">{item.day}</span>\n+                      </>\n+                    ) : (\n+                      item.day\n+                    )}\n                   </span>\n-                )\n-              })}\n-            </div>\n-\n-            {signStatus && (\n-              <span\n-                data-checkin-sign-status={signStatus.signed ? 'signed' : 'unsigned'}\n-                className={`mt-3 flex items-center gap-1 text-[11px] leading-4 ${\n-                  signStatus.signed ? 'text-reward-text' : 'text-text-tertiary'\n-                }`}\n-              >\n-                {signStatus.signed && <Check className=\"h-3 w-3\" strokeWidth={3} aria-hidden />}\n-                {buildSignStatusHint(signStatus)}\n-              </span>\n-            )}\n-          </button>\n-\n-          <BottomSheet\n-            open={calendarOpen}\n-            title=\"7 天签到日历\"\n-            onClose={() => setCalendarOpen(false)}\n-            actions={\n-              <Button variant=\"ghost\" onClick={() => setCalendarOpen(false)}>\n-                收起\n-              </Button>\n-            }\n-          >\n-            <div className=\"pb-1\" data-checkin-calendar-sheet>\n-              <div className=\"flex items-end justify-between gap-3\">\n-                <div>\n-                  <p className=\"text-[11px] font-medium tracking-[0.12em] text-reward-strong\">WEEKLY CHECK-IN</p>\n-                  <p className=\"mt-1 text-lg font-bold text-text-primary\">\n-                    {remainingDays === 0 ? '本轮 7 天挑战已完成' : `再签到 ${remainingDays} 天完成本轮`}\n-                  </p>\n-                </div>\n-                <span className=\"rounded-pill bg-reward-subtle px-2.5 py-1 text-xs font-bold text-reward-text\">\n-                  {completedDays} / {CHECKIN_CYCLE_TARGET}\n                 </span>\n-              </div>\n-\n-              <ProgressIndicator\n-                value={completedDays}\n-                max={CHECKIN_CYCLE_TARGET}\n-                label={`7 天签到挑战进度 ${completedDays} / ${CHECKIN_CYCLE_TARGET}`}\n-                className=\"mt-3 [&>div]:h-2 [&>div]:bg-surface-subtle [&>div>div]:bg-[image:var(--gradient-bubble)]\"\n-              />\n-\n-              <div className=\"mt-5 grid grid-cols-7 gap-1.5\">\n-                {cycleDays.map((item) => (\n-                  <div key={item.dateKey} className=\"min-w-0\">\n-                    <span className=\"mb-1.5 block text-center text-[10px] font-medium text-text-tertiary\">\n-                      周{CHECKIN_WEEK_LABELS[new Date(`${item.dateKey}T00:00:00`).getDay()]}\n-                    </span>\n-                    <CalendarCell item={item} onMakeup={handleMakeupDay} />\n-                  </div>\n-                ))}\n-              </div>\n+              )\n+            })}\n+          </div>\n \n-              <div className=\"mt-5 flex items-center gap-3 border-t border-border-subtle pt-4\">\n-                <span className=\"flex h-10 w-10 flex-none items-center justify-center rounded-full bg-reward-subtle text-reward-strong\" aria-hidden>\n-                  {remainingDays === 0 ? <Check className=\"h-4 w-4\" strokeWidth={3} /> : <Gift className=\"h-4 w-4\" />}\n-                </span>\n-                <div className=\"min-w-0 flex-1\">\n-                  <p className=\"text-sm font-semibold text-text-primary\">\n-                    {remainingDays === 0 ? '连续签到目标已达成' : '继续点亮，完成 7 日挑战'}\n-                  </p>\n-                  <p className=\"mt-0.5 text-[11px] leading-4 text-text-tertiary\">\n-                    {remainingDays === 0 ? '本轮签到已完成，可以继续保持节奏。' : `完整 ${calendar.monthLabel} 签到记录与补签请进入「每日打卡」内页查看。`}\n-                  </p>\n-                </div>\n-              </div>\n-            </div>\n-          </BottomSheet>\n-        </>\n+          <span className=\"mt-3 flex items-center justify-between gap-2 text-[11px] leading-4 text-text-tertiary\">\n+            <span>周奖励以后台活动规则为准</span>\n+            <span className=\"flex items-center gap-0.5 whitespace-nowrap text-reward-text\">查看月历 <ChevronRight className=\"h-3 w-3\" aria-hidden /></span>\n+          </span>\n+          {signStatus && (\n+            <span\n+              data-checkin-sign-status={signStatus.signed ? 'signed' : 'unsigned'}\n+              className={`mt-2 flex items-center gap-1 text-[11px] leading-4 ${\n+                signStatus.signed ? 'text-reward-text' : 'text-text-tertiary'\n+              }`}\n+            >\n+              {signStatus.signed && <Check className=\"h-3 w-3\" strokeWidth={3} aria-hidden />}\n+              {buildSignStatusHint(signStatus)}\n+            </span>\n+          )}\n+        </button>\n       ) : (\n         <section className=\"mx-4 mt-4 rounded-feature bg-surface px-4 pb-4 pt-4 shadow-bubble\" aria-label={`${calendar.monthLabel}签到日历`}>\n           <header className=\"flex items-center justify-between gap-3\">\ndiff --git a/src/pages/home/useHomeFeed.test.tsx b/src/pages/home/useHomeFeed.test.tsx\nnew file mode 100644\nindex 0000000..0daf245\n--- /dev/null\n+++ b/src/pages/home/useHomeFeed.test.tsx\n@@ -0,0 +1,61 @@\n+import { afterEach, describe, expect, it, vi } from 'vitest'\n+import { act, cleanup, renderHook, waitFor } from '@testing-library/react'\n+\n+const mocks = vi.hoisted(() => ({ fetchSignStatus: vi.fn() }))\n+\n+vi.mock('../../services/signrecords', () => ({\n+  fetchSignStatus: mocks.fetchSignStatus,\n+}))\n+\n+import { useSignStatus } from './useHomeFeed'\n+\n+afterEach(() => {\n+  cleanup()\n+  mocks.fetchSignStatus.mockReset()\n+})\n+\n+describe('Issue #220: home sign status cross-day refresh', () => {\n+  it('reloads the real status feed and replaces the previous day result', async () => {\n+    let resolveRefresh!: (value: {\n+      signed: boolean\n+      consecutive_days: number\n+      points: number\n+      reward_desc: string\n+    }) => void\n+    const refresh = new Promise<{\n+      signed: boolean\n+      consecutive_days: number\n+      points: number\n+      reward_desc: string\n+    }>((resolve) => { resolveRefresh = resolve })\n+    mocks.fetchSignStatus\n+      .mockResolvedValueOnce({\n+        signed: true,\n+        consecutive_days: 7,\n+        points: 10,\n+        reward_desc: '',\n+      })\n+      .mockReturnValueOnce(refresh)\n+\n+    const { result } = renderHook(() => useSignStatus())\n+\n+    await waitFor(() => {\n+      expect(result.current.state).toBe('success')\n+      if (result.current.state === 'success') expect(result.current.data.signed).toBe(true)\n+    })\n+\n+    act(() => result.current.reload())\n+    expect(result.current.state).toBe('loading')\n+\n+    await act(async () => {\n+      resolveRefresh({ signed: false, consecutive_days: 8, points: 10, reward_desc: '' })\n+      await refresh\n+    })\n+\n+    await waitFor(() => {\n+      expect(result.current.state).toBe('success')\n+      if (result.current.state === 'success') expect(result.current.data.signed).toBe(false)\n+    })\n+    expect(mocks.fetchSignStatus).toHaveBeenCalledTimes(2)\n+  })\n+})\ndiff --git a/src/pages/home/useHomeFeed.ts b/src/pages/home/useHomeFeed.ts\nindex 57a38ef..2437592 100644\n--- a/src/pages/home/useHomeFeed.ts\n+++ b/src/pages/home/useHomeFeed.ts\n@@ -1,4 +1,4 @@\n-import { useEffect, useState } from 'react'\n+import { useCallback, useEffect, useState } from 'react'\n \n import { fetchHomeBanners, type BannerItem } from '../../services/banners'\n import { fetchSignStatus, type SignStatus } from '../../services/signrecords'\n@@ -12,11 +12,19 @@ export type HomeRemoteData<T> =\n  * 首页接口数据加载（mock/api 同路径，页面不做模式分支）。\n  * 单个接口失败只落在对应区域的降级展示上，不阻塞其它模块。\n  */\n-function useRemoteData<T>(load: () => Promise<T>): HomeRemoteData<T> {\n+function useRemoteData<T>(load: () => Promise<T>): HomeRemoteData<T> & { reload: () => void } {\n   const [remote, setRemote] = useState<HomeRemoteData<T>>({ state: 'loading' })\n+  const [reloadToken, setReloadToken] = useState(0)\n+\n+  // 同步清空上一业务日的状态，避免恢复可见时旧「已签到」闪现一帧。\n+  const reload = useCallback(() => {\n+    setRemote({ state: 'loading' })\n+    setReloadToken((value) => value + 1)\n+  }, [])\n \n   useEffect(() => {\n     let active = true\n+    setRemote({ state: 'loading' })\n     void load().then(\n       (data) => {\n         if (active) setRemote({ state: 'success', data })\n@@ -33,15 +41,15 @@ function useRemoteData<T>(load: () => Promise<T>): HomeRemoteData<T> {\n     return () => {\n       active = false\n     }\n-  }, [load])\n+  }, [load, reloadToken])\n \n-  return remote\n+  return { ...remote, reload }\n }\n \n export function useHomeBanners(): HomeRemoteData<BannerItem[]> {\n   return useRemoteData(fetchHomeBanners)\n }\n \n-export function useSignStatus(): HomeRemoteData<SignStatus> {\n+export function useSignStatus(): HomeRemoteData<SignStatus> & { reload: () => void } {\n   return useRemoteData(fetchSignStatus)\n }",
      "actualWorkers": 2
    },
    "preserveOutput": "always",
    "projects": [
      {
        "outputDir": "/home/runner/work/dr-card-web/dr-card-web/test-results/artifacts",
        "repeatEach": 1,
        "retries": 1,
        "metadata": {
          "ci": {
            "commitHref": "https://github.com/sanchuang-dev/dr-card-web/commit/7c3a337e0dcbf7c39431d3a8d3f7c55d07735666",
            "commitHash": "7c3a337e0dcbf7c39431d3a8d3f7c55d07735666",
            "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/223",
            "prTitle": "release(test): 晋级 #220 首页自然周签到",
            "prBaseHash": "cbff303792dfcc2d6eca29c81f5ce67b46a528a3",
            "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/38380467349"
          },
          "gitCommit": {
            "shortHash": "7c3a337",
            "hash": "7c3a337e0dcbf7c39431d3a8d3f7c55d07735666",
            "subject": "Merge 06d9e1064ad7079591cbb7d8d97a41d8e32cae30 into cbff303792dfcc2d6eca29c81f5ce67b46a528a3",
            "body": "Merge 06d9e1064ad7079591cbb7d8d97a41d8e32cae30 into cbff303792dfcc2d6eca29c81f5ce67b46a528a3\n",
            "author": {
              "name": "Tomz Dang",
              "email": "dangjingtao@163.com",
              "time": 1791885670000
            },
            "committer": {
              "name": "GitHub",
              "email": "noreply@github.com",
              "time": 1791885670000
            },
            "branch": "HEAD"
          },
          "gitDiff": "diff --git a/src/pages/Home.signRecords.test.tsx b/src/pages/Home.signRecords.test.tsx\nindex 0c04ec9..10e88aa 100644\n--- a/src/pages/Home.signRecords.test.tsx\n+++ b/src/pages/Home.signRecords.test.tsx\n@@ -1,8 +1,8 @@\n import { afterEach, describe, expect, it, vi } from 'vitest'\n-import { cleanup, render, screen } from '@testing-library/react'\n+import { cleanup, fireEvent, render, screen } from '@testing-library/react'\n import type { ReactNode } from 'react'\n \n-const mocks = vi.hoisted(() => ({ useSignRecords: vi.fn(), navigate: vi.fn() }))\n+const mocks = vi.hoisted(() => ({ useSignRecords: vi.fn(), navigate: vi.fn(), reloadSignStatus: vi.fn() }))\n \n vi.mock('react-router-dom', async (importOriginal) => {\n   const original = await importOriginal<typeof import('react-router-dom')>()\n@@ -15,7 +15,7 @@ vi.mock('react-router-dom', async (importOriginal) => {\n vi.mock('./checkin/useCheckinFeed', () => ({ useSignRecords: mocks.useSignRecords }))\n vi.mock('./home/useHomeFeed', () => ({\n   useHomeBanners: () => ({ state: 'success', data: [] }),\n-  useSignStatus: () => ({ state: 'loading' }),\n+  useSignStatus: () => ({ state: 'loading', reload: mocks.reloadSignStatus }),\n }))\n vi.mock('../app/fixtures/useFixture', () => ({\n   useFixtureState: () => null,\n@@ -38,6 +38,8 @@ afterEach(() => {\n   cleanup()\n   mocks.useSignRecords.mockReset()\n   mocks.navigate.mockReset()\n+  mocks.reloadSignStatus.mockReset()\n+  vi.useRealTimers()\n })\n \n describe('Issue #89: home check-in scope', () => {\n@@ -48,3 +50,21 @@ describe('Issue #89: home check-in scope', () => {\n     expect(mocks.useSignRecords).toHaveBeenCalledWith('week')\n   })\n })\n+\n+describe('Issue #220: cross-day refresh', () => {\n+  it('reloads week records on returning to the visible app after the local day changes', () => {\n+    vi.useFakeTimers()\n+    vi.setSystemTime(new Date(2026, 9, 11, 23, 30))\n+    const reload = vi.fn()\n+    mocks.useSignRecords.mockReturnValue({ remote: { state: 'success', data: [] }, reload })\n+    render(<Home />)\n+\n+    vi.setSystemTime(new Date(2026, 9, 12, 8))\n+    fireEvent(document, new Event('visibilitychange'))\n+    expect(reload).toHaveBeenCalledTimes(1)\n+    expect(mocks.reloadSignStatus).toHaveBeenCalledTimes(1)\n+    fireEvent(document, new Event('visibilitychange'))\n+    expect(reload).toHaveBeenCalledTimes(1)\n+    expect(mocks.reloadSignStatus).toHaveBeenCalledTimes(1)\n+  })\n+})\ndiff --git a/src/pages/Home.tsx b/src/pages/Home.tsx\nindex cd987d5..7998359 100644\n--- a/src/pages/Home.tsx\n+++ b/src/pages/Home.tsx\n@@ -1,4 +1,4 @@\n-import { useMemo } from 'react'\n+import { useEffect, useMemo, useRef } from 'react'\n import { useNavigate, useSearchParams } from 'react-router-dom'\n import { BookOpen, ChevronLeft, ChevronRight, Gift, Heart, Search } from 'lucide-react'\n import BannerCarousel from '../components/mobile/BannerCarousel'\n@@ -46,6 +46,21 @@ export default function Home() {\n   const signStatus = useSignStatus()\n   // 首页只需要本周七日记录，不读取完整月份。\n   const signRecords = useSignRecords(SIGN_RECORDS_RANGE_WEEK)\n+  const lastSeenDay = useRef(new Date().toDateString())\n+\n+  // App 跨日恢复时同步刷新本周记录与今日签到状态，避免两者显示不同日期。\n+  useEffect(() => {\n+    const onVisible = () => {\n+      if (document.visibilityState !== 'visible') return\n+      const todayKey = new Date().toDateString()\n+      if (todayKey === lastSeenDay.current) return\n+      lastSeenDay.current = todayKey\n+      signRecords.reload()\n+      signStatus.reload()\n+    }\n+    document.addEventListener('visibilitychange', onVisible)\n+    return () => document.removeEventListener('visibilitychange', onVisible)\n+  }, [signRecords.reload, signStatus.reload])\n \n   const bannerSlides = useMemo(\n     () =>\n@@ -129,6 +144,7 @@ export default function Home() {\n           debug={debug}\n           loading={signStatus.state === 'loading' || signRecords.remote.state === 'loading'}\n           records={signRecords.remote.state === 'success' ? signRecords.remote.data : null}\n+          weekRecordsState={signRecords.remote.state}\n           signStatus={\n             signStatus.state === 'success'\n               ? {\ndiff --git a/src/pages/checkin/CheckinCalendar.test.ts b/src/pages/checkin/CheckinCalendar.test.ts\nindex 24822cb..67efe73 100644\n--- a/src/pages/checkin/CheckinCalendar.test.ts\n+++ b/src/pages/checkin/CheckinCalendar.test.ts\n@@ -1,6 +1,6 @@\n import { describe, expect, it } from 'vitest'\n \n-import { buildCheckinCalendar, buildCycleDays, resolveChallengeCompletedDays } from './components/CheckinBoard'\n+import { buildCheckinCalendar, buildCycleDays, buildRollingCycleDays, resolveChallengeCompletedDays, resolveWeeklyCompletedDays } from './components/CheckinBoard'\n \n const record = (id: number, createTime: string, status: number) => {\n   const [year, month, day] = createTime.slice(0, 10).split('-')\n@@ -169,49 +169,78 @@ describe('buildCheckinCalendar', () => {\n })\n \n \n-describe('7-day challenge model', () => {\n-  it('shows the previous six days plus today instead of future dates', () => {\n+describe('Issue #220: natural-week model', () => {\n+  it('shows Monday through Sunday with future dates, never a rolling last-seven-days window', () => {\n     const days = buildCycleDays(\n       [record(1, '2026-09-26 09:00:00', 10), record(2, '2026-09-27 09:00:00', 10)],\n-      new Date(2026, 8, 28),\n+      new Date(2026, 8, 28), // Monday\n     )\n-\n     expect(days.map((item) => item.dateKey)).toEqual([\n-      '2026-09-22',\n-      '2026-09-23',\n-      '2026-09-24',\n-      '2026-09-25',\n-      '2026-09-26',\n-      '2026-09-27',\n-      '2026-09-28',\n+      '2026-09-28', '2026-09-29', '2026-09-30',\n+      '2026-10-01', '2026-10-02', '2026-10-03', '2026-10-04',\n     ])\n-    expect(days.filter((item) => item.signed).map((item) => item.dateKey)).toEqual([\n-      '2026-09-26',\n-      '2026-09-27',\n+    expect(days[0].state).toBe('today')\n+    expect(days.slice(1).every((item) => item.state === 'upcoming')).toBe(true)\n+    expect(resolveWeeklyCompletedDays(days)).toBe(0)\n+  })\n+\n+  it('includes seven completed days on Sunday and resets the display on Monday', () => {\n+    const records = Array.from({ length: 7 }, (_, index) =>\n+      record(index + 1, `2026-10-${String(5 + index).padStart(2, '0')} 09:00:00`, 10),\n+    )\n+    const sunday = buildCycleDays(records, new Date(2026, 9, 11))\n+    expect(sunday[0].dateKey).toBe('2026-10-05')\n+    expect(sunday[6].dateKey).toBe('2026-10-11')\n+    expect(resolveWeeklyCompletedDays(sunday)).toBe(7)\n+\n+    const monday = buildCycleDays(records, new Date(2026, 9, 12))\n+    expect(monday[0].dateKey).toBe('2026-10-12')\n+    expect(monday[6].dateKey).toBe('2026-10-18')\n+    expect(resolveWeeklyCompletedDays(monday)).toBe(0)\n+  })\n+\n+  it('keeps a cross-year natural week and counts valid makeup exactly once', () => {\n+    const days = buildCycleDays(\n+      [record(1, '2026-12-31 09:00:00', 20), record(2, '2027-01-01 09:00:00', 10),\n+        record(3, '2026-12-31 18:00:00', 20)],\n+      new Date(2027, 0, 2),\n+    )\n+    expect(days.map((item) => item.dateKey)).toEqual([\n+      '2026-12-28', '2026-12-29', '2026-12-30', '2026-12-31',\n+      '2027-01-01', '2027-01-02', '2027-01-03',\n     ])\n-    expect(days[6].state).toBe('today')\n-  })\n-\n-  it('uses current consecutive-day truth and adjusts the unsigned projected value', () => {\n-    const days = buildCycleDays([], new Date(2026, 8, 28))\n-\n-    expect(\n-      resolveChallengeCompletedDays(\n-        { signed: true, consecutiveDays: 5, points: 5, rewardDesc: '' },\n-        days,\n-      ),\n-    ).toBe(5)\n-    expect(\n-      resolveChallengeCompletedDays(\n-        { signed: false, consecutiveDays: 5, points: 5, rewardDesc: '连续签到5天' },\n-        days,\n-      ),\n-    ).toBe(4)\n-    expect(\n-      resolveChallengeCompletedDays(\n-        { signed: true, consecutiveDays: 99, points: 5, rewardDesc: '' },\n-        days,\n-      ),\n-    ).toBe(7)\n+    expect(days[3].makeup).toBe(true)\n+    expect(resolveWeeklyCompletedDays(days)).toBe(2)\n+  })\n+\n+  it('ignores future records and includes an optimistic makeup only for a past day', () => {\n+    const days = buildCycleDays(\n+      [record(1, '2026-10-17 09:00:00', 10)],\n+      new Date(2026, 9, 14),\n+      ['2026-10-13', '2026-10-16'],\n+    )\n+    expect(days[1].makeupApplied).toBe(true)\n+    expect(days[4].state).toBe('upcoming')\n+    expect(days[5].signed).toBe(false)\n+    expect(resolveWeeklyCompletedDays(days)).toBe(1)\n+  })\n+\n+  it('does not mistake a 20-day account streak for natural-week progress', () => {\n+    const days = buildCycleDays([record(1, '2026-10-12 09:00:00', 10)], new Date(2026, 9, 14))\n+    expect(resolveWeeklyCompletedDays(days)).toBe(1)\n+    // Old streak resolver remains exclusive to the existing /checkin Hero until #221.\n+    expect(resolveChallengeCompletedDays({ signed: true, consecutiveDays: 20, points: 5, rewardDesc: '' }, days)).toBe(7)\n+  })\n+})\n+\n+describe('Issue #220: keep detail-page fallback unchanged', () => {\n+  it('still shows the prior rolling 7-day streak on Monday if status is unavailable', () => {\n+    const today = new Date(2026, 9, 12)\n+    const lastSeven = Array.from({ length: 7 }, (_, index) => {\n+      const day = new Date(2026, 9, 6 + index)\n+      return record(index + 1, `${day.getFullYear()}-${String(day.getMonth() + 1).padStart(2, '0')}-${String(day.getDate()).padStart(2, '0')} 09:00:00`, 10)\n+    })\n+    expect(resolveWeeklyCompletedDays(buildCycleDays(lastSeven, today))).toBe(1)\n+    expect(resolveChallengeCompletedDays(null, buildRollingCycleDays(lastSeven, today))).toBe(7)\n   })\n })\ndiff --git a/src/pages/checkin/components/CheckinBoard.test.tsx b/src/pages/checkin/components/CheckinBoard.test.tsx\nindex 241977e..e4062c3 100644\n--- a/src/pages/checkin/components/CheckinBoard.test.tsx\n+++ b/src/pages/checkin/components/CheckinBoard.test.tsx\n@@ -1,10 +1,10 @@\n-import { afterEach, describe, expect, it } from 'vitest'\n-import { cleanup, render, screen, within } from '@testing-library/react'\n-import { MemoryRouter } from 'react-router-dom'\n+import { afterEach, describe, expect, it, vi } from 'vitest'\n+import { cleanup, fireEvent, render, screen, within } from '@testing-library/react'\n+import { MemoryRouter, Route, Routes } from 'react-router-dom'\n \n import CheckinBoard, { type HomeSignStatusView } from './CheckinBoard'\n \n-afterEach(cleanup)\n+afterEach(() => { cleanup(); vi.useRealTimers() })\n \n function renderBoard(signStatus?: HomeSignStatusView) {\n   render(\n@@ -44,3 +44,55 @@ describe('每日打卡奖励', () => {\n     expect(mission.queryByText(/\\+\\d+ 泡泡值/)).toBeNull()\n   })\n })\n+\n+describe('Issue #220: homepage natural week entry', () => {\n+  it('shows the actual week count instead of the account streak and navigates straight to the month', () => {\n+    vi.useFakeTimers()\n+    vi.setSystemTime(new Date(2026, 9, 14, 12))\n+    render(\n+      <MemoryRouter initialEntries={['/']}>\n+        <Routes>\n+          <Route path=\"/\" element={\n+            <CheckinBoard\n+              mode=\"home\"\n+              weekRecordsState=\"success\"\n+              records={[\n+                { id: 1, user_id: 3, points: 10, consecutive_days: 20, status: 10, year: '2026', month: '10', day: '12' },\n+                { id: 2, user_id: 3, points: 10, consecutive_days: 21, status: 20, year: '2026', month: '10', day: '13' },\n+              ]}\n+              signStatus={{ signed: false, consecutiveDays: 22, points: 10, rewardDesc: '' }}\n+            />\n+          } />\n+          <Route path=\"/checkin\" element={<div>自然月签到详情</div>} />\n+        </Routes>\n+      </MemoryRouter>,\n+    )\n+    const entry = screen.getByRole('button', { name: /已签到 2 \\/ 7 天，进入本月签到日历/ })\n+    expect(entry.textContent).toContain('2/7')\n+    expect(entry.textContent).not.toContain('7/7')\n+    expect(screen.getByText('周一至周日 · 10/12—10/18')).toBeTruthy()\n+    fireEvent.click(entry)\n+    expect(screen.getByText('自然月签到详情')).toBeTruthy()\n+  })\n+\n+  it('does not fabricate zero progress or weekly reward when API data is unavailable', () => {\n+    vi.useFakeTimers()\n+    vi.setSystemTime(new Date(2026, 9, 14, 12))\n+    render(\n+      <MemoryRouter>\n+        <CheckinBoard\n+          mode=\"home\"\n+          weekRecordsState=\"error\"\n+          records={null}\n+          signStatus={{ signed: true, consecutiveDays: 99, points: 10, rewardDesc: '' }}\n+        />\n+      </MemoryRouter>,\n+    )\n+    const entry = screen.getByRole('button', { name: '查看本周签到，进入本月签到日历' })\n+    expect(entry.getAttribute('data-checkin-week-state')).toBe('error')\n+    expect(entry.textContent).toContain('—/7')\n+    expect(entry.textContent).toContain('记录暂不可用')\n+    expect(entry.textContent).not.toContain('已满勤')\n+    expect(entry.textContent).toContain('周奖励以后台活动规则为准')\n+  })\n+})\ndiff --git a/src/pages/checkin/components/CheckinBoard.tsx b/src/pages/checkin/components/CheckinBoard.tsx\nindex ea410d2..1b686b7 100644\n--- a/src/pages/checkin/components/CheckinBoard.tsx\n+++ b/src/pages/checkin/components/CheckinBoard.tsx\n@@ -1,7 +1,7 @@\n import { useEffect, useMemo, useState } from 'react'\n import { useNavigate } from 'react-router-dom'\n import { CalendarCheck, CalendarDays, Check, CheckCircle2, ChevronRight, Gift, Sparkles, X } from 'lucide-react'\n-import { BottomSheet, Button, ProgressIndicator, Skeleton } from '../../../components/ui'\n+import { Button, ProgressIndicator, Skeleton } from '../../../components/ui'\n import {\n   CHECKIN_DAILY_TASK,\n   CHECKIN_RULE_STATUS,\n@@ -13,7 +13,7 @@ import checkinRitualHero from '../../../assets/brand/bubble/checkin-ritual-hero-\n import exchangePromoShampoo from '../../../assets/brand/exchange/exchange-promo-shampoo.webp'\n \n const CHECKIN_CYCLE_TARGET = 7\n-const CHECKIN_WEEK_LABELS = ['日', '一', '二', '三', '四', '五', '六'] as const\n+const CHECKIN_WEEK_LABELS = ['一', '二', '三', '四', '五', '六', '日'] as const\n const CHECKIN_MONTH_WEEK_LABELS = ['日', '一', '二', '三', '四', '五', '六'] as const\n \n /** 一个月的天数（本地时区）。 */\n@@ -128,7 +128,7 @@ export function buildCheckinCalendar(options: BuildCalendarOptions = {}): Checki\n   }\n }\n \n-/** 7 天签到挑战轨道：展示过去 6 天 + 今天，不把未来日期当成可累计进度。 */\n+/** 首页自然周：从周一到周日七天。签到和补签共用业务日期，同一天至多计一次。 */\n export function buildCycleDays(\n   records: SignRecord[] | null | undefined,\n   today: Date,\n@@ -137,17 +137,54 @@ export function buildCycleDays(\n   const dayMap = buildSignRecordDayMap(records ?? [])\n   const optimistic = new Set(optimisticMakeupDays)\n   const todayKey = toLocalDateKey(today)\n+  const daysSinceMonday = (today.getDay() + 6) % 7\n+  const monday = new Date(today.getFullYear(), today.getMonth(), today.getDate() - daysSinceMonday)\n \n   return Array.from({ length: CHECKIN_CYCLE_TARGET }, (_, index) => {\n-    const date = new Date(today)\n-    date.setDate(today.getDate() - (CHECKIN_CYCLE_TARGET - 1 - index))\n+    // 使用本地日历构造日期，避免 UTC 与夏令时换日影响自然周边界。\n+    const date = new Date(monday.getFullYear(), monday.getMonth(), monday.getDate() + index)\n     const dateKey = toLocalDateKey(date)\n+    const record = dayMap.get(dateKey)\n+    const isOptimistic = optimistic.has(dateKey) && dateKey < todayKey\n+    const signed = dateKey <= todayKey && (Boolean(record) || isOptimistic)\n+    const state: CheckinDayState = dateKey === todayKey\n+      ? 'today'\n+      : signed\n+        ? 'done'\n+        : dateKey < todayKey\n+          ? 'makeup'\n+          : 'upcoming'\n+\n+    return {\n+      day: date.getDate(),\n+      dateKey,\n+      state,\n+      signed,\n+      makeup: signed && (record?.status === SIGN_RECORD_STATUS_MAKEUP || isOptimistic),\n+      makeupApplied: isOptimistic && !record,\n+    }\n+  })\n+}\n \n+/**\n+ * 签到详情页既有「7 天挑战」的兼容兜底：当 status 不可用时仍按过去六天＋今天显示。\n+ * 此函数只供 full 模式使用，不能用于首页自然周进度。\n+ */\n+export function buildRollingCycleDays(\n+  records: SignRecord[] | null | undefined,\n+  today: Date,\n+  optimisticMakeupDays: string[] = [],\n+): CheckinCalendarDay[] {\n+  const dayMap = buildSignRecordDayMap(records ?? [])\n+  const optimistic = new Set(optimisticMakeupDays)\n+  const todayKey = toLocalDateKey(today)\n+  return Array.from({ length: CHECKIN_CYCLE_TARGET }, (_, index) => {\n+    const date = new Date(today.getFullYear(), today.getMonth(), today.getDate() - (CHECKIN_CYCLE_TARGET - 1 - index))\n+    const dateKey = toLocalDateKey(date)\n     const record = dayMap.get(dateKey)\n     const isOptimistic = optimistic.has(dateKey)\n     const signed = Boolean(record) || isOptimistic\n     const state: CheckinDayState = dateKey === todayKey ? 'today' : signed ? 'done' : 'makeup'\n-\n     return {\n       day: date.getDate(),\n       dateKey,\n@@ -159,6 +196,11 @@ export function buildCycleDays(\n   })\n }\n \n+/** 自然周进度只由本周有效日期计算，不读取跨周期连续签到天数。 */\n+export function resolveWeeklyCompletedDays(weekDays: CheckinCalendarDay[]): number {\n+  return weekDays.filter((day) => day.signed).length\n+}\n+\n /** 是日任务进度条比例：解析夹具里既有的「1 / 1」文案，不额外引入进度字段。 */\n const CHECKIN_DAILY_TASK_PERCENT = (() => {\n   const [done, total] = CHECKIN_DAILY_TASK.progress.split('/').map((part) => Number(part.trim()))\n@@ -178,7 +220,7 @@ export interface HomeSignStatusView {\n }\n \n /**\n- * 7 天挑战累计值优先使用 status 接口的连续签到口径。\n+ * 仅为既有签到详情页的连续签到 Hero 保留旧口径；首页自然周不得调用此函数。\n  * 未签到时后端 consecutive_days 表示「今天签到后将达到的值」，因此当前值减 1。\n  */\n export function resolveChallengeCompletedDays(\n@@ -209,6 +251,8 @@ export interface CheckinBoardProps {\n   loading?: boolean\n   /** 接口签到记录（打卡日历依据）；缺省时不渲染任何已签格。 */\n   records?: SignRecord[] | null\n+  /** 首页周记录请求状态；仅 success 才可显示确定的周进度。 */\n+  weekRecordsState?: 'loading' | 'success' | 'error'\n   /** 本次会话补签乐观点亮的日期。 */\n   optimisticMakeupDays?: string[]\n   /** 点击日历中的「补签」，参数为该格本地日期 `YYYY-MM-DD`；由宿主页面执行（先过广告闸门）。 */\n@@ -239,6 +283,7 @@ export default function CheckinBoard({\n   signStatus,\n   loading = false,\n   records,\n+  weekRecordsState,\n   optimisticMakeupDays,\n   onMakeupDay,\n   onSignIn,\n@@ -246,7 +291,6 @@ export default function CheckinBoard({\n   debug = false,\n }: CheckinBoardProps) {\n   const navigate = useNavigate()\n-  const [calendarOpen, setCalendarOpen] = useState(false)\n   const [now, setNow] = useState(() => new Date())\n \n   // 「今天」按本地时间计算；跨日（含从后台恢复）时刷新一次，避免停在昨天。\n@@ -267,12 +311,16 @@ export default function CheckinBoard({\n     [records, now, optimisticMakeupDays],\n   )\n \n-  // 7 天挑战累计值以 status 接口的连续签到口径为准；接口不可用时才回退近 7 日记录。\n-  const completedDays = resolveChallengeCompletedDays(signStatus, cycleDays)\n-  const remainingDays = Math.max(0, CHECKIN_CYCLE_TARGET - completedDays)\n+  // 详情页原有 Hero 暂不纳入 #220；首页自然周进度严格使用本周记录。\n+  const completedDays = resolveChallengeCompletedDays(\n+    signStatus,\n+    mode === 'full' ? buildRollingCycleDays(records, now, optimisticMakeupDays) : cycleDays,\n+  )\n+  const weekRecordsReady = weekRecordsState === 'success' || (weekRecordsState === undefined && Array.isArray(records))\n+  const weeklyCompletedDays = weekRecordsReady ? resolveWeeklyCompletedDays(cycleDays) : null\n+  const weekLabel = `${cycleDays[0].dateKey.slice(5).replace('-', '/')}—${cycleDays[6].dateKey.slice(5).replace('-', '/')}`\n \n   const handleMakeupDay = (dateKey: string) => {\n-    setCalendarOpen(false)\n     onMakeupDay?.(dateKey)\n   }\n \n@@ -325,133 +373,88 @@ export default function CheckinBoard({\n       )}\n \n       {mode === 'home' ? (\n-        <>\n-          <button\n-            type=\"button\"\n-            onClick={() => navigate('/checkin')}\n-            aria-label={`查看 7 天签到日历，当前 ${completedDays} / ${CHECKIN_CYCLE_TARGET}`}\n-            className=\"mx-4 block w-[calc(100%-2rem)] rounded-[16px] bg-checkin-home-surface px-3.5 pb-3.5 pt-3.5 text-left shadow-bubble transition active:scale-[0.995] active:bg-surface-pressed\"\n-          >\n-            <div className=\"flex items-center justify-between gap-3\">\n-              <span className=\"flex min-w-0 items-center gap-2 text-[14px] font-semibold leading-5 text-text-primary\">\n-                <span className=\"flex h-7 w-7 flex-none items-center justify-center rounded-[8px] bg-secondary text-reward-strong\" aria-hidden>\n-                  <CalendarDays className=\"h-4 w-4\" />\n-                </span>\n-                <span>7天签到</span>\n-                <span className=\"text-[12px] font-semibold text-reward-text\">{completedDays}/{CHECKIN_CYCLE_TARGET}</span>\n+        <button\n+          type=\"button\"\n+          onClick={() => navigate('/checkin')}\n+          data-checkin-week-state={weekRecordsReady ? 'success' : weekRecordsState ?? 'loading'}\n+          aria-label={weeklyCompletedDays === null\n+            ? '查看本周签到，进入本月签到日历'\n+            : `查看本周签到，已签到 ${weeklyCompletedDays} / 7 天，进入本月签到日历`}\n+          className=\"mx-4 block w-[calc(100%-2rem)] rounded-[16px] bg-checkin-home-surface px-3.5 pb-3.5 pt-3.5 text-left shadow-bubble transition active:scale-[0.995] active:bg-surface-pressed\"\n+        >\n+          <div className=\"flex items-center justify-between gap-3\">\n+            <span className=\"flex min-w-0 items-center gap-2 text-[14px] font-semibold leading-5 text-text-primary\">\n+              <span className=\"flex h-7 w-7 flex-none items-center justify-center rounded-[8px] bg-secondary text-reward-strong\" aria-hidden>\n+                <CalendarDays className=\"h-4 w-4\" />\n               </span>\n-              <span className=\"flex flex-none items-center gap-0.5 rounded-pill bg-surface-subtle px-2.5 py-1 text-[11px] font-medium leading-4 text-text-secondary\">\n-                {remainingDays === 0 ? '已完成' : `还差 ${remainingDays} 天`}\n-                <ChevronRight className=\"h-3 w-3\" aria-hidden />\n+              <span>本周签到</span>\n+              <span className=\"text-[12px] font-semibold text-reward-text\">\n+                {weeklyCompletedDays === null ? '—/7' : `${weeklyCompletedDays}/7`}\n               </span>\n-            </div>\n+            </span>\n+            <span className=\"flex flex-none items-center gap-0.5 rounded-pill bg-surface-subtle px-2.5 py-1 text-[11px] font-medium leading-4 text-text-secondary\">\n+              {weeklyCompletedDays === null\n+                ? weekRecordsState === 'error' ? '记录暂不可用' : '记录加载中'\n+                : weeklyCompletedDays === 7 ? '本周已满勤' : `已签到 ${weeklyCompletedDays} 天`}\n+              <ChevronRight className=\"h-3 w-3\" aria-hidden />\n+            </span>\n+          </div>\n \n-            <div className=\"mt-3 grid grid-cols-7 gap-1\" aria-hidden>\n-              {cycleDays.map((item) => {\n-                const completed = item.signed\n-                return (\n-                  <span key={item.dateKey} className=\"flex min-w-0 flex-col items-center gap-1.5\">\n-                    <span className=\"text-[9px] leading-none text-checkin-home-muted\">\n-                      {CHECKIN_WEEK_LABELS[new Date(`${item.dateKey}T00:00:00`).getDay()]}\n-                    </span>\n-                    <span\n-                      className={`flex h-7 w-7 items-center justify-center text-[10px] font-semibold ${\n-                        item.state === 'today' && !completed\n-                          ? 'flex-col rounded-[10px] bg-checkin-home-today text-checkin-home-today-text shadow-sm'\n-                          : completed\n-                            ? 'flex-col rounded-full bg-checkin-home-day-surface text-reward-strong'\n+          <span className=\"mt-2 block text-[10px] text-text-tertiary\">周一至周日 · {weekLabel}</span>\n+          <div className=\"mt-3 grid grid-cols-7 gap-1\" aria-hidden>\n+            {cycleDays.map((item, index) => {\n+              const completed = weekRecordsReady && item.signed\n+              return (\n+                <span key={item.dateKey} className=\"flex min-w-0 flex-col items-center gap-1.5\">\n+                  <span className=\"text-[9px] leading-none text-checkin-home-muted\">\n+                    {CHECKIN_WEEK_LABELS[index]}\n+                  </span>\n+                  <span\n+                    className={`flex h-7 w-7 items-center justify-center text-[10px] font-semibold ${\n+                      item.state === 'today' && !completed\n+                        ? 'flex-col rounded-[10px] bg-checkin-home-today text-checkin-home-today-text shadow-sm'\n+                        : completed\n+                          ? 'flex-col rounded-full bg-checkin-home-day-surface text-reward-strong'\n+                          : item.state === 'upcoming'\n+                            ? 'rounded-full bg-checkin-home-day-surface text-text-tertiary/60'\n                             : 'rounded-full bg-checkin-home-day-surface text-checkin-home-day-text'\n-                      }`}\n-                    >\n-                      {item.state === 'today' && !completed ? (\n-                        <>\n-                          <span className=\"text-[10px] leading-3\">{item.day}</span>\n-                          <span className=\"mt-0.5 text-[8px] font-medium leading-3\">今天</span>\n-                        </>\n-                      ) : completed ? (\n-                        <>\n-                          <Check className=\"h-2.5 w-2.5\" strokeWidth={3} />\n-                          <span className=\"mt-0.5 text-[8px] leading-none\">{item.day}</span>\n-                        </>\n-                      ) : (\n-                        item.day\n-                      )}\n-                    </span>\n+                    }`}\n+                  >\n+                    {item.state === 'today' && !completed ? (\n+                      <>\n+                        <span className=\"text-[10px] leading-3\">{item.day}</span>\n+                        <span className=\"mt-0.5 text-[8px] font-medium leading-3\">今天</span>\n+                      </>\n+                    ) : completed ? (\n+                      <>\n+                        <Check className=\"h-2.5 w-2.5\" strokeWidth={3} />\n+                        <span className=\"mt-0.5 text-[8px] leading-none\">{item.day}</span>\n+                      </>\n+                    ) : (\n+                      item.day\n+                    )}\n                   </span>\n-                )\n-              })}\n-            </div>\n-\n-            {signStatus && (\n-              <span\n-                data-checkin-sign-status={signStatus.signed ? 'signed' : 'unsigned'}\n-                className={`mt-3 flex items-center gap-1 text-[11px] leading-4 ${\n-                  signStatus.signed ? 'text-reward-text' : 'text-text-tertiary'\n-                }`}\n-              >\n-                {signStatus.signed && <Check className=\"h-3 w-3\" strokeWidth={3} aria-hidden />}\n-                {buildSignStatusHint(signStatus)}\n-              </span>\n-            )}\n-          </button>\n-\n-          <BottomSheet\n-            open={calendarOpen}\n-            title=\"7 天签到日历\"\n-            onClose={() => setCalendarOpen(false)}\n-            actions={\n-              <Button variant=\"ghost\" onClick={() => setCalendarOpen(false)}>\n-                收起\n-              </Button>\n-            }\n-          >\n-            <div className=\"pb-1\" data-checkin-calendar-sheet>\n-              <div className=\"flex items-end justify-between gap-3\">\n-                <div>\n-                  <p className=\"text-[11px] font-medium tracking-[0.12em] text-reward-strong\">WEEKLY CHECK-IN</p>\n-                  <p className=\"mt-1 text-lg font-bold text-text-primary\">\n-                    {remainingDays === 0 ? '本轮 7 天挑战已完成' : `再签到 ${remainingDays} 天完成本轮`}\n-                  </p>\n-                </div>\n-                <span className=\"rounded-pill bg-reward-subtle px-2.5 py-1 text-xs font-bold text-reward-text\">\n-                  {completedDays} / {CHECKIN_CYCLE_TARGET}\n                 </span>\n-              </div>\n-\n-              <ProgressIndicator\n-                value={completedDays}\n-                max={CHECKIN_CYCLE_TARGET}\n-                label={`7 天签到挑战进度 ${completedDays} / ${CHECKIN_CYCLE_TARGET}`}\n-                className=\"mt-3 [&>div]:h-2 [&>div]:bg-surface-subtle [&>div>div]:bg-[image:var(--gradient-bubble)]\"\n-              />\n-\n-              <div className=\"mt-5 grid grid-cols-7 gap-1.5\">\n-                {cycleDays.map((item) => (\n-                  <div key={item.dateKey} className=\"min-w-0\">\n-                    <span className=\"mb-1.5 block text-center text-[10px] font-medium text-text-tertiary\">\n-                      周{CHECKIN_WEEK_LABELS[new Date(`${item.dateKey}T00:00:00`).getDay()]}\n-                    </span>\n-                    <CalendarCell item={item} onMakeup={handleMakeupDay} />\n-                  </div>\n-                ))}\n-              </div>\n+              )\n+            })}\n+          </div>\n \n-              <div className=\"mt-5 flex items-center gap-3 border-t border-border-subtle pt-4\">\n-                <span className=\"flex h-10 w-10 flex-none items-center justify-center rounded-full bg-reward-subtle text-reward-strong\" aria-hidden>\n-                  {remainingDays === 0 ? <Check className=\"h-4 w-4\" strokeWidth={3} /> : <Gift className=\"h-4 w-4\" />}\n-                </span>\n-                <div className=\"min-w-0 flex-1\">\n-                  <p className=\"text-sm font-semibold text-text-primary\">\n-                    {remainingDays === 0 ? '连续签到目标已达成' : '继续点亮，完成 7 日挑战'}\n-                  </p>\n-                  <p className=\"mt-0.5 text-[11px] leading-4 text-text-tertiary\">\n-                    {remainingDays === 0 ? '本轮签到已完成，可以继续保持节奏。' : `完整 ${calendar.monthLabel} 签到记录与补签请进入「每日打卡」内页查看。`}\n-                  </p>\n-                </div>\n-              </div>\n-            </div>\n-          </BottomSheet>\n-        </>\n+          <span className=\"mt-3 flex items-center justify-between gap-2 text-[11px] leading-4 text-text-tertiary\">\n+            <span>周奖励以后台活动规则为准</span>\n+            <span className=\"flex items-center gap-0.5 whitespace-nowrap text-reward-text\">查看月历 <ChevronRight className=\"h-3 w-3\" aria-hidden /></span>\n+          </span>\n+          {signStatus && (\n+            <span\n+              data-checkin-sign-status={signStatus.signed ? 'signed' : 'unsigned'}\n+              className={`mt-2 flex items-center gap-1 text-[11px] leading-4 ${\n+                signStatus.signed ? 'text-reward-text' : 'text-text-tertiary'\n+              }`}\n+            >\n+              {signStatus.signed && <Check className=\"h-3 w-3\" strokeWidth={3} aria-hidden />}\n+              {buildSignStatusHint(signStatus)}\n+            </span>\n+          )}\n+        </button>\n       ) : (\n         <section className=\"mx-4 mt-4 rounded-feature bg-surface px-4 pb-4 pt-4 shadow-bubble\" aria-label={`${calendar.monthLabel}签到日历`}>\n           <header className=\"flex items-center justify-between gap-3\">\ndiff --git a/src/pages/home/useHomeFeed.test.tsx b/src/pages/home/useHomeFeed.test.tsx\nnew file mode 100644\nindex 0000000..0daf245\n--- /dev/null\n+++ b/src/pages/home/useHomeFeed.test.tsx\n@@ -0,0 +1,61 @@\n+import { afterEach, describe, expect, it, vi } from 'vitest'\n+import { act, cleanup, renderHook, waitFor } from '@testing-library/react'\n+\n+const mocks = vi.hoisted(() => ({ fetchSignStatus: vi.fn() }))\n+\n+vi.mock('../../services/signrecords', () => ({\n+  fetchSignStatus: mocks.fetchSignStatus,\n+}))\n+\n+import { useSignStatus } from './useHomeFeed'\n+\n+afterEach(() => {\n+  cleanup()\n+  mocks.fetchSignStatus.mockReset()\n+})\n+\n+describe('Issue #220: home sign status cross-day refresh', () => {\n+  it('reloads the real status feed and replaces the previous day result', async () => {\n+    let resolveRefresh!: (value: {\n+      signed: boolean\n+      consecutive_days: number\n+      points: number\n+      reward_desc: string\n+    }) => void\n+    const refresh = new Promise<{\n+      signed: boolean\n+      consecutive_days: number\n+      points: number\n+      reward_desc: string\n+    }>((resolve) => { resolveRefresh = resolve })\n+    mocks.fetchSignStatus\n+      .mockResolvedValueOnce({\n+        signed: true,\n+        consecutive_days: 7,\n+        points: 10,\n+        reward_desc: '',\n+      })\n+      .mockReturnValueOnce(refresh)\n+\n+    const { result } = renderHook(() => useSignStatus())\n+\n+    await waitFor(() => {\n+      expect(result.current.state).toBe('success')\n+      if (result.current.state === 'success') expect(result.current.data.signed).toBe(true)\n+    })\n+\n+    act(() => result.current.reload())\n+    expect(result.current.state).toBe('loading')\n+\n+    await act(async () => {\n+      resolveRefresh({ signed: false, consecutive_days: 8, points: 10, reward_desc: '' })\n+      await refresh\n+    })\n+\n+    await waitFor(() => {\n+      expect(result.current.state).toBe('success')\n+      if (result.current.state === 'success') expect(result.current.data.signed).toBe(false)\n+    })\n+    expect(mocks.fetchSignStatus).toHaveBeenCalledTimes(2)\n+  })\n+})\ndiff --git a/src/pages/home/useHomeFeed.ts b/src/pages/home/useHomeFeed.ts\nindex 57a38ef..2437592 100644\n--- a/src/pages/home/useHomeFeed.ts\n+++ b/src/pages/home/useHomeFeed.ts\n@@ -1,4 +1,4 @@\n-import { useEffect, useState } from 'react'\n+import { useCallback, useEffect, useState } from 'react'\n \n import { fetchHomeBanners, type BannerItem } from '../../services/banners'\n import { fetchSignStatus, type SignStatus } from '../../services/signrecords'\n@@ -12,11 +12,19 @@ export type HomeRemoteData<T> =\n  * 首页接口数据加载（mock/api 同路径，页面不做模式分支）。\n  * 单个接口失败只落在对应区域的降级展示上，不阻塞其它模块。\n  */\n-function useRemoteData<T>(load: () => Promise<T>): HomeRemoteData<T> {\n+function useRemoteData<T>(load: () => Promise<T>): HomeRemoteData<T> & { reload: () => void } {\n   const [remote, setRemote] = useState<HomeRemoteData<T>>({ state: 'loading' })\n+  const [reloadToken, setReloadToken] = useState(0)\n+\n+  // 同步清空上一业务日的状态，避免恢复可见时旧「已签到」闪现一帧。\n+  const reload = useCallback(() => {\n+    setRemote({ state: 'loading' })\n+    setReloadToken((value) => value + 1)\n+  }, [])\n \n   useEffect(() => {\n     let active = true\n+    setRemote({ state: 'loading' })\n     void load().then(\n       (data) => {\n         if (active) setRemote({ state: 'success', data })\n@@ -33,15 +41,15 @@ function useRemoteData<T>(load: () => Promise<T>): HomeRemoteData<T> {\n     return () => {\n       active = false\n     }\n-  }, [load])\n+  }, [load, reloadToken])\n \n-  return remote\n+  return { ...remote, reload }\n }\n \n export function useHomeBanners(): HomeRemoteData<BannerItem[]> {\n   return useRemoteData(fetchHomeBanners)\n }\n \n-export function useSignStatus(): HomeRemoteData<SignStatus> {\n+export function useSignStatus(): HomeRemoteData<SignStatus> & { reload: () => void } {\n   return useRemoteData(fetchSignStatus)\n }",
          "actualWorkers": 2
        },
        "id": "chromium-mobile",
        "name": "chromium-mobile",
        "testDir": "/home/runner/work/dr-card-web/dr-card-web/tests/e2e",
        "testIgnore": [],
        "testMatch": [
          "**/*.@(spec|test).?(c|m)[jt]s?(x)"
        ],
        "timeout": 30000
      }
    ],
    "quiet": false,
    "reporter": [
      [
        "line",
        null
      ],
      [
        "html",
        {
          "outputFolder": "playwright-report",
          "open": "never"
        }
      ],
      [
        "json",
        {
          "outputFile": "test-results/playwright-results.json"
        }
      ]
    ],
    "reportSlowTests": {
      "max": 5,
      "threshold": 300000
    },
    "shard": null,
    "tags": [],
    "updateSnapshots": "missing",
    "updateSourceMethod": "patch",
    "version": "1.63.0",
    "workers": 2,
    "webServer": null
  },
  "suites": [
    {
      "title": "test-gate.spec.ts",
      "file": "test-gate.spec.ts",
      "column": 0,
      "line": 0,
      "specs": [
        {
          "title": "@test-gate build identity matches the production-like test policy",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 39,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-13T10:02:17.313Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-c3b6e0ce5d8c5d8734e5",
          "file": "test-gate.spec.ts",
          "line": 31,
          "column": 1
        },
        {
          "title": "@test-gate browser entry does not start the app router",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 1011,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-13T10:02:17.296Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8ed37c8041402a2a6b34",
          "file": "test-gate.spec.ts",
          "line": 40,
          "column": 1
        },
        {
          "title": "@test-gate / shows the unsupported-host notice in a browser",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 997,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-13T10:02:17.405Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-b030d57053896c93a550",
          "file": "test-gate.spec.ts",
          "line": 51,
          "column": 3
        },
        {
          "title": "@test-gate /profile shows the unsupported-host notice in a browser",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 980,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-13T10:02:21.097Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8605535a2807e17fcccc",
          "file": "test-gate.spec.ts",
          "line": 51,
          "column": 3
        },
        {
          "title": "@test-gate /settings shows the unsupported-host notice in a browser",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 856,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-13T10:02:21.111Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-4a2fdc9884c1725de4f8",
          "file": "test-gate.spec.ts",
          "line": 51,
          "column": 3
        },
        {
          "title": "@test-gate gated browser entry issues no business API request",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 983,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-13T10:02:21.981Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-09f38a2b8ed735e192d8",
          "file": "test-gate.spec.ts",
          "line": 65,
          "column": 1
        },
        {
          "title": "@test-gate injected native host bypasses the notice and starts the app",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 878,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-13T10:02:22.092Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8caf237c4d42513fff2f",
          "file": "test-gate.spec.ts",
          "line": 80,
          "column": 1
        }
      ]
    }
  ],
  "errors": [],
  "stats": {
    "startTime": "2026-10-13T10:02:16.292Z",
    "duration": 6733.857,
    "expected": 7,
    "skipped": 0,
    "unexpected": 0,
    "flaky": 0
  }
}