feat: complete frozen MVP — period-aware stats, layout-driven UI, quick checkin

- Fix first daily record 422 (expectedVersion now accepts 0)
- Add period_window + summarize; /api/summary and /api/tasks/{id}/summary
  aggregate each metric inside its own day/week/month/lifetime window,
  reading raw facts only (never the rebuildable month archives)
- Drive frontend rendering from per-page layout JSON with a component
  registry; scope/cardinality enforced server-side, invalid layouts fall back
- Add quick checkin and duration input in H:M:S that round-trips integer seconds
- Fix heatmap/completedAt timezone drift to Asia/Shanghai
- Add server-side session TTL (30d) matching the login cookie
- Handle date.min/date.max boundaries without OverflowError
- Half-up rounding to match browser Math.round

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MobKBK
2026-08-24 07:15:50 +08:00
parent 094e03f341
commit bcc2abe623
12 changed files with 555 additions and 59 deletions

View File

@@ -17,6 +17,19 @@ ALLOWED_COMPONENTS = {
"task-settings",
}
# 每个组件允许出现的页面与区域。组件在各自区域内必须单例,避免重复 ID
# 与重复事件绑定;跨页/跨区错放(例如把 today-record 放进首页)视为非法布局。
COMPONENT_SCOPES: dict[str, tuple[tuple[str, ...], str]] = {
"task-list": (("home", "task-detail"), "sidebar"),
"overview-summary": (("home",), "main"),
"task-progress-grid": (("home",), "main"),
"task-header": (("task-detail",), "main"),
"today-record": (("task-detail",), "main"),
"metric-progress": (("task-detail",), "main"),
"activity-heatmap": (("task-detail",), "main"),
"task-settings": (("task-detail",), "main"),
}
FALLBACKS = {
"home": {
"schemaVersion": 1,
@@ -51,16 +64,24 @@ def validate_layout(layout: Any, expected_page: str) -> bool:
regions = layout.get("regions")
if not isinstance(regions, dict) or set(regions) - {"sidebar", "main"}:
return False
for items in regions.values():
for region, items in regions.items():
if not isinstance(items, list):
return False
seen: set[str] = set()
for item in items:
if not isinstance(item, dict) or set(item) - {"component", "options"}:
return False
if item.get("component") not in ALLOWED_COMPONENTS:
component = item.get("component")
if component not in ALLOWED_COMPONENTS:
return False
if "options" in item and not isinstance(item["options"], dict):
return False
pages, expected_region = COMPONENT_SCOPES[component]
if expected_page not in pages or region != expected_region:
return False
if component in seen:
return False
seen.add(component)
return True