跳轉到

Time Compass — Data Flow Timing

Summary

  • Internal backend datetime baseline remains Asia/Taipei (UTC+8).
  • Frontend planner payload is now normalized to camelCase at ingress (api.js).
  • External API contract remains snake_case (/api/planner/*, /api/context/fetch).
  • Recurring master events now keep original start/end anchor timestamps.
  • 08:00 heuristic for all-day detection is removed.

Google Calendar Event Semantics

1. Backend Datetime Retention (No Early String Casting)

File: src/time_compass/integrations/google_calendar/models/models_read.py - start and end are retained as native datetime objects (with Asia/Taipei timezone) in GoogleEventRead. - Why: The frontend planner components and intermediate normalization layers (like planner_utils.py -> _to_taipei_datetime()) expect robust input processing. Eagerly casting dictionaries ({'dateTime': ...}) to strings (str({'dateTime': ...})) silently breaks the downstream parsers. - String serialization (like format_for_llm()) is strictly deferred to the final TOON encoding layer (safe_encode()) or the final API Response Payload output.

2. All-day detection (backend)

File: src/time_compass/integrations/google_calendar/models/models_read.py - start.date / end.date present in the original raw dict => all-day event. - start.dateTime / end.dateTime only => timed event. - endTimeUnspecified=true => treated as all-day by project policy.

Notes: - endTimeUnspecified => all_day=True is an explicit product decision for planner rendering consistency. - Previous 08:00 => all_day inference has been completely removed to avoid accidental mutation of midnight events.

TOON conversion alignment

File: src/time_compass/integrations/google_calendar/models/models_toon.py - TOON conversion now trusts event.all_day directly. - No secondary 08:00 fallback exists anymore.

Google Tasks Event Semantics

1. Eager String Formatting (Discrepancy)

File: src/time_compass/integrations/google_tasks/models/models_read.py - Unlike GoogleEventRead, GoogleTaskRead still immediately casts due and completed native datetime objects into strings using format_for_llm(dt) during from_dict() parsing. - Risk: If downstream modules need to perform date math on Tasks, they must re-parse the string. - Note: This is acceptable for the current architecture because Google Tasks lack precise "time" tracking (only days/midnight UTC), but it creates a structural mismatch with Calendar models.

2. All-day detection (backend)

  • GoogleTaskRead.all_day is hardcoded to True. Because Google Tasks API's due timestamp is always sent at T00:00:00.000Z (UTC Midnight) which translates to 08:00 UTC+8, no precise duration exists.

Recurrence Anchor Policy

File: src/time_compass/utils/planner_utils.py - Removed logic that re-anchored recurring master start/end to view_date. - Master events retain original source anchor timestamps. - Exceptions/cancellations are still modeled via recurring_event_id, original_start, status, and exdate aggregation.

Impact: - Frontend no longer receives rewritten master start times. - Prevents planner start-time drift introduced by synthetic re-anchoring.

Frontend Normalization & Naming

Element cache naming

Files: - src/time_compass/mcp/ui/js/core/state.js - src/time_compass/mcp/ui/js/services/api.js - src/time_compass/mcp/ui/js/features/view.js - src/time_compass/mcp/ui/js/features/zoom.js - src/time_compass/mcp/ui/js/debug/payload_test_page.js

Changes: - el renamed to elements across UI modules. - Internal helper names expanded for clarity (getFixedCalendarEvents, getDraftCalendarEvents, calculateIso8601Duration, etc.).

Ingress mapper (snake_case -> camelCase)

File: src/time_compass/mcp/ui/js/services/api.js - _validatePlannerPayload validates raw payload via Zod schema (snake_case). - normalizePlannerPayload maps to internal camelCase state: - session_id -> sessionId - calendar_events -> calendarEvents - draft_variants -> draftVariants - writable_calendars -> writableCalendars - apply_state.applied_at -> applyState.appliedAt - etc.

Outbound API request bodies stay snake_case.

FullCalendar Integration Policy

File: src/time_compass/mcp/ui/js/features/view.js - Calendar option explicitly sets timeZone: "local". - Recurring fixed events are emitted with structured recurrence fields: - rrule: { dtstart, ...ruleParts } - exdate: [...] - duration supplied for master recurrence rendering.

This keeps browser-local display stable while preserving RFC3339 offsets from backend payload timestamps.

Verification Checklist

Static

  • rg -n "\bel\b|el\." src/time_compass/mcp/ui/js
  • rg -n "_durationInput|fixedCalendarEvents|draftCalendarEvents|pad2|formatHm|overlap\(" src/time_compass/mcp/ui/js

Runtime / unit

  • uv run pytest tests/unit/integrations/google_calendar/test_event_read_model.py -v
  • uv run pytest tests/unit/test_planner_utils.py -v
  • uv run pytest tests/unit/planner/test_planner_web.py -v

UI smoke

  • Open Planner Studio and verify:
  • recurring start anchor no longer drifts to view_date
  • apply/toggle/zoom/date navigation still function
  • event times match browser local timezone display expectations

Version

  • v2.0 (2026-02-21)
  • Removed 08:00 all-day heuristic.
  • Removed recurrence master re-anchor behavior.
  • Added frontend ingress normalization and naming standardization.
  • Explicit FullCalendar local timezone configuration.