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:
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user