{"openapi":"3.1.0","info":{"title":"Tokuma User API","version":"0.1.0"},"paths":{"/v1/activity-codes":{"get":{"tags":["activity-codes"],"summary":"Get Activity Codes","description":"Get all available activity codes.\n\nReturns all UTBMS activity codes available for time tracking and billing.\nThis is reference data but requires authentication.","operationId":"activity-codes_get_activity_codes","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActivityCodesResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/auth/token":{"post":{"tags":["auth"],"summary":"Login","description":"OAuth2 password flow for authentication using username and password.","operationId":"auth_login","parameters":[{"name":"x-device-id","in":"header","required":true,"schema":{"type":"string","title":"X-Device-Id"}},{"name":"x-device-name","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Device-Name"}},{"name":"x-platform","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Platform"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_auth_login"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/auth/refresh":{"post":{"tags":["auth"],"summary":"Refresh","description":"Refresh access token using refresh token.\n\nImplements rotating refresh tokens with a supersession grace window:\n\n- Normal rotation: the presented token is the current tail of its\n  chain. Mint a successor, link the rows, return the new pair.\n- Replay-within-grace: the presented token has already been\n  superseded but the grace window hasn't elapsed. This is the\n  \"client never received the previous response\" case (network\n  drop, laptop sleep mid-flight). Mint a fresh tail extending the\n  chain and return it — the client gets a working token without\n  being kicked to /login.\n- Replay-after-grace: the same presentation past the grace window\n  is presumed reuse (legitimate clients couldn't still be holding\n  the old token this long after rotation). Revoke **every** token\n  the user holds and return ``REFRESH_TOKEN_REUSE_DETECTED``.\n\nToken plaintext is never stored — only the SHA-256 hash. Replays\ntherefore always mint a fresh successor rather than re-handing\nthe exact original successor; the sweep prunes intermediate\nsuperseded rows once they pass the grace window.\n\n**The successor's ``device_id`` comes from the presented row, never\nfrom ``X-Device-Id``.** Both revocation paths — reuse detection and\nlogout — key on ``(user_id, device_id)``, so a caller who could\nchoose the successor's device could refresh once under a device\nname the victim's client never touches and move their chain out of\nreach of the victim's own logout *and* of the reuse detection the\nvictim's replay triggers. The descriptive fields (name, platform,\napp version) still follow the headers; they are labels, not\nrevocation keys. A header that disagrees with the stored device is\nlogged (``event=auth.refresh_token.device_id_mismatch``) — that\ndisagreement is the signature of the attack it now cannot perform.","operationId":"auth_refresh","parameters":[{"name":"x-device-id","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Device-Id"}},{"name":"x-device-name","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Device-Name"}},{"name":"x-platform","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Platform"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}},{"name":"tokuma_rt","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tokuma Rt"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/RefreshRequest"},{"type":"null"}],"title":"Request"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/auth/logout":{"post":{"tags":["auth"],"summary":"Logout","description":"Logout user by revoking the device's refresh-token chain.\n\nRevoking the whole `(user_id, device_id)` family — not just the\npresented token — is load-bearing: with rotating refresh tokens,\npredecessors of the current tail still sit in the table until the\ngrace-window sweep prunes them. A bare `revoke_by_token_hash` of\njust the tail would leave those predecessors live (the FK's\n``ON DELETE SET NULL`` nulls their ``superseded_by_id``), so a\nreplay during the grace window would walk the now-orphaned\npredecessor as if it were the tail and resurrect the session.","operationId":"auth_logout","parameters":[{"name":"tokuma_rt","in":"cookie","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tokuma Rt"}}],"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/LogoutRequest"},{"type":"null"}],"title":"Request"}}}},"responses":{"204":{"description":"Successful Response"},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/auth/check-method":{"get":{"tags":["auth"],"summary":"Check Auth Method","description":"Check authentication method for a username (email address).","operationId":"auth_check_auth_method","parameters":[{"name":"username","in":"query","required":true,"schema":{"type":"string","maxLength":1024,"title":"Username"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckAuthMethodResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/auth/status":{"get":{"tags":["auth"],"summary":"Get Status","operationId":"auth_get_status","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserStatus"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/auth/timezone":{"put":{"tags":["auth"],"summary":"Update Timezone","description":"Update the current user's timezone.","operationId":"auth_update_timezone","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTimezoneRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTimezoneResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/auth/target-minutes":{"put":{"tags":["auth"],"summary":"Update Target Minutes","description":"Update the current user's daily target minutes for utilization calculation.","operationId":"auth_update_target_minutes","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTargetMinutesRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTargetMinutesResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/auth/week-start":{"put":{"tags":["auth"],"summary":"Update Week Start Day","description":"Update which weekday the current user's calendars start on.\n\nDisplay-only, and the desktop never reads the value from this\nresponse: the write bumps ``users.version``, which replays the\nsynthetic ``user_settings`` row through the cursor sync feed on the\nclient's next round. The response exists so the caller can confirm\nwhat was stored.","operationId":"auth_update_week_start_day","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWeekStartDayRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWeekStartDayResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/auth/ms365/initiate":{"post":{"tags":["auth"],"summary":"Ms365 Initiate","description":"Initiate MS365 OAuth flow for client authentication.","operationId":"auth_ms365_initiate","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MS365InitiateRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MS365InitiateResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/auth/ms365/callback":{"post":{"tags":["auth"],"summary":"Ms365 Callback","description":"Handle MS365 OAuth callback and exchange code for Tokuma JWT tokens.","operationId":"auth_ms365_callback","parameters":[{"name":"x-device-id","in":"header","required":true,"schema":{"type":"string","title":"X-Device-Id"}},{"name":"x-device-name","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Device-Name"}},{"name":"x-platform","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-Platform"}},{"name":"x-app-version","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"X-App-Version"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MS365CallbackRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/capture-images/{image_id}":{"put":{"tags":["capture-images"],"summary":"Upload Capture Image","description":"Upload a capture image to Azure Blob Storage with client-assigned image ID.","operationId":"capture-images_upload_capture_image","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"image_id","in":"path","required":true,"schema":{"type":"string","title":"Image Id"}},{"name":"captured_at","in":"query","required":true,"schema":{"type":"string","format":"date-time","description":"Image capture timestamp (ISO 8601)","title":"Captured At"},"description":"Image capture timestamp (ISO 8601)"},{"name":"device_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Client device ID","title":"Device Id"},"description":"Client device ID"},{"name":"span_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Span ID","title":"Span Id"},"description":"Span ID"},{"name":"session_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Session ID","title":"Session Id"},"description":"Session ID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImageUploadResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/client/events":{"post":{"tags":["client-events"],"summary":"Ingest Client Events","operationId":"client-events_ingest_client_events","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientEventBatchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClientEventBatchResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/client-logs":{"post":{"tags":["client-logs"],"summary":"Upload Client Log","operationId":"client-logs_upload_client_log","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadClientLogRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadClientLogResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/feedback":{"post":{"tags":["feedback"],"summary":"Create Feedback","operationId":"feedback_create_feedback","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFeedbackRequest"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFeedbackResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/recording-config":{"get":{"tags":["recording-config"],"summary":"Get Recording Config","description":"Get recording configuration for the client app.","operationId":"recording-config_get_recording_config","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RecordingConfigResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/integrations/microsoft-365/sync":{"post":{"tags":["integrations"],"summary":"Trigger Sync","description":"Trigger manual MS365 email sync for the current user.\n\nQueues the sync task on a high-priority queue for immediate processing.\nOnly syncs emails (Teams calls sync on scheduled interval).","operationId":"integrations_trigger_sync","responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSyncResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/matter-segments/suggestions":{"get":{"tags":["matter-segments"],"summary":"Get Suggested Segments","description":"AI-suggested segments for a time entry, as bare ids.\n\nFor clients that mirror the matter tree locally — see\n``SuggestedSegmentsResponse``. Returns an\nempty list when no ``time_entry_id`` is given (the create-entry\ndialog has no entry to suggest against yet).\n\nOnly segments the user may still assign are returned — a detection\nrecorded before its matter closed, or before the matter was\nrestricted away from them, is dropped here rather than left for the\nclient's mirror to catch. A matter the user is a participant of ranks\nahead of an equal score on one they are not, with the same factor\nsegment detection uses.","operationId":"matter-segments_get_suggested_segments","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"time_entry_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Time Entry Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SuggestedSegmentsResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/span-updates":{"post":{"tags":["span-updates"],"summary":"Create Span Updates","operationId":"span-updates_create_span_updates","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpanUpdateBatchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpanUpdateBatchResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/status":{"get":{"tags":["status"],"summary":"Get Server Status","operationId":"status_get_server_status","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerStatusResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/sync":{"get":{"tags":["sync"],"summary":"Get Sync","description":"Return rows changed since the cursor for every cursor-synced resource.","operationId":"sync_get_sync","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor blob from the previous response's `next_since`. Omit or pass empty for cold-start (returns everything for the user).","title":"Since"},"description":"Opaque cursor blob from the previous response's `next_since`. Omit or pass empty for cold-start (returns everything for the user)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":2000,"minimum":1,"description":"Per-resource page size (default 500, max 2000).","default":500,"title":"Limit"},"description":"Per-resource page size (default 500, max 2000)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SyncResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/time-blocks":{"get":{"tags":["time-blocks"],"summary":"Get Time Blocks","description":"Get time blocks for the specified date.\n\nThis endpoint returns raw time blocks for the chronology display.\nFor managing time entries (create, update, delete, release), use the /time-entries endpoints.","operationId":"time-blocks_get_time_blocks","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"date","in":"query","required":true,"schema":{"type":"string","format":"date","description":"Date to get time blocks for (YYYY-MM-DD)","title":"Date"},"description":"Date to get time blocks for (YYYY-MM-DD)"},{"name":"source_types","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"$ref":"#/components/schemas/TimeBlockSourceType"}},{"type":"null"}],"description":"Optional filter restricting returned time blocks by source type. Repeat the parameter for multiple values (e.g. ?source_types=APP&source_types=MS365_CALL). Omit to return blocks from every source.","title":"Source Types"},"description":"Optional filter restricting returned time blocks by source type. Repeat the parameter for multiple values (e.g. ?source_types=APP&source_types=MS365_CALL). Omit to return blocks from every source."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeBlockListResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/time-blocks/{time_block_id}":{"patch":{"tags":["time-blocks"],"summary":"Move Time Block","description":"Move a time block to a different entry or detach it.\n\nReturns the updated block plus every entry whose duration was\nrecalculated, in the cursor-sync wire shape so the desktop client\ncan apply the response directly to its local SQLite mirror.","operationId":"time-blocks_move_time_block","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"time_block_id","in":"path","required":true,"schema":{"type":"integer","title":"Time Block Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTimeBlockRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveTimeBlockResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/time-blocks/move":{"post":{"tags":["time-blocks"],"summary":"Move Time Blocks","description":"Bulk move of time blocks to one target entry (multi-select drag).\n\nMoves 1..N blocks to one target entry (or detaches with ``null``)\nin a single transaction. Per-block domain failures come back as\nstructured ``FailedBlockMove`` items; the whole request fails only\non a missing/released target entry or infrastructure errors.\n``moved`` and ``affected_time_entries`` are in cursor-sync wire\nshape for direct local-mirror apply.","operationId":"time-blocks_move_time_blocks","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveTimeBlocksRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveTimeBlocksResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/time-entries":{"get":{"tags":["time-entries"],"summary":"Get Time Entries","description":"Get time entries for the specified date.","operationId":"time-entries_get_time_entries","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"date","in":"query","required":true,"schema":{"type":"string","format":"date","description":"Date to get time entries for (YYYY-MM-DD)","title":"Date"},"description":"Date to get time entries for (YYYY-MM-DD)"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeEntryListResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"post":{"tags":["time-entries"],"summary":"Create Time Entry","description":"Create a new time entry, optionally sourced from existing time blocks.\n\nReturns the row in the cursor-sync wire shape (``version``,\n``deleted_at``, denormalized ``matter_segment``) so the desktop\nclient can apply the response directly to its local SQLite mirror\nvia the same idempotent upsert path used by ``GET /api/v1/sync``.\n\n``time_block_ids`` attaches those blocks to the new entry in the same\ntransaction; the mutated blocks come back in ``attached_blocks`` so the\nclient's activity feed stops drawing them as pending without waiting for\nthe next sync round. The entry's own fields stay at the top level, so a\nclient that models only ``TimeEntrySyncDto`` reads this response exactly\nas it read the old one.","operationId":"time-entries_create_time_entry","security":[{"OAuth2PasswordBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTimeEntryRequest"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTimeEntryResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/time-entries/generate":{"post":{"tags":["time-entries"],"summary":"Generate Time Entries","description":"Rebuild a day's timesheet, or decide it does not need rebuilding.\n\nAnswers one of three ways, and **none of them is an error**: a new run, the\nrun already in flight (adopt it and watch its progress), or nothing —\nbecause the day was rebuilt moments ago or nothing has changed since. The\nthird is a 200 carrying ``outcome=skipped`` and no ``task_id``.\n\nThat shape is what lets the client's Refresh control run on every press and\nevery window focus without asking permission. Refreshing the *data* is\nfree and always happens client-side; reprocessing the *day* costs a\nreasoning call, so the server declines it silently rather than telling the\nuser their gesture was rejected. Whichever way it answers, the worker\nwrites status onto the ``generation_tasks`` row and the desktop client\nobserves it via the cursor sync feed (``GET /api/v1/sync``).\n\nA request carrying no ``trigger`` comes from a client predating this\nbehaviour and takes the old path exactly: ``mode`` is honoured, a run is\nalways minted, and a collision is a 409.","operationId":"time-entries_generate_time_entries","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateTimeEntriesRequest"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationTaskResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/time-entries/{time_entry_id}":{"patch":{"tags":["time-entries"],"summary":"Update Time Entry","description":"Update an existing time entry.\n\nUpdates a time entry with the provided fields. Only provided fields\nwill be updated. Released time entries cannot be edited. The\nresponse is the cursor-sync wire DTO of the row.","operationId":"time-entries_update_time_entry","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"time_entry_id","in":"path","required":true,"schema":{"type":"integer","description":"ID of the time entry to update","title":"Time Entry Id"},"description":"ID of the time entry to update"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTimeEntryRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TimeEntrySyncDto"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["time-entries"],"summary":"Delete Time Entry","description":"Soft-delete a time entry and detach its associated time blocks.\n\nReturns the soft-deleted entry (``deleted_at`` populated) plus every\ndetached child block — blocks stay live with ``time_entry_id``\ncleared, so they return to the unattached/pending pool instead of\nbeing discarded. Both carry the trigger-advanced ``version``. The\ndesktop client applies the response directly to its local SQLite\nmirror; the next ``GET /api/v1/sync`` round re-delivers the same rows\nand the version-check predicate makes the second apply a no-op.\nReleased time entries cannot be deleted.","operationId":"time-entries_delete_time_entry","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"time_entry_id","in":"path","required":true,"schema":{"type":"integer","description":"ID of the time entry to delete","title":"Time Entry Id"},"description":"ID of the time entry to delete"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteTimeEntryResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/time-entries/move":{"post":{"tags":["time-entries"],"summary":"Move Time Entries","description":"Bulk segment reassignment of time entries (multi-select drag).\n\nMoves 1..N entries to one target segment (or Unassigned with\n``null``) in a single transaction, re-deriving each entry's\nbillability from the target segment. Per-entry domain failures come\nback as structured ``FailedEntryMove`` items; the whole request\nfails only on a missing target segment or infrastructure errors.\n``moved`` rows are in cursor-sync wire shape for direct local-mirror\napply.","operationId":"time-entries_move_time_entries","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveTimeEntriesRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoveTimeEntriesResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/time-entries/merge":{"post":{"tags":["time-entries"],"summary":"Merge Time Entries","description":"Fold 1..N time entries into another one, discarding the sources.\n\nBacks the desktop gesture of dropping one or more entries onto\nanother entry's row. Every source's time blocks move to the target\nand each source is soft-deleted; the target keeps its own narrative,\nmatter/segment, activity code and billability, and its legal time is\nre-derived or summed by the rule in ``resolve_merged_legal_time``.\n\n**This is deliberately not a block move with a flag.** A block-level\ngesture never deletes the source entry (server/AGENTS.md), so the\ndeletion belongs to an explicitly entry-level endpoint. Per-source\nrejections come back as structured ``FailedEntryMerge`` items;\nproblems with the target reject the whole request, since every\nsource shares it. Every returned row is in cursor-sync wire shape\nfor direct local-mirror apply.","operationId":"time-entries_merge_time_entries","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MergeTimeEntriesRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MergeTimeEntriesResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/time-entries/delete":{"post":{"tags":["time-entries"],"summary":"Delete Time Entries","description":"Bulk soft-delete of time entries (the desktop Selection header).\n\nEach member takes the same write ``DELETE /{time_entry_id}`` takes —\na ``deleted_at`` tombstone on the entry, its live child blocks\ndetached (``time_entry_id`` cleared, rows still live) back into the\npending pool — but in a single transaction. Every returned row is in\ncursor-sync wire shape for direct local-mirror apply.\n\nPer-entry rejections come back as structured ``FailedEntryDelete``\nitems and **never abort the batch**: the only rejection is a released\nentry, which is locked permanently, so refusing the whole request\nwould make one locked row block every other deletion in the\nselection. See\n``docs/decisions/0010-a-bulk-delete-skips-what-it-cannot-delete.md``.\nInfrastructure errors still fail the whole request.","operationId":"time-entries_delete_time_entries","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteTimeEntriesRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteTimeEntriesResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/time-entries/release/check":{"post":{"tags":["time-entries"],"summary":"Release Check","description":"Preflight the release gate for the confirmation dialog.\n\nRuns the same structural validation matrix as ``/release`` but\nwrites nothing. The desktop client calls this on dialog open and\nrefuses to show Confirm while ``failed`` is non-empty.","operationId":"time-entries_release_check","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReleaseTimeEntriesRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReleaseCheckResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/time-entries/release":{"post":{"tags":["time-entries"],"summary":"Release Time Entries","description":"Release multiple time entries.\n\nOnce released, time entries cannot be edited or deleted. This\naction is irreversible. The structural validation matrix (empty,\nno matter, no activity code) is shared with ``/release/check``;\nrejections come back as structured ``FailedRelease`` entries the\nclient renders per-entry.\n\nThe ``released`` list carries the freshly-inserted\n``TimeEntryReleaseSyncDto`` rows so the desktop client can apply\nthem to its local SQLite mirror immediately (Channel A) rather\nthan wait for the next cursor-sync round.","operationId":"time-entries_release_time_entries","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReleaseTimeEntriesRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReleaseTimeEntriesResponse"}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/v1/updates/latest":{"get":{"tags":["updates"],"summary":"Get Latest Update","description":"Serve the update manifest for the Tauri updater.\n\nQuery params (Tauri replaces ``{{target}}``/``{{current_version}}``):\n- target: e.g. \"darwin-aarch64\". Filters to that platform.\n- current_version: informational only.\n\nReturns 204 No Content when no update is available.","operationId":"updates_get_latest_update","parameters":[{"name":"target","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Target"}},{"name":"current_version","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Current Version"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LatestManifestResponse"}}}},"400":{"description":"Invalid target platform"},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"204":{"description":"No update available"}}}},"/v1/updates/download/{blob_path}":{"get":{"tags":["updates"],"summary":"Download Update","operationId":"updates_download_update","parameters":[{"name":"blob_path","in":"path","required":true,"schema":{"type":"string","title":"Blob Path"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/v1/updates/failures":{"post":{"tags":["updates"],"summary":"Report Update Failure","description":"Record an auto-update failure from the desktop app.\n\nUnauthenticated by design: the client path reports failures from startup,\nbackground-hourly, and manual-check flows, which all run before login\nand while logged out. Identity is the ``device_id`` in the payload.","operationId":"updates_report_update_failure","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateFailureReport"}}},"required":true},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"400":{"description":"Validation error or domain rule violation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing, expired, or invalid authentication.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Authenticated but not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"Resource not found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"409":{"description":"Resource state conflict (duplicate, illegal transition).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"422":{"description":"Request body failed schema validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"500":{"description":"Server-side failure.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}}},"components":{"schemas":{"APIErrorCode":{"type":"string","enum":["INVALID_CREDENTIALS","REFRESH_TOKEN_REQUIRED","INVALID_REFRESH_TOKEN","REFRESH_TOKEN_REUSE_DETECTED","USER_NOT_FOUND","NOT_AUTHENTICATED","MS365_CONFIG_NOT_FOUND","MS365_INVALID_STATE","MS365_TOKEN_EXCHANGE_FAILED","MS365_INVALID_USER","MS365_GRAPH_REQUEST_FAILED","MS365_CREDENTIALS_REJECTED","MS365_PERMISSION_DENIED","WRONG_AUTH_METHOD","CAPABILITY_REQUIRED","LAST_ADMINISTRATOR","TOO_MANY_REQUESTS","VALIDATION_ERROR","NOT_FOUND","CONFLICT","ALREADY_EXISTS","STALE_CURSOR","INVALID_CURSOR","DATA_WIPE_FORBIDDEN_ENV","TIME_BLOCK_INVALID_TIME_RANGE","TIME_BLOCK_INVALID_SEGMENT","SEGMENT_NOT_ASSIGNABLE","TIME_BLOCK_INVALID_ACTIVITY_CODE","TIME_ENTRY_RELEASE_NOT_FOUND","TIME_ENTRY_RELEASE_EMPTY","TIME_ENTRY_RELEASE_NO_MATTER","TIME_ENTRY_RELEASE_NO_ACTIVITY_CODE","TIME_ENTRY_RELEASE_NARRATIVE_PENDING","TIME_ENTRY_NO_BLOCKS","TIME_ENTRY_MERGE_NOT_FOUND","TIME_ENTRY_MERGE_RELEASED","TIME_ENTRY_MERGE_DIFFERENT_DATE","TIME_ENTRY_MERGE_SELF","GENERATION_UNAVAILABLE","GENERATION_AI_INVALID_RESPONSE","GENERATION_AI_PROVIDER_ERROR","GENERATION_INPUT_TOO_LARGE","GENERATION_CONCURRENT","GENERATION_IN_PROGRESS","GENERATION_TIMEOUT_RECOVERED","GENERATION_DISPATCH_FAILED","GENERATION_DISPATCH_LOST","GENERATION_FAILED","SPAN_EVENT_DISPATCH_EXHAUSTED","EXTRACTION_IMAGE_NOT_FOUND","EXTRACTION_IMAGE_INVALID","EXTRACTION_AI_INVALID_RESPONSE","EXTRACTION_AI_PROVIDER_ERROR","EXTRACTION_INPUT_TOO_LARGE","REASSEMBLY_ENRICH_FAILED","REASSEMBLY_REBUILD_FAILED","REASSEMBLY_TIMEZONE_INVALID","REASSEMBLY_FAILED","SPAN_OWNERSHIP_MISMATCH","SPAN_INGEST_FAILED","CAPTURE_IMAGE_OWNERSHIP_MISMATCH","DATABASE_UNAVAILABLE","UNKNOWN_ERROR"],"title":"APIErrorCode","description":"Centralized error codes for the API."},"ActivityCodeDto":{"properties":{"id":{"type":"string","title":"Id","description":"Activity code ID"},"name":{"type":"string","title":"Name","description":"Activity code name"},"description":{"type":"string","title":"Description","description":"Activity code description"},"order":{"type":"integer","title":"Order","description":"Display order of the activity code"}},"type":"object","required":["id","name","description","order"],"title":"ActivityCodeDto","description":"Data transfer object for activity code."},"ActivityCodesResponse":{"properties":{"activity_codes":{"items":{"$ref":"#/components/schemas/ActivityCodeDto"},"type":"array","title":"Activity Codes","description":"List of available activity codes"}},"type":"object","required":["activity_codes"],"title":"ActivityCodesResponse","description":"Response model for activity codes list."},"AppConfig":{"properties":{"app_id":{"type":"string","title":"App Id"},"name":{"type":"string","title":"Name"},"app_type":{"type":"string","title":"App Type"},"bundle_ids":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Bundle Ids"},"executable_names":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Executable Names"},"url_patterns":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Url Patterns"},"icon_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Icon Label"}},"type":"object","required":["app_id","name","app_type"],"title":"AppConfig","description":"Configuration for a single app."},"AppFocusChangedPayload":{"properties":{"focused":{"type":"boolean","title":"Focused"},"window":{"type":"string","maxLength":16,"title":"Window"}},"type":"object","required":["focused","window"],"title":"AppFocusChangedPayload"},"AppInfo":{"properties":{"app_id":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"App Id"},"app_name":{"type":"string","maxLength":255,"title":"App Name"},"bundle_id":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Bundle Id"},"app_path":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"App Path"}},"type":"object","required":["app_name"],"title":"AppInfo"},"AppStartedPayload":{"properties":{"app_version":{"type":"string","maxLength":32,"title":"App Version"},"os":{"type":"string","maxLength":16,"title":"Os"},"os_version":{"type":"string","maxLength":64,"title":"Os Version"},"install_id":{"type":"string","maxLength":64,"title":"Install Id"},"permissions":{"additionalProperties":{"type":"string","maxLength":32},"propertyNames":{"maxLength":64},"type":"object","maxProperties":16,"title":"Permissions"}},"type":"object","required":["app_version","os","os_version","install_id"],"title":"AppStartedPayload"},"AppStoppedPayload":{"properties":{"reason":{"type":"string","maxLength":32,"title":"Reason"}},"type":"object","required":["reason"],"title":"AppStoppedPayload"},"ArtifactDto":{"properties":{"kind":{"type":"string","enum":["doc","email"],"title":"Kind"},"title":{"type":"string","title":"Title"},"location":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Location"}},"type":"object","required":["kind","title"],"title":"ArtifactDto","description":"One thing the user worked on, as seen by the client.\n\nDeliberately a wire type, NOT ``shared...block_facts.Artifact``: that\none is internal and must stay free to refactor. This module's contract\n(see module docstring) is that sync DTOs are decoupled from internal\nshapes and carry only what the client consumes.\n\n``location`` is NOT called ``path``: the host app reports whatever it\nconsiders the document's location, which for a SharePoint / OneDrive\nfile opened in desktop Word is an ``https://`` URL, not a filesystem\npath. Clients must branch on shape — an ``exists()``-style gate would\nsilently reject every cloud-hosted document. ``None`` for an ``email``\n(a subject line has no location) and for a ``doc`` whose location the\nhost app never reported (e.g. an email attachment)."},"BackgroundTaskErrorPayload":{"properties":{"task_name":{"type":"string","maxLength":64,"title":"Task Name"},"error_kind":{"type":"string","maxLength":32,"title":"Error Kind"},"stalled_seconds":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Stalled Seconds"}},"type":"object","required":["task_name","error_kind"],"title":"BackgroundTaskErrorPayload","description":"A desktop-client background task reported an abnormal condition.\n\n``task_name`` is the stable name the task was spawned/instrumented\nunder (e.g. ``\"sync_engine\"``, ``\"recording_pusher\"``);\n``stalled_seconds`` carries the observed heartbeat gap for stalls.\n\n``error_kind`` is an open, client-defined vocabulary — deliberately a\nbounded ``str`` rather than an enum, so a client can report a new\ncondition without a server deploy ahead of it. Known values:\n\n* ``\"panicked\"`` — the task died instead of completing.\n* ``\"stalled\"`` — the task missed its declared heartbeat deadline.\n* ``\"identity_ambiguous\"`` — the local sync mirror held an identity\n  other than the one the feed serves, and was wiped + cold-started\n  (``sync_engine`` only). **Not a dead task**: the client recovered.\n  Alerting that treats every row here as a failure must exclude it."},"BlockArtifactsDto":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ArtifactDto"},"type":"array","title":"Items"},"total":{"type":"integer","title":"Total","default":0}},"type":"object","title":"BlockArtifactsDto","description":"Artifacts for one block: a capped list plus the honest total.\n\n``total`` is the pre-cap count of displayed (doc+email) artifacts — the\nsame population as ``items`` — so a client showing the first N can render\nan honest \"+more\" without the wire ever implying the capped list is\ncomplete."},"BlockMoveOrigin":{"properties":{"time_block_id":{"type":"integer","title":"Time Block Id","description":"ID of the moved time block"},"previous_time_entry_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Previous Time Entry Id","description":"Entry the block was under immediately before the move (null=Unassigned)"}},"type":"object","required":["time_block_id","previous_time_entry_id"],"title":"BlockMoveOrigin","description":"Authoritative pre-move parent of one actually-moved block.\n\nSame contract as ``EntryMoveOrigin``: the client's Undo groups by\nthese server-reported parents; no-op members (already under the\ntarget) appear in ``moved`` but never here."},"Body_auth_login":{"properties":{"grant_type":{"anyOf":[{"type":"string","pattern":"^password$"},{"type":"null"}],"title":"Grant Type"},"username":{"type":"string","title":"Username"},"password":{"type":"string","format":"password","title":"Password"},"scope":{"type":"string","title":"Scope","default":""},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"format":"password","title":"Client Secret"}},"type":"object","required":["username","password"],"title":"Body_auth_login"},"ChatAttributes":{"properties":{"type":{"type":"string","const":"chat","title":"Type"},"chat_title":{"type":"string","maxLength":500,"title":"Chat Title"},"has_external_participants":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has External Participants"}},"type":"object","required":["type","chat_title"],"title":"ChatAttributes","description":"Attributes for a non-Teams chat conversation (WhatsApp web first).\n\nEach conversation is its own session — ``chat_title`` keys the session\nidentity so the server groups per-conversation TimeBlocks separately,\nexactly as :class:`TeamsChatAttributes` does for Teams.\n\nDeliberately generic rather than WhatsApp-named: the WhatsApp desktop\nclients, and later Slack / WeChat, reuse this variant and the\n``CHAT_IDENTITY`` event rather than each minting their own. Only the\n*reader* is per-app.\n\n``has_external_participants`` has no meaning in every chat app (WhatsApp\nhas no org boundary) and is simply ``None`` there.\n\n**Deploy ordering — this is the hard one.** ``SessionAttributes`` is a\ndiscriminated union with fixed tags, so a client emitting ``type:\n\"chat\"`` against a server that predates this class 422s the *entire*\nspan-update batch and the user's span is lost. Server must ship first."},"CheckAuthMethodResponse":{"properties":{"user_type":{"$ref":"#/components/schemas/UserType","description":"Authentication type: MANUAL or MS365"},"username":{"type":"string","title":"Username","description":"Username (email address)"}},"type":"object","required":["user_type","username"],"title":"CheckAuthMethodResponse"},"ClientEventBatchRequest":{"properties":{"device_id":{"type":"string","maxLength":255,"title":"Device Id"},"events":{"items":{"oneOf":[{"$ref":"#/components/schemas/_PermissionStateChange"},{"$ref":"#/components/schemas/_PermissionRequestDenied"},{"$ref":"#/components/schemas/_PermissionEntitlementMissing"},{"$ref":"#/components/schemas/_AppStarted"},{"$ref":"#/components/schemas/_AppStopped"},{"$ref":"#/components/schemas/_AppFocusChanged"},{"$ref":"#/components/schemas/_SystemSleep"},{"$ref":"#/components/schemas/_SystemWake"},{"$ref":"#/components/schemas/_ScreenLocked"},{"$ref":"#/components/schemas/_ScreenUnlocked"},{"$ref":"#/components/schemas/_ConnectivityChanged"},{"$ref":"#/components/schemas/_BackgroundTaskError"},{"$ref":"#/components/schemas/_RecordingStarted"},{"$ref":"#/components/schemas/_RecordingStopped"},{"$ref":"#/components/schemas/_RecordingStartBlocked"},{"$ref":"#/components/schemas/_RecordingError"},{"$ref":"#/components/schemas/_RecordingQuotaChanged"},{"$ref":"#/components/schemas/_RecordingExtractionOutcome"},{"$ref":"#/components/schemas/_RecordingForceClose"},{"$ref":"#/components/schemas/_RecordingSpanSendGivenUp"},{"$ref":"#/components/schemas/_RecordingUndeliverableCapture"},{"$ref":"#/components/schemas/_LoginSucceeded"},{"$ref":"#/components/schemas/_LoginFailed"},{"$ref":"#/components/schemas/_Logout"},{"$ref":"#/components/schemas/_PageView"},{"$ref":"#/components/schemas/_FeatureUsed"}],"discriminator":{"propertyName":"type","mapping":{"app_focus_changed":"#/components/schemas/_AppFocusChanged","app_started":"#/components/schemas/_AppStarted","app_stopped":"#/components/schemas/_AppStopped","background_task_error":"#/components/schemas/_BackgroundTaskError","connectivity_changed":"#/components/schemas/_ConnectivityChanged","feature_used":"#/components/schemas/_FeatureUsed","login_failed":"#/components/schemas/_LoginFailed","login_succeeded":"#/components/schemas/_LoginSucceeded","logout":"#/components/schemas/_Logout","page_view":"#/components/schemas/_PageView","permission_entitlement_missing":"#/components/schemas/_PermissionEntitlementMissing","permission_request_denied":"#/components/schemas/_PermissionRequestDenied","permission_state_change":"#/components/schemas/_PermissionStateChange","recording_error":"#/components/schemas/_RecordingError","recording_extraction_outcome":"#/components/schemas/_RecordingExtractionOutcome","recording_force_close":"#/components/schemas/_RecordingForceClose","recording_quota_changed":"#/components/schemas/_RecordingQuotaChanged","recording_span_send_given_up":"#/components/schemas/_RecordingSpanSendGivenUp","recording_start_blocked":"#/components/schemas/_RecordingStartBlocked","recording_started":"#/components/schemas/_RecordingStarted","recording_stopped":"#/components/schemas/_RecordingStopped","recording_undeliverable_capture":"#/components/schemas/_RecordingUndeliverableCapture","screen_locked":"#/components/schemas/_ScreenLocked","screen_unlocked":"#/components/schemas/_ScreenUnlocked","system_sleep":"#/components/schemas/_SystemSleep","system_wake":"#/components/schemas/_SystemWake"}}},"type":"array","maxItems":50,"title":"Events"}},"type":"object","required":["device_id","events"],"title":"ClientEventBatchRequest","description":"Request body for `POST /api/v1/client/events`.\n\n`device_id` matches the precedent set by `span_updates` (request\nbody, not header). `events[]` length cap matches `span_updates`."},"ClientEventBatchResponse":{"properties":{"accepted":{"items":{"type":"integer"},"type":"array","title":"Accepted"},"rejected":{"items":{"$ref":"#/components/schemas/RejectedEvent"},"type":"array","title":"Rejected"}},"type":"object","required":["accepted","rejected"],"title":"ClientEventBatchResponse","description":"Per-event accept/reject decision.\n\n`accepted` lists indices into `events[]` that were persisted;\n`rejected` carries per-event errors for the rest. The client marks\naccepted rows sent and bumps `send_attempts` on rejected ones."},"ClientSyncDto":{"properties":{"id":{"type":"string","title":"Id"},"organization_id":{"type":"string","title":"Organization Id"},"name":{"type":"string","title":"Name"},"is_active":{"type":"boolean","title":"Is Active"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"},"version":{"type":"integer","title":"Version"}},"type":"object","required":["id","organization_id","name","is_active","created_at","updated_at","version"],"title":"ClientSyncDto","description":"A client as seen by the cursor sync feed.\n\nOrg reference data, not user-owned — see ``service.query_clients``\nfor why the feed still scopes it per user.\n\n``is_active`` rides the wire and the **client** applies it, rather\nthan the server filtering inactive rows out. That is load-bearing:\nthe feed's only vocabulary for \"this row is gone\" is a ``deleted_at``\ntombstone, so a server-side ``is_active`` filter would make a\ndeactivated row simply stop appearing — never delivered, never\nremoved, and pinned in every local mirror forever."},"ConnectivityChangedPayload":{"properties":{"online":{"type":"boolean","title":"Online"},"offline_secs":{"type":"integer","minimum":0.0,"title":"Offline Secs","default":0}},"type":"object","required":["online"],"title":"ConnectivityChangedPayload","description":"One server-reachability transition observed by the desktop client.\n\n``offline_secs`` is the duration of the offline period that just\nended (0 when ``online`` is false — the period is only measurable\nonce it closes)."},"CreateFeedbackRequest":{"properties":{"message":{"type":"string","maxLength":5000,"minLength":1,"title":"Message","description":"Feedback message"}},"type":"object","required":["message"],"title":"CreateFeedbackRequest"},"CreateFeedbackResponse":{"properties":{"id":{"type":"string","title":"Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","created_at"],"title":"CreateFeedbackResponse"},"CreateTimeEntryRequest":{"properties":{"date":{"type":"string","format":"date","title":"Date","description":"The date for this entry"},"narrative":{"type":"string","maxLength":5000,"minLength":1,"title":"Narrative","description":"Description of the work performed"},"legal_time_minutes":{"type":"integer","minimum":0.0,"title":"Legal Time Minutes","description":"Legal time in minutes"},"activity_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Activity Code","description":"Activity code for the work"},"segment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Segment Id","description":"Segment ID to associate with this time entry"},"is_billable":{"type":"boolean","title":"Is Billable","description":"Whether the time entry is billable","default":false},"time_block_ids":{"items":{"type":"integer"},"type":"array","maxItems":500,"title":"Time Block Ids","description":"Unattached time blocks to attach to the new entry, in one transaction. Backs the desktop gesture that drops activity-feed blocks on the timesheet. A block that already belongs to a time entry is rejected: moving one between entries is what POST /v1/time-blocks/move is for, and it recalculates both sides. Empty (the default) creates an entry with no sources, which is what the plain Add-entry dialog does."},"legal_time_pinned":{"type":"boolean","title":"Legal Time Pinned","description":"Whether ``legal_time_minutes`` is a figure the user typed. True stores it, snapped to the matter's billing increment. False means the client merely pre-filled the box from the attached blocks and the user did not touch it: the entry is left with a computed legal time and keeps following its blocks as they move. Defaults to true — the behaviour every caller before this field had.","default":true}},"type":"object","required":["date","narrative","legal_time_minutes"],"title":"CreateTimeEntryRequest","description":"Request model for creating a new time entry."},"CreateTimeEntryResponse":{"properties":{"id":{"type":"integer","title":"Id"},"organization_id":{"type":"string","title":"Organization Id"},"user_id":{"type":"string","title":"User Id"},"date":{"type":"string","format":"date","title":"Date"},"legal_time_minutes":{"type":"integer","title":"Legal Time Minutes"},"legal_time_is_computed":{"type":"boolean","title":"Legal Time Is Computed"},"tracked_duration_minutes":{"type":"integer","title":"Tracked Duration Minutes"},"narrative":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Narrative"},"narrative_suggestions":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Narrative Suggestions"},"activity_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Activity Code"},"segment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Segment Id"},"matter_segment":{"anyOf":[{"$ref":"#/components/schemas/MatterSegmentSummaryDto"},{"type":"null"}]},"is_billable":{"type":"boolean","title":"Is Billable"},"legal_time_pinned":{"type":"boolean","title":"Legal Time Pinned"},"legal_time_resnapped_from":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Legal Time Resnapped From"},"is_user_created":{"type":"boolean","title":"Is User Created"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"},"version":{"type":"integer","title":"Version"},"attached_blocks":{"items":{"$ref":"#/components/schemas/TimeBlockSyncDto"},"type":"array","title":"Attached Blocks","description":"Time blocks attached to the new entry by this request, in cursor-sync wire shape. Empty when the request carried no ``time_block_ids``."}},"type":"object","required":["id","organization_id","user_id","date","legal_time_minutes","legal_time_is_computed","tracked_duration_minutes","narrative","is_billable","legal_time_pinned","is_user_created","created_at","updated_at","version"],"title":"CreateTimeEntryResponse","description":"The created entry, plus any blocks the same request attached to it.\n\n**A subclass, not a widened ``TimeEntrySyncDto``.** ``TimeEntrySyncDto`` is\nthe shared cursor-sync row shape; putting ``attached_blocks`` on it would\nship a meaningless field on every row the feed serves and churn the golden\nwire fixtures. Subclassing keeps the extra field on this one endpoint.\n\n**Why the blocks come back at all.** The desktop client applies a mutation\nresponse straight into its local SQLite mirror. The attached blocks have a\nnew ``time_entry_id`` and a bumped ``version``, and without them in the\nresponse the activity feed keeps drawing them as pending until the next\nsync round lands.\n\n**Backward compatible by construction.** The entry's own fields are\nunchanged and at the top level, so a client that models only\n``TimeEntrySyncDto`` parses this response exactly as before — the Rust DTOs\nset no ``deny_unknown_fields`` and drop the extra key. That is what makes\nthe server safe to deploy ahead of the app."},"DefaultAttributes":{"properties":{"type":{"type":"string","const":"default","title":"Type"}},"type":"object","required":["type"],"title":"DefaultAttributes","description":"Attributes for sessions without app-specific identity data."},"DeleteTimeEntriesRequest":{"properties":{"time_entry_ids":{"items":{"type":"integer"},"type":"array","maxItems":500,"minItems":1,"title":"Time Entry Ids","description":"List of time entry IDs to delete"}},"type":"object","required":["time_entry_ids"],"title":"DeleteTimeEntriesRequest","description":"Request model for bulk deletion of time entries.\n\nBacks the desktop client's Selection header; a single-entry delete\nkeeps its own ``DELETE /{time_entry_id}`` route, which the edit\ndialog and older app builds call."},"DeleteTimeEntriesResponse":{"properties":{"deleted":{"items":{"$ref":"#/components/schemas/TimeEntrySyncDto"},"type":"array","title":"Deleted","description":"Successfully soft-deleted entries in cursor-sync wire shape"},"detached_blocks":{"items":{"$ref":"#/components/schemas/TimeBlockSyncDto"},"type":"array","title":"Detached Blocks","description":"Child time blocks detached from the deleted entries — live (deleted_at null) with time_entry_id cleared, returned to the pending pool"},"failed":{"items":{"$ref":"#/components/schemas/FailedEntryDelete"},"type":"array","title":"Failed","description":"Per-entry rejections with structured codes"}},"type":"object","title":"DeleteTimeEntriesResponse","description":"Partial-success envelope for the bulk entry delete.\n\n``deleted`` carries the soft-deleted entries (``deleted_at`` set) and\n``detached_blocks`` every child block that was detached — live\n(``deleted_at`` null) with ``time_entry_id`` cleared, back in the\npending pool. Both are cursor-sync wire shape for direct local-mirror\napply, the same as the single-entry route's response.\n\nA released member comes back in ``failed`` and the rest of the batch\nstill goes: a released entry is locked forever, so \"fix or deselect\"\nis not an instruction the user can act on. See\n``docs/decisions/0010-a-bulk-delete-skips-what-it-cannot-delete.md``."},"DeleteTimeEntryResponse":{"properties":{"time_entry":{"$ref":"#/components/schemas/TimeEntrySyncDto","description":"The soft-deleted time entry (deleted_at populated)"},"cascaded_blocks":{"items":{"$ref":"#/components/schemas/TimeBlockSyncDto"},"type":"array","title":"Cascaded Blocks","description":"Child time blocks detached from the deleted entry — live (deleted_at null) with time_entry_id cleared, returned to the pending pool"}},"type":"object","required":["time_entry"],"title":"DeleteTimeEntryResponse","description":"Response model for deleting a time entry.\n\nReturns the soft-deleted entry (``deleted_at`` set) plus every\n**detached** child time block — blocks stay live (``deleted_at`` is\n``None``) with their ``time_entry_id`` cleared, so they return to the\nunattached/pending pool rather than vanishing. Both carry the\ntrigger-bumped ``version``. The desktop client applies the response\ndirectly to its local SQLite mirror via the same idempotent upsert\npath used by ``GET /api/v1/sync``: the entry's row is evicted and the\ndetached blocks reappear as pending."},"DocumentAttributes":{"properties":{"type":{"type":"string","const":"document","title":"Type"},"document_name":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Document Name"},"document_path":{"anyOf":[{"type":"string","maxLength":1000},{"type":"null"}],"title":"Document Path"}},"type":"object","required":["type"],"title":"DocumentAttributes","description":"Attributes for document-based sessions (Word, Excel, PowerPoint)."},"EmailMessageAttributes":{"properties":{"type":{"type":"string","const":"email_message","title":"Type"},"subject":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Subject"},"from_email":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"From Email"},"view_type":{"type":"string","maxLength":50,"title":"View Type"}},"type":"object","required":["type","view_type"],"title":"EmailMessageAttributes","description":"Attributes for Outlook desktop email/compose/calendar-invite sessions.\n\nEach opened email, compose draft, or calendar invite is its own session\n— `(subject, from_email)` keys the session so returning to the same\nmessage reuses the session. `subject` is null for calendar invites\n(Outlook AX exposes no `HeaderMainContainer` there).\n\n`view_type` is one of `email`, `calendar_invite`, `compose`, `other`\n(see `AxOutlookViewType` in `shared/types/span_event_types.py`). It is\nstored as a free string at the wire boundary so a future client-side\naddition does not 422 every Outlook span; downstream parsing logs and\nskips unknown values."},"EntryLegalTimeRestore":{"properties":{"time_entry_id":{"type":"integer","title":"Time Entry Id","description":"Entry to restore; ignored if not being moved"},"legal_time_minutes":{"type":"integer","title":"Legal Time Minutes","description":"The figure the entry held before the move"}},"type":"object","required":["time_entry_id","legal_time_minutes"],"title":"EntryLegalTimeRestore","description":"One typed legal time an Undo is putting back.\n\nA move re-grids a typed figure onto the destination Matter, which makes it\nlossy: replaying the move in reverse snaps the already-snapped figure again\nand can land somewhere else entirely (15 typed on a 15-minute Matter, moved\nto a 6-minute one, becomes 18; sent back it becomes 30). So Undo does not\nre-derive — it echoes the pre-move figure the server reported in\n``moved_from`` and the server writes it **verbatim**.\n\nVerbatim is deliberate, and it is why this field exists rather than a\n``legal_time_minutes`` on the move request. A typed figure is allowed to sit\noff its Matter's grid — a Matter whose rounding changed after the figure was\ntyped keeps it (the admin impact dialog promises exactly that), and\n``merge`` writes unsnapped sums by decision 0009 — so snapping on the way\nback would make Undo destroy the very figures the product promises to keep.\nWriting an off-grid typed figure is therefore not a new capability here;\n``merge`` already does it through a supported endpoint."},"EntryMoveOrigin":{"properties":{"time_entry_id":{"type":"integer","title":"Time Entry Id","description":"ID of the moved time entry"},"previous_segment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Previous Segment Id","description":"Segment the entry was in immediately before the move (null=Unassigned)"},"previous_legal_time_minutes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Previous Legal Time Minutes","description":"The typed legal time the move re-snapped away, for Undo to echo back in ``restore_legal_times``. Null when nothing was re-snapped"}},"type":"object","required":["time_entry_id","previous_segment_id"],"title":"EntryMoveOrigin","description":"Authoritative pre-move parent of one actually-moved entry.\n\nDrives the client's Undo: grouping by the server-reported previous\nsegment (rather than parents captured client-side at selection\ntime) means a no-op member or a stale captured parent can never\nsend Undo to a segment the entry wasn't actually in. No-op members\n(already on the target) appear in ``moved`` but never here."},"ErrorResponse":{"properties":{"code":{"type":"string","title":"Code"},"message":{"type":"string","title":"Message"},"details":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Details"}},"type":"object","required":["code","message"],"title":"ErrorResponse"},"FailedBlockMove":{"properties":{"time_block_id":{"type":"integer","title":"Time Block Id","description":"ID of the time block that failed to move"},"code":{"$ref":"#/components/schemas/APIErrorCode","description":"Structured failure code clients branch on for per-block copy"},"message":{"type":"string","title":"Message","description":"Default human-readable message paired with code"}},"type":"object","required":["time_block_id","code","message"],"title":"FailedBlockMove","description":"Per-block rejection from the bulk block move.\n\nSame structured shape as ``FailedRelease``: ``code`` is the contract\nthe client branches on, ``message`` the default English copy."},"FailedEntryDelete":{"properties":{"time_entry_id":{"type":"integer","title":"Time Entry Id","description":"ID of the time entry that failed to delete"},"code":{"$ref":"#/components/schemas/APIErrorCode","description":"Structured failure code clients branch on for per-entry copy"},"message":{"type":"string","title":"Message","description":"Default human-readable message paired with code"}},"type":"object","required":["time_entry_id","code","message"],"title":"FailedEntryDelete","description":"Per-entry rejection from the bulk entry delete.\n\nSame structured shape as ``FailedRelease`` / ``FailedEntryMove``.\nOnly two codes reach it: ``NOT_FOUND`` (gone, or another user's) and\n``CONFLICT`` (released, and therefore locked forever)."},"FailedEntryMerge":{"properties":{"time_entry_id":{"type":"integer","title":"Time Entry Id","description":"ID of the source entry that was not merged"},"code":{"$ref":"#/components/schemas/APIErrorCode","description":"Structured failure code clients branch on for per-entry copy"},"message":{"type":"string","title":"Message","description":"Default human-readable message paired with code"}},"type":"object","required":["time_entry_id","code","message"],"title":"FailedEntryMerge","description":"Per-source rejection from a merge.\n\nSame structured shape as ``FailedEntryMove``: ``code`` is the\ncontract the client branches on, ``message`` the default English\ncopy. Only *sources* fail per-item — a problem with the target is\nshared by every source, so it rejects the whole request. The one\nexception is a target whose Segment takes no new time (a Closed\nMatter): that refuses only the sources on a *different* Segment, so it\nis reported per source."},"FailedEntryMove":{"properties":{"time_entry_id":{"type":"integer","title":"Time Entry Id","description":"ID of the time entry that failed to move"},"code":{"$ref":"#/components/schemas/APIErrorCode","description":"Structured failure code clients branch on for per-entry copy"},"message":{"type":"string","title":"Message","description":"Default human-readable message paired with code"}},"type":"object","required":["time_entry_id","code","message"],"title":"FailedEntryMove","description":"Per-entry rejection from the bulk entry move.\n\nSame structured shape as ``FailedRelease``: ``code`` is the contract\nthe client branches on, ``message`` the default English copy."},"FailedRelease":{"properties":{"time_entry_id":{"type":"integer","title":"Time Entry Id","description":"ID of the time entry that failed to release"},"code":{"$ref":"#/components/schemas/APIErrorCode","description":"Structured failure code clients branch on for per-entry copy"},"message":{"type":"string","title":"Message","description":"Default human-readable message paired with code"}},"type":"object","required":["time_entry_id","code","message"],"title":"FailedRelease","description":"Per-entry rejection from the release pipeline.\n\n``code`` is the contract the client branches on (one of the\n``TIME_ENTRY_RELEASE_*`` values in ``APIErrorCode``); ``message`` is\nthe default English copy for clients that don't yet recognise the\ncode. The structured shape mirrors the persisted-async-error\nconvention documented in ``server/AGENTS.md`` → \"Persisted async\nerrors\"."},"FeatureUsedPayload":{"properties":{"feature":{"type":"string","maxLength":64,"title":"Feature"},"app_session_id":{"type":"string","maxLength":64,"title":"App Session Id"},"sequence":{"type":"integer","minimum":0.0,"title":"Sequence"},"context":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Context"}},"type":"object","required":["feature","app_session_id","sequence"],"title":"FeatureUsedPayload"},"GenerateTimeEntriesRequest":{"properties":{"date":{"type":"string","format":"date","title":"Date","description":"Date to generate entries for"},"trigger":{"anyOf":[{"$ref":"#/components/schemas/GenerationTrigger"},{"type":"null"}],"description":"What the user did. When set, the server decides how deeply to rebuild and whether a rebuild is worth minting at all, and `mode` is ignored. When omitted the request is read as coming from a client predating this field, and `mode` is honoured exactly as it always was — see that field."},"mode":{"anyOf":[{"$ref":"#/components/schemas/GenerationMode"},{"type":"null"}],"description":"full: reset the day's watermark to day-start and regenerate all non-released entries. after: additively file every ungenerated unassigned block plus incomplete-skeleton reclaim. Unattached blocks are eligible regardless of the watermark (so back-dated or dragged-out blocks are picked up), so this is not limited to work after the mark; the watermark cutoff applies only to skeleton reclaim. Existing entries are never modified — the cutoff is derived server-side from the daily_generation watermark; clients no longer send a time.\n\nDEPRECATED and removed next release. Honoured only for a request that sends no `trigger`, so a desktop build predating that field keeps the exact behaviour it was written against — including `full` replacing entries the user edited, which it showed a confirmation for. New clients send `trigger` and let the server choose.","deprecated":true}},"type":"object","required":["date"],"title":"GenerateTimeEntriesRequest","description":"Request model for on-demand time entry generation."},"GenerationMode":{"type":"string","enum":["full","after"],"title":"GenerationMode"},"GenerationRequestOutcome":{"type":"string","enum":["started","adopted","skipped"],"title":"GenerationRequestOutcome","description":"What the server did about this request.\n\n``skipped`` is a success, not a refusal — see\n``GenerationTaskResponse.outcome``."},"GenerationSkipReason":{"type":"string","enum":["spacing_floor","day_closed","failure_capped","no_blocks","nothing_new"],"title":"GenerationSkipReason","description":"Why the server declined to reprocess the day.\n\nPresent only on ``outcome=skipped``, and it exists because the client had no\nway to tell these apart: every one of them rendered as \"Already up to date\",\nincluding the two that are not remotely that. A day whose rebuild was failing\ntold the user their timesheet was current, for six hours at a time.\n\nMirrors the SKIP reasons of\n:func:`shared.time_entry_processing.refresh_gate.evaluate_refresh`. That gate\ntypes its reasons as plain strings (one field carries both trigger arms and\nskip reasons), so the mapping is by value in ``_skip_reason`` — which returns\n``None`` for anything it does not recognise, so a reason added to the gate\ndegrades to \"no reason given\" rather than a 500 on a response that was\notherwise fine. ``tests/unit/.../test_router_smoke.py`` pins the five.\n\nThe two arms a client should treat differently from the rest:\n``failure_capped`` is a broken day, and ``day_closed`` is one only \"Rebuild\nfrom scratch\" can reopen. Neither is \"up to date\".\n\n**``failure_capped`` no longer reaches a Refresh press**, because the gate\nstopped applying the cap to one — a press now runs, and if the run fails the\nuser started it and gets the failure card. It is still in the vocabulary and\nstill worth rendering: the focus trigger keeps the cap, a previous-release\nserver mid-rollout still sends it, and the value is the gate's to define\nrather than this endpoint's to prune. Do not read its presence here as\nevidence a press can still be capped."},"GenerationTaskResponse":{"properties":{"outcome":{"$ref":"#/components/schemas/GenerationRequestOutcome","description":"What the server did. `started` minted a new run; `adopted` returned one already in flight, whoever started it; `skipped` decided the day did not need reprocessing and minted nothing.\n\n**`skipped` is a success.** The client refreshed its data either way — only the AI reprocessing was declined, because the day was rebuilt moments ago or nothing has changed since. It must not be surfaced as an error or a refusal; the freshness label already tells the user how current the timesheet is.","default":"started"},"skip_reason":{"anyOf":[{"$ref":"#/components/schemas/GenerationSkipReason"},{"type":"null"}],"description":"Why the day was not reprocessed. Set only when `outcome` is `skipped`; null otherwise, and null for a skip reason this server does not name.\n\n**Not every skip means the timesheet is current**, which is the whole reason this field exists. `nothing_new` and `no_blocks` do. `failure_capped` means this day's rebuild has failed repeatedly and automatic ones have stood down — say so and offer a retry. `day_closed` means the day is finalized and only a confirmed rebuild will reopen it. `spacing_floor` means a run just happened and another is due shortly."},"task_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Task Id","description":"Unique task identifier. Null only when `outcome` is `skipped` — there is no run to watch."},"date":{"type":"string","format":"date","title":"Date","description":"The date this generation targets"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"Current task status. Null when `outcome` is `skipped`. A string rather than a closed enum: the adoption path returns an existing row, which may carry a status a newer pod wrote. See decision record 0007."},"error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Code","description":"Machine-readable code (an APIErrorCode value, e.g. GENERATION_AI_INVALID_RESPONSE) for the terminal failure. Only populated when status=failed. Pair with `error` for fallback rendering."},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error","description":"Default human-readable message for the terminal failure, matching `error_code`. Show as-is if the client doesn't recognise the code."},"retry_count":{"type":"integer","title":"Retry Count","description":"Number of autoretries the worker has performed for this task.","default":0},"max_retries":{"type":"integer","title":"Max Retries","description":"Maximum number of autoretries the worker will perform before giving up. Combined with retry_count, lets the client show an 'attempt N of M' progress indicator while the worker is in a retry chain."},"last_error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error Code","description":"Machine-readable code for the most recent transient failure while the task is still retrying. Distinct from `error_code`, which is set only on terminal failure."},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error","description":"Default human-readable message for the most recent transient failure while still retrying. Pair with `last_error_code`."}},"type":"object","required":["date","max_retries"],"title":"GenerationTaskResponse","description":"Response for generation task creation and status polling.\n\nFailure information uses the same (code, message) shape as the\nsynchronous APIException envelope, but persisted on the row and\nsurfaced asynchronously. The client branches on the *_code field\nfor localized rendering and falls back to the *_message text for\ncodes it doesn't yet know.\n\nSee server/AGENTS.md → \"Persisted async errors\" for the contract."},"GenerationTaskSyncDto":{"properties":{"id":{"type":"string","title":"Id"},"organization_id":{"type":"string","title":"Organization Id"},"user_id":{"type":"string","title":"User Id"},"date":{"type":"string","format":"date","title":"Date"},"status":{"type":"string","title":"Status"},"phase":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phase"},"is_rebuild":{"type":"boolean","title":"Is Rebuild","default":true},"error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Code"},"error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error"},"retry_count":{"type":"integer","title":"Retry Count"},"last_error_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error Code"},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"version":{"type":"integer","title":"Version"}},"type":"object","required":["id","organization_id","user_id","date","status","retry_count","created_at","updated_at","version"],"title":"GenerationTaskSyncDto","description":"A generation task as seen by the cursor sync feed.\n\nInternal sweep bookkeeping (``heartbeat_at``,\n``last_dispatched_at``, ``dispatch_count``) is excluded — clients\nonly care about user-visible state transitions. The specialized\nDB trigger also suppresses ``version`` bumps when only those\ncolumns change, so the heartbeat (every ~30s per running task)\ndoes not flood the cursor feed.\n\n``phase`` IS surfaced: it is the user-visible sub-status within\n``status='running'`` (``segmenting | narrating | assembling``)\nthat lets the desktop client tell \"clustering\" apart from\n\"writing narratives\" and drive the generation view off a single\nsynced record. The trigger already bumps ``version`` on every\nphase change, so transitions reach clients in real time. ``None``\nwhenever the task is not actively running."},"GenerationTrigger":{"type":"string","enum":["refresh","focus","rebuild"],"title":"GenerationTrigger","description":"What the user did, in the client's vocabulary.\n\nDeliberately not the same word list as ``GenerationProducer``: this is the\n*public* surface, so it names gestures (\"the user pressed Refresh\") rather\nthan server-side producers, and the server maps it. The mapping is where\nthe app's focus trigger becomes distinguishable from a press without the\nclient having to know what a producer is."},"HeadlineAttemptStatus":{"type":"string","enum":["not_attempted","succeeded","skipped_no_signal","failed_terminal"],"title":"HeadlineAttemptStatus","description":"Vocabulary for `time_blocks.headline_attempt_status`.\n\nStored as a `(str, Enum)` so the cursor-sync wire DTO can type the\nfield as the enum and Pydantic v2 serializes to the bare value\n('not_attempted' etc.) — the qualified-name footgun of plain\n`str(EnumMember)` is avoided."},"ImageUploadResponse":{"properties":{"storage_key":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Storage Key","description":"Storage key for the uploaded image; null when rejected"},"rejected":{"type":"boolean","title":"Rejected","description":"True when the upload was refused and nothing was stored","default":false},"code":{"anyOf":[{"$ref":"#/components/schemas/APIErrorCode"},{"type":"null"}],"description":"Structured refusal reason when rejected"}},"type":"object","title":"ImageUploadResponse","description":"Response for direct image upload.\n\nCarries a refusal as well as a success: an image whose referenced\nsession/span belongs to a different ``(user, org)`` comes back with\n``rejected=true``, a ``code``, and no ``storage_key`` — nothing is\nwritten. See ``APIErrorCode.CAPTURE_IMAGE_OWNERSHIP_MISMATCH`` for why\nthat rides a 200 rather than a 4xx."},"LatestManifestResponse":{"properties":{"version":{"type":"string","title":"Version"},"urgency":{"type":"string","enum":["normal","asap"],"title":"Urgency"},"notes":{"type":"string","title":"Notes"},"pub_date":{"type":"string","title":"Pub Date"},"platforms":{"additionalProperties":{"$ref":"#/components/schemas/PlatformEntry"},"type":"object","title":"Platforms"}},"type":"object","required":["version","urgency","notes","pub_date","platforms"],"title":"LatestManifestResponse","description":"`GET /api/v1/updates/latest` 200 body.\n\nConsumed by three clients today: the desktop app's auto-updater\n(`tauri_plugin_updater::Updater::check` reads `version` / `pub_date` /\n`platforms`; the runner reads `urgency` from the parsed body), the\nadmin DownloadsPage, and the admin \"Check Latest\" dev dialog. Any\nfield rename here ripples to all three."},"LoginFailedPayload":{"properties":{"error_kind":{"type":"string","maxLength":64,"title":"Error Kind"}},"type":"object","required":["error_kind"],"title":"LoginFailedPayload"},"LoginSucceededPayload":{"properties":{"method":{"type":"string","maxLength":32,"title":"Method"}},"type":"object","required":["method"],"title":"LoginSucceededPayload"},"LogoutPayload":{"properties":{"reason":{"type":"string","maxLength":32,"title":"Reason"}},"type":"object","required":["reason"],"title":"LogoutPayload"},"LogoutRequest":{"properties":{"refresh_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refresh Token","description":"Refresh token (optional for non-browser clients)"}},"type":"object","title":"LogoutRequest"},"MS365CallbackRequest":{"properties":{"code":{"type":"string","title":"Code","description":"Authorization code from Microsoft"},"state":{"type":"string","title":"State","description":"OAuth state parameter received from Microsoft"},"code_verifier":{"type":"string","title":"Code Verifier","description":"PKCE code verifier (original random string)"},"redirect_uri":{"type":"string","title":"Redirect Uri","description":"Same redirect URI used in initiate request"}},"type":"object","required":["code","state","code_verifier","redirect_uri"],"title":"MS365CallbackRequest"},"MS365InitiateRequest":{"properties":{"username":{"type":"string","maxLength":1024,"title":"Username","description":"User email address"},"code_challenge":{"type":"string","title":"Code Challenge","description":"PKCE code challenge (SHA256 of code_verifier)"},"redirect_uri":{"type":"string","title":"Redirect Uri","description":"Desktop app callback URI (e.g., http://localhost:PORT)"}},"type":"object","required":["username","code_challenge","redirect_uri"],"title":"MS365InitiateRequest"},"MS365InitiateResponse":{"properties":{"authorization_url":{"type":"string","title":"Authorization Url","description":"Microsoft authorization URL to open in browser"},"state":{"type":"string","title":"State","description":"OAuth state parameter for CSRF protection"}},"type":"object","required":["authorization_url","state"],"title":"MS365InitiateResponse"},"MatterSegmentSummaryDto":{"properties":{"matter_id":{"type":"string","title":"Matter Id","description":"Matter ID"},"matter_name":{"type":"string","title":"Matter Name","description":"Matter name"},"segment_id":{"type":"string","title":"Segment Id","description":"Segment ID"},"segment_name":{"type":"string","title":"Segment Name","description":"Segment name"},"segment_order":{"type":"integer","title":"Segment Order","description":"Display order of the segment within the matter"},"is_root":{"type":"boolean","title":"Is Root","description":"Whether this is the root segment"},"root_segment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Root Segment Id","description":"Root segment ID of the matter"}},"type":"object","required":["matter_id","matter_name","segment_id","segment_name","segment_order","is_root"],"title":"MatterSegmentSummaryDto","description":"Summary of matter and segment for time blocks."},"MatterSyncDto":{"properties":{"id":{"type":"string","title":"Id"},"organization_id":{"type":"string","title":"Organization Id"},"client_id":{"type":"string","title":"Client Id"},"name":{"type":"string","title":"Name"},"external_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"External Id"},"status":{"type":"string","title":"Status","default":"open"},"closed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Closed At"},"billing_increment_minutes":{"type":"integer","title":"Billing Increment Minutes","default":6},"billing_rounding_rule":{"type":"string","title":"Billing Rounding Rule","default":"up"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"},"version":{"type":"integer","title":"Version"}},"type":"object","required":["id","organization_id","client_id","name","created_at","updated_at","version"],"title":"MatterSyncDto","description":"A matter as seen by the cursor sync feed.\n\n``external_id`` is on the wire because it is a search key the picker\nmatches against (firms file by matter number). ``description`` is\nnot: no client surface renders it, and this module ships only what\nthe client consumes.\n\n``status`` rides the wire and the client filters on it, for the reason\n``ClientSyncDto`` gives for its ``is_active``."},"MergeTimeEntriesRequest":{"properties":{"source_time_entry_ids":{"items":{"type":"integer"},"type":"array","maxItems":500,"minItems":1,"title":"Source Time Entry Ids","description":"Entries to fold into the target. Each is discarded once its blocks have moved."},"target_time_entry_id":{"type":"integer","title":"Target Time Entry Id","description":"The surviving entry. Keeps its own narrative, matter, code and billability."}},"type":"object","required":["source_time_entry_ids","target_time_entry_id"],"title":"MergeTimeEntriesRequest","description":"Request model for folding 1..N time entries into another one.\n\nBacks the desktop gesture of dropping one or more entries onto\nanother entry's row. Every source's time blocks move to the target\nand each source is discarded; the target keeps its own narrative,\nmatter/segment, activity code and billable flag — see\n``docs/decisions/0008-a-merge-keeps-the-target-entry.md``.\n\nNothing about the surviving entry's content is client-supplied. In\nparticular the resulting legal time is the server's to decide\n(``resolve_merged_legal_time``), because the rule depends on whether\neach figure was typed by a person — state the client mirrors but does\nnot own."},"MergeTimeEntriesResponse":{"properties":{"time_entry":{"$ref":"#/components/schemas/TimeEntrySyncDto","description":"The surviving target entry, post-merge"},"merged_blocks":{"items":{"$ref":"#/components/schemas/TimeBlockSyncDto"},"type":"array","title":"Merged Blocks","description":"Every time block this merge moved, in its post-merge state — normally re-parented onto the target"},"discarded":{"items":{"$ref":"#/components/schemas/TimeEntrySyncDto"},"type":"array","title":"Discarded","description":"The merged-away source entries, tombstoned (deleted_at populated)"},"failed":{"items":{"$ref":"#/components/schemas/FailedEntryMerge"},"type":"array","title":"Failed","description":"Per-source rejections with structured codes; these entries were not touched"}},"type":"object","required":["time_entry"],"title":"MergeTimeEntriesResponse","description":"Partial-success envelope for a merge.\n\nEvery row is in cursor-sync wire shape and carries its post-mutation\n``version``, so the desktop client force-applies the whole response\nto its local SQLite mirror rather than waiting a round: the target\nrow gains the minutes, the discarded rows arrive as tombstones and\nevict, and the moved blocks re-parent.\n\nA source in ``failed`` ships no row data — the server never touched\nit, so the client's local row is already correct. When ``failed``\naccounts for every source, nothing was written at all and\n``time_entry`` is the untouched target."},"MoveTimeBlockResponse":{"properties":{"time_block":{"$ref":"#/components/schemas/TimeBlockSyncDto","description":"The mutated time block"},"affected_time_entries":{"items":{"$ref":"#/components/schemas/TimeEntrySyncDto"},"type":"array","title":"Affected Time Entries","description":"Entries whose tracked_duration_minutes (or membership) changed"}},"type":"object","required":["time_block"],"title":"MoveTimeBlockResponse","description":"Response model for moving a time block between entries.\n\nReturns the modified block plus every entry whose duration or\nmembership changed (source + target). Both in the cursor-sync wire\nshape so the desktop client can apply the response directly to local\nSQLite."},"MoveTimeBlocksRequest":{"properties":{"time_block_ids":{"items":{"type":"integer"},"type":"array","maxItems":500,"minItems":1,"title":"Time Block Ids","description":"Time blocks to move"},"time_entry_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Time Entry Id","description":"Target time entry ID. Set to null to detach every block."}},"type":"object","required":["time_block_ids","time_entry_id"],"title":"MoveTimeBlocksRequest","description":"Request model for the bulk time-block move.\n\nBacks the desktop client's multi-select drag; a single-block drag is\na one-element list through the same path. All blocks move to ONE\ntarget (or detach together) — the drag gesture has exactly one drop\ntarget."},"MoveTimeBlocksResponse":{"properties":{"moved":{"items":{"$ref":"#/components/schemas/TimeBlockSyncDto"},"type":"array","title":"Moved","description":"Successfully moved blocks in cursor-sync wire shape"},"failed":{"items":{"$ref":"#/components/schemas/FailedBlockMove"},"type":"array","title":"Failed","description":"Per-block rejections with structured codes"},"affected_time_entries":{"items":{"$ref":"#/components/schemas/TimeEntrySyncDto"},"type":"array","title":"Affected Time Entries","description":"Entries whose tracked_duration_minutes (or membership) changed"},"moved_from":{"items":{"$ref":"#/components/schemas/BlockMoveOrigin"},"type":"array","title":"Moved From","description":"Pre-move parents of actually-moved blocks (no-op members excluded)"}},"type":"object","title":"MoveTimeBlocksResponse","description":"Partial-success envelope for the bulk block move.\n\n``moved`` and ``affected_time_entries`` are cursor-sync wire rows\n(the desktop client force-applies them to its local SQLite mirror);\n``failed`` carries per-block rejections with structured codes. A\nfailed block ships no row data — the server never touched it, so the\nclient's local row is already correct. ``moved_from`` lists the\nauthoritative pre-move parents of the rows that actually changed\n(no-op members excluded) — the client's Undo source of truth."},"MoveTimeEntriesRequest":{"properties":{"time_entry_ids":{"items":{"type":"integer"},"type":"array","maxItems":500,"minItems":1,"title":"Time Entry Ids","description":"Time entries to move"},"segment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Segment Id","description":"Target segment ID. Set to null to move to Unassigned."},"restore_legal_times":{"items":{"$ref":"#/components/schemas/EntryLegalTimeRestore"},"type":"array","title":"Restore Legal Times","description":"Undo only: typed legal times to put back verbatim instead of re-snapping, echoed from a prior response's ``moved_from``"}},"type":"object","required":["time_entry_ids","segment_id"],"title":"MoveTimeEntriesRequest","description":"Request model for bulk segment reassignment of time entries.\n\nBacks the desktop client's multi-select drag; a single-entry drag is\na one-element list through the same path. Billability is deliberately\nabsent — the server derives it from the target segment\n(``resolve_entry_billability``)."},"MoveTimeEntriesResponse":{"properties":{"moved":{"items":{"$ref":"#/components/schemas/TimeEntrySyncDto"},"type":"array","title":"Moved","description":"Successfully moved entries in cursor-sync wire shape"},"failed":{"items":{"$ref":"#/components/schemas/FailedEntryMove"},"type":"array","title":"Failed","description":"Per-entry rejections with structured codes"},"moved_from":{"items":{"$ref":"#/components/schemas/EntryMoveOrigin"},"type":"array","title":"Moved From","description":"Pre-move parents of actually-moved entries (no-op members excluded)"},"resnapped":{"items":{"type":"integer"},"type":"array","title":"Resnapped","description":"Entries whose typed legal time the destination Matter's Billing rounding moved. The new figure is on the matching ``moved`` row"}},"type":"object","title":"MoveTimeEntriesResponse","description":"Partial-success envelope for the bulk entry move.\n\n``moved`` carries the updated entries in cursor-sync wire shape (the\ndesktop client force-applies them to its local SQLite mirror);\n``failed`` carries per-entry rejections with structured codes. A\nfailed entry ships no row data — the server never touched it, so the\nclient's local row is already correct. ``moved_from`` lists the\nauthoritative pre-move parents of the rows that actually changed\n(no-op members excluded) — the client's Undo source of truth.\n\n``resnapped`` names the entries whose **typed** legal time the destination\nMatter's Billing rounding moved, so the client can say so rather than\nchanging a figure someone typed in silence. The new value is already on\nthat entry's ``moved`` row; this list only says which changed and why. An\nolder build that does not model the field drops it, and the move still\nlands correctly — the snap is the server's, not the client's."},"PageViewPayload":{"properties":{"page":{"type":"string","maxLength":64,"title":"Page"},"app_session_id":{"type":"string","maxLength":64,"title":"App Session Id"},"sequence":{"type":"integer","minimum":0.0,"title":"Sequence"}},"type":"object","required":["page","app_session_id","sequence"],"title":"PageViewPayload"},"PermissionEntitlementMissingPayload":{"properties":{"entitlement":{"type":"string","maxLength":255,"title":"Entitlement"},"app_version":{"type":"string","maxLength":32,"title":"App Version"}},"type":"object","required":["entitlement","app_version"],"title":"PermissionEntitlementMissingPayload"},"PermissionRequestDeniedPayload":{"properties":{"permission_key":{"type":"string","maxLength":128,"title":"Permission Key"},"target":{"anyOf":[{"type":"string","maxLength":255},{"type":"null"}],"title":"Target"},"denial_kind":{"type":"string","maxLength":64,"title":"Denial Kind"}},"type":"object","required":["permission_key","denial_kind"],"title":"PermissionRequestDeniedPayload"},"PermissionStateChangePayload":{"properties":{"permission_key":{"type":"string","maxLength":128,"title":"Permission Key"},"from":{"type":"string","maxLength":32,"title":"From"},"to":{"type":"string","maxLength":32,"title":"To"}},"type":"object","required":["permission_key","from","to"],"title":"PermissionStateChangePayload"},"PlatformEntry":{"properties":{"url":{"type":"string","title":"Url"},"signature":{"type":"string","title":"Signature"},"version":{"type":"string","title":"Version"},"pub_date":{"type":"string","title":"Pub Date"},"installer_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Installer Url"},"installer_size":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Installer Size"},"installer_filename":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Installer Filename"}},"type":"object","required":["url","signature","version","pub_date","installer_url","installer_size","installer_filename"],"title":"PlatformEntry","description":"Per-platform entry inside `LatestManifestResponse.platforms`.\n\n`pub_date` is a pre-formatted `\"%Y-%m-%dT%H:%M:%SZ\"` string (empty when\nunset) — kept as `str` to preserve byte-identical wire shape with the\nTauri-updater plugin's parser and the admin DownloadsPage consumer."},"RecordingConfigResponse":{"properties":{"apps":{"items":{"$ref":"#/components/schemas/AppConfig"},"type":"array","title":"Apps"}},"type":"object","required":["apps"],"title":"RecordingConfigResponse"},"RecordingErrorPayload":{"properties":{"session_id":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Session Id"},"error_kind":{"type":"string","maxLength":64,"title":"Error Kind"}},"type":"object","required":["error_kind"],"title":"RecordingErrorPayload"},"RecordingExtractionOutcomePayload":{"properties":{"span_id":{"type":"string","maxLength":64,"title":"Span Id"},"session_id":{"type":"string","maxLength":64,"title":"Session Id"},"strategy":{"type":"string","maxLength":32,"title":"Strategy"},"full_extraction":{"type":"boolean","title":"Full Extraction"},"had_metadata":{"type":"boolean","title":"Had Metadata"},"had_error":{"type":"boolean","title":"Had Error"},"error_kind":{"type":"string","maxLength":64,"title":"Error Kind"},"extraction_ms":{"type":"integer","minimum":0.0,"title":"Extraction Ms"},"attempt_n":{"type":"integer","minimum":1.0,"title":"Attempt N"},"attempt_kind":{"anyOf":[{"type":"string","maxLength":16},{"type":"null"}],"title":"Attempt Kind"},"policy_timeout_ms":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Policy Timeout Ms"}},"type":"object","required":["span_id","session_id","strategy","full_extraction","had_metadata","had_error","error_kind","extraction_ms","attempt_n"],"title":"RecordingExtractionOutcomePayload","description":"One direct-integration extraction attempt outcome.\n\nEmitted per dispatched extraction (identity or full) so each\n`MetadataExtracted` / `ExtractionFailed` result leaves a queryable\naudit trail. Silent-no-result failures (heavy AppleScript returns\nsuccessfully with no document) are invisible without this event."},"RecordingForceClosePayload":{"properties":{"cause":{"type":"string","enum":["idle","sleep","lock"],"title":"Cause"},"span_duration_ms":{"type":"integer","minimum":0.0,"title":"Span Duration Ms"},"gap_duration_ms":{"type":"integer","minimum":0.0,"title":"Gap Duration Ms"}},"type":"object","required":["cause","span_duration_ms","gap_duration_ms"],"title":"RecordingForceClosePayload","description":"One forced span close in the recording loop.\n\nEmitted next to each `span_tracker.force_close_current(...)` call\nin the desktop client's `recording/recording_service/detection_loop.rs`\nwhen the user is \"not actually working\" for a structural reason:\n\n- ``idle``  — keyboard / mouse idle past the client's\n  `IDLE_THRESHOLD` (≈ 600s). `gap_duration_ms` carries the\n  threshold-crossing idle window.\n- ``sleep`` — machine sleep / hibernate detected via the\n  client's dual-clock divergence. `gap_duration_ms` carries\n  the measured sleep duration.\n- ``lock``  — screen lock / screensaver transition detected.\n  `gap_duration_ms` is always ``0`` — the client only sees the\n  false→true transition edge at detection time, not how long\n  the lock had been on."},"RecordingQuotaChangedPayload":{"properties":{"blocked":{"type":"boolean","title":"Blocked"},"bytes_used":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Bytes Used"}},"type":"object","required":["blocked"],"title":"RecordingQuotaChangedPayload","description":"One screenshot-disk-quota edge on the desktop client.\n\n``blocked`` true means screenshot capture paused at the local disk\ncap; false means eviction freed space and capture resumed."},"RecordingSpanSendGivenUpPayload":{"properties":{"span_id":{"type":"string","maxLength":64,"title":"Span Id"},"attempts":{"type":"integer","minimum":0.0,"title":"Attempts"}},"type":"object","required":["span_id","attempts"],"title":"RecordingSpanSendGivenUpPayload","description":"One recorded span the pusher gave up re-sending.\n\nEmitted by the desktop recording pusher when a span has been\nrejected by ``POST /api/v1/span-updates`` ``send_attempts`` times\n(``MAX_SPAN_SEND_ATTEMPTS``) AND the unsent backlog has grown past\nthe batch window — the pusher force-marks it sent to keep the\n``started_at ASC`` feed flowing, accepting the loss of that one\nspan's billable activity. Rare and billing-relevant, so it is a\nserver-visible telemetry event (not just a local dev-dashboard\ncounter): a non-zero rate means recorded activity is being dropped\nand needs investigation (the per-item server log carries the actual\nDB failure reason, joined by ``span_id``)."},"RecordingStartBlockedPayload":{"properties":{"reason":{"type":"string","maxLength":64,"title":"Reason"}},"type":"object","required":["reason"],"title":"RecordingStartBlockedPayload","description":"One rejected user-initiated recording start.\n\n``reason`` is the desktop client's ``PreconditionFailure``\ndiscriminator (``not_authenticated`` / ``session_unverified`` /\n``user_settings_missing`` / ``permissions_missing``)."},"RecordingStartedPayload":{"properties":{"session_id":{"type":"string","maxLength":64,"title":"Session Id"},"trigger":{"type":"string","maxLength":32,"title":"Trigger"}},"type":"object","required":["session_id","trigger"],"title":"RecordingStartedPayload"},"RecordingStoppedPayload":{"properties":{"session_id":{"type":"string","maxLength":64,"title":"Session Id"},"reason":{"type":"string","maxLength":32,"title":"Reason"}},"type":"object","required":["session_id","reason"],"title":"RecordingStoppedPayload"},"RecordingUndeliverableCapturePayload":{"properties":{"image_id":{"type":"string","maxLength":64,"title":"Image Id"},"reason":{"type":"string","maxLength":32,"title":"Reason"}},"type":"object","required":["image_id","reason"],"title":"RecordingUndeliverableCapturePayload","description":"One capture the app took, told us about, and then lost on the user's\nown machine before it could be uploaded.\n\nEmitted by the desktop recording pusher and the screenshot-quota eviction\nwhen a staged PNG will never reach ``PUT /api/v1/capture-images/{id}`` —\nthe file gone from disk, the quota reclaiming it before the outbox drained,\nor a permanent server refusal.\n\n**This is the observability half, not the control path.** The capture's own\n``SCREENSHOT`` span event carries the same reason in its ``data``, and that\nis what stops extraction retrying an upload that is never coming (see\n``docs/decisions/0019-an-undeliverable-capture-is-declared-by-the-app.md``).\nThis event exists so the rate is readable per device and per reason without\njoining span data — and because the batch-level ``recording_error`` it\nreplaces named no image at all, which is precisely why a production\noccurrence could not be attributed to a client path.\n\nShould stay rare. A non-zero rate means capture bytes are being lost before\nupload, and ``reason`` says which of the three ways.\n\n``reason`` is deliberately an open ``str``, matching ``LoginFailedPayload``:\nthe app may learn a new way to lose a capture without needing a server\nrelease to report it."},"RefreshRequest":{"properties":{"refresh_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refresh Token","description":"Refresh token (optional for browser clients)"}},"type":"object","title":"RefreshRequest"},"RejectedEvent":{"properties":{"index":{"type":"integer","title":"Index"},"error_kind":{"type":"string","title":"Error Kind"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"}},"type":"object","required":["index","error_kind"],"title":"RejectedEvent","description":"One per-event rejection in the batch response."},"ReleaseCheckResponse":{"properties":{"eligible_ids":{"items":{"type":"integer"},"type":"array","title":"Eligible Ids","description":"IDs that would be released on Confirm"},"failed":{"items":{"$ref":"#/components/schemas/FailedRelease"},"type":"array","title":"Failed","description":"IDs that would be rejected, with codes"}},"type":"object","title":"ReleaseCheckResponse","description":"Preflight result for the release confirmation dialog.\n\nThe dialog calls ``POST /api/v1/time-entries/release/check`` before\nshowing the irreversible Confirm button and refuses to proceed\nwhile ``failed`` is non-empty. The same validator runs here and\ninside ``release_time_entries_bulk`` so the preview and the\nauthoritative gate cannot disagree."},"ReleaseTimeEntriesRequest":{"properties":{"time_entry_ids":{"items":{"type":"integer"},"type":"array","maxItems":500,"minItems":1,"title":"Time Entry Ids","description":"List of time entry IDs to release"}},"type":"object","required":["time_entry_ids"],"title":"ReleaseTimeEntriesRequest","description":"Request model for releasing multiple time entries."},"ReleaseTimeEntriesResponse":{"properties":{"released":{"items":{"$ref":"#/components/schemas/TimeEntryReleaseSyncDto"},"type":"array","title":"Released","description":"Successfully created release rows in cursor-sync wire shape"},"failed":{"items":{"$ref":"#/components/schemas/FailedRelease"},"type":"array","title":"Failed","description":"Per-entry rejections with structured codes"}},"type":"object","title":"ReleaseTimeEntriesResponse","description":"Response model for bulk release operation.\n\nThe ``released`` list carries the freshly-inserted release rows in\nthe cursor-sync wire shape (``TimeEntryReleaseSyncDto``); the\ndesktop client applies them directly to local SQLite via the same\nidempotent upsert used by ``GET /api/v1/sync``, so the user sees\n``is_released`` flip immediately without waiting for the next sync\nround."},"ScreenLockEventPayload":{"properties":{"locked_secs":{"type":"integer","minimum":0.0,"title":"Locked Secs","default":0}},"type":"object","title":"ScreenLockEventPayload","description":"One OS screen-lock edge (mirrors ``SystemEventPayload``).\n\n``locked_secs`` is 0 on the lock edge (the period is only\nmeasurable once it closes) and the measured duration on unlock."},"SegmentSyncDto":{"properties":{"id":{"type":"string","title":"Id"},"organization_id":{"type":"string","title":"Organization Id"},"matter_id":{"type":"string","title":"Matter Id"},"name":{"type":"string","title":"Name"},"is_root":{"type":"boolean","title":"Is Root"},"is_billable":{"type":"boolean","title":"Is Billable"},"is_active":{"type":"boolean","title":"Is Active"},"order":{"type":"integer","title":"Order"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"},"version":{"type":"integer","title":"Version"}},"type":"object","required":["id","organization_id","matter_id","name","is_root","is_billable","is_active","order","created_at","updated_at","version"],"title":"SegmentSyncDto","description":"A segment as seen by the cursor sync feed.\n\n``is_billable`` is what seeds a time entry's billability when the\nsegment is assigned (``resolve_entry_billability``), so the picker\nrenders it per row. See ``ClientSyncDto`` for the ``is_active``\nnote."},"ServerStatusResponse":{"properties":{"version":{"type":"string","title":"Version"},"environment":{"$ref":"#/components/schemas/TokumaEnvironment"}},"type":"object","required":["version","environment"],"title":"ServerStatusResponse"},"SpanEventData":{"properties":{"type":{"type":"string","maxLength":100,"title":"Type"},"at":{"type":"string","format":"date-time","title":"At"},"data":{"additionalProperties":true,"type":"object","title":"Data","default":{}}},"type":"object","required":["type","at"],"title":"SpanEventData"},"SpanRejectionResponse":{"properties":{"span_id":{"type":"string","title":"Span Id"},"code":{"$ref":"#/components/schemas/APIErrorCode"}},"type":"object","required":["span_id","code"],"title":"SpanRejectionResponse","description":"One rejected span-update, carrying the structured reason so the client\ncan branch. ``SPAN_OWNERSHIP_MISMATCH`` is permanent (dead-letter it);\n``SPAN_INGEST_FAILED`` is savepoint-survivable (hold back and retry)."},"SpanUpdateBatchRequest":{"properties":{"updates":{"items":{"$ref":"#/components/schemas/SpanUpdateRequest"},"type":"array","maxItems":50,"title":"Updates"}},"type":"object","required":["updates"],"title":"SpanUpdateBatchRequest"},"SpanUpdateBatchResponse":{"properties":{"processed":{"type":"integer","title":"Processed"},"failed_span_ids":{"items":{"type":"string"},"type":"array","title":"Failed Span Ids"},"rejections":{"items":{"$ref":"#/components/schemas/SpanRejectionResponse"},"type":"array","title":"Rejections"}},"type":"object","required":["processed"],"title":"SpanUpdateBatchResponse"},"SpanUpdateRequest":{"properties":{"device_id":{"type":"string","maxLength":255,"title":"Device Id"},"session_id":{"type":"string","maxLength":255,"title":"Session Id"},"span_id":{"type":"string","maxLength":255,"title":"Span Id"},"started_at":{"type":"string","format":"date-time","title":"Started At"},"ended_at":{"type":"string","format":"date-time","title":"Ended At"},"app":{"$ref":"#/components/schemas/AppInfo"},"attributes":{"oneOf":[{"$ref":"#/components/schemas/DocumentAttributes"},{"$ref":"#/components/schemas/DefaultAttributes"},{"$ref":"#/components/schemas/TeamsChatAttributes"},{"$ref":"#/components/schemas/ChatAttributes"},{"$ref":"#/components/schemas/EmailMessageAttributes"}],"title":"Attributes","default":{"type":"default"},"discriminator":{"propertyName":"type","mapping":{"chat":"#/components/schemas/ChatAttributes","default":"#/components/schemas/DefaultAttributes","document":"#/components/schemas/DocumentAttributes","email_message":"#/components/schemas/EmailMessageAttributes","teams_chat":"#/components/schemas/TeamsChatAttributes"}}},"events":{"items":{"$ref":"#/components/schemas/SpanEventData"},"type":"array","title":"Events","default":[]},"is_closed":{"type":"boolean","title":"Is Closed","default":false}},"type":"object","required":["device_id","session_id","span_id","started_at","ended_at","app"],"title":"SpanUpdateRequest"},"SuggestedSegmentRefDto":{"properties":{"segment_id":{"type":"string","title":"Segment Id","description":"Segment ID, in descending suggestion order"}},"type":"object","required":["segment_id"],"title":"SuggestedSegmentRefDto","description":"A suggested segment by reference — the client resolves the names.\n\nNo matter, client or segment names ride along: the client mirrors the\nmatter tree and already knows all of them."},"SuggestedSegmentsResponse":{"properties":{"suggested":{"items":{"$ref":"#/components/schemas/SuggestedSegmentRefDto"},"type":"array","title":"Suggested"}},"type":"object","required":["suggested"],"title":"SuggestedSegmentsResponse","description":"Suggestions alone, for clients that mirror matters locally.\n\nThe desktop app syncs clients / matters / segments through\n``GET /api/v1/sync`` and computes \"Recently used\" from its own\nmirrored ``time_entries`` ⋈ ``time_entry_releases``. Suggestions are\nthe one part it cannot derive: they are scored from\n``activities.segment_confidence_*``, and activities are telemetry\nthat is deliberately not synced.\n\nSo this endpoint is what remains of the picker's network surface —\na handful of ids, fetched once per dialog open."},"SyncResponse":{"properties":{"rows":{"$ref":"#/components/schemas/SyncRows"},"next_since":{"type":"string","title":"Next Since","description":"Opaque cursor blob to send back as `since` on the next call."},"has_more":{"type":"boolean","title":"Has More","description":"True when at least one resource hit its per-resource limit on this page; the client should immediately call again with `next_since`."},"server_time":{"type":"string","format":"date-time","title":"Server Time","description":"Server's wall-clock when the response was assembled. Diagnostic only — never used as a cursor."},"reset":{"items":{"type":"string"},"type":"array","title":"Reset","description":"Resources the client must wipe locally **before** applying this page. Sent when what the caller may see has changed: the feed has no tombstone for a row that was withdrawn rather than deleted, so cold-starting the slot alone would leave the withdrawn rows in place. Empty on virtually every round."}},"type":"object","required":["rows","next_since","has_more","server_time"],"title":"SyncResponse","description":"Cursor sync response envelope."},"SyncRows":{"properties":{"time_block":{"items":{"$ref":"#/components/schemas/TimeBlockSyncDto"},"type":"array","title":"Time Block"},"time_entry":{"items":{"$ref":"#/components/schemas/TimeEntrySyncDto"},"type":"array","title":"Time Entry"},"time_entry_release":{"items":{"$ref":"#/components/schemas/TimeEntryReleaseSyncDto"},"type":"array","title":"Time Entry Release"},"generation_task":{"items":{"$ref":"#/components/schemas/GenerationTaskSyncDto"},"type":"array","title":"Generation Task"},"user_settings":{"items":{"$ref":"#/components/schemas/UserSettingsSyncDto"},"type":"array","title":"User Settings"},"client":{"items":{"$ref":"#/components/schemas/ClientSyncDto"},"type":"array","title":"Client"},"matter":{"items":{"$ref":"#/components/schemas/MatterSyncDto"},"type":"array","title":"Matter"},"segment":{"items":{"$ref":"#/components/schemas/SegmentSyncDto"},"type":"array","title":"Segment"}},"type":"object","title":"SyncRows","description":"Per-resource row buckets returned by ``/api/v1/sync``."},"SyncStatus":{"type":"string","enum":["queued"],"title":"SyncStatus","description":"Status of a sync operation."},"SystemEventPayload":{"properties":{"gap_seconds":{"type":"integer","minimum":0.0,"title":"Gap Seconds","default":0}},"type":"object","title":"SystemEventPayload"},"TeamsChatAttributes":{"properties":{"type":{"type":"string","const":"teams_chat","title":"Type"},"chat_title":{"type":"string","maxLength":500,"title":"Chat Title"},"has_external_participants":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has External Participants"}},"type":"object","required":["type","chat_title"],"title":"TeamsChatAttributes","description":"Attributes for Microsoft Teams chat sessions.\n\nEach Teams chat conversation is its own session — chat_title keys the\nsession identity so the server groups per-chat TimeBlocks separately."},"TimeBlockDto":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier of the time block"},"start_time":{"type":"string","format":"date-time","title":"Start Time","description":"Start time of the block"},"end_time":{"type":"string","format":"date-time","title":"End Time","description":"End time of the block"},"headline":{"type":"string","title":"Headline","description":"Human-readable description of the time block"},"is_live":{"type":"boolean","title":"Is Live","description":"Whether the time block is live (still being updated)"},"app_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"App Id","description":"Application code"},"app_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"App Name","description":"Application display name"},"icon_label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Icon Label","description":"Short label (1-2 letters) for client-side icon display"},"matter_segment":{"anyOf":[{"$ref":"#/components/schemas/MatterSegmentSummaryDto"},{"type":"null"}],"description":"Matter and segment"},"time_entry_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Time Entry Id","description":"ID of the associated time entry"},"source_type":{"$ref":"#/components/schemas/TimeBlockSourceType","description":"Source of the time block: APP, MS365_CALL, MS365_EMAIL","default":"APP"}},"type":"object","required":["id","start_time","end_time","headline","is_live"],"title":"TimeBlockDto","description":"Data transfer object for a time block."},"TimeBlockListResponse":{"properties":{"time_blocks":{"items":{"$ref":"#/components/schemas/TimeBlockDto"},"type":"array","title":"Time Blocks","description":"List of time blocks for the specified date"}},"type":"object","required":["time_blocks"],"title":"TimeBlockListResponse","description":"Response model for a list of time blocks."},"TimeBlockSourceType":{"type":"string","enum":["APP","MS365_CALL","MS365_EMAIL","MS365_CALENDAR_EVENT"],"title":"TimeBlockSourceType","description":"Source of a time block."},"TimeBlockSyncDto":{"properties":{"id":{"type":"integer","title":"Id"},"organization_id":{"type":"string","title":"Organization Id"},"user_id":{"type":"string","title":"User Id"},"start_time":{"type":"string","format":"date-time","title":"Start Time"},"end_time":{"type":"string","format":"date-time","title":"End Time"},"headline":{"type":"string","title":"Headline"},"is_live":{"type":"boolean","title":"Is Live"},"app_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"App Id"},"app_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"App Name"},"segment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Segment Id"},"matter_segment":{"anyOf":[{"$ref":"#/components/schemas/MatterSegmentSummaryDto"},{"type":"null"}]},"activity_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Activity Code"},"time_entry_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Time Entry Id"},"source_type":{"$ref":"#/components/schemas/TimeBlockSourceType"},"headline_attempt_status":{"anyOf":[{"$ref":"#/components/schemas/HeadlineAttemptStatus"},{"type":"null"}]},"artifacts":{"$ref":"#/components/schemas/BlockArtifactsDto"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"},"version":{"type":"integer","title":"Version"}},"type":"object","required":["id","organization_id","user_id","start_time","end_time","headline","is_live","source_type","created_at","updated_at","version"],"title":"TimeBlockSyncDto","description":"A time block as seen by the cursor sync feed.\n\n``matter_segment`` is a denormalized snapshot of the segment + matter\ninfo at the time the row was returned. The desktop client renders\ndirectly from this; segment/matter renames take effect for a row only\nwhen the row's own ``version`` advances. Accepted as the\nrename-staleness compromise of the denormalized design.\n\n``version`` is the trigger-bumped global cursor counter (BIGINT).\nLocal mirror's apply predicate compares it as the freshness signal\n(``WHERE excluded.version > target.version``). ``updated_at`` is\ninformational only.\n\n``artifacts`` is projected from the internal ``time_blocks.facts``\nfact sheet by ``dto_builders.block_artifacts_dto`` — ``BlockFacts``\nitself is deliberately NOT on the wire, so it stays refactorable.\nOnly ``artifacts`` is projected today; ``sub_app`` / ``action`` /\n``counterparties`` / ``has_external`` have no client consumer, and\nper this module's contract we ship only what the client consumes.\nAdding one later is a DTO line plus a mapper line — the client\nstores the blob opaquely, so no migration is involved."},"TimeDisplayFormat":{"type":"string","enum":["time","decimal"],"title":"TimeDisplayFormat"},"TimeEntryDto":{"properties":{"id":{"type":"string","title":"Id","description":"Unique identifier of the time entry"},"date":{"type":"string","format":"date","title":"Date","description":"The date this entry belongs to"},"narrative":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Narrative","description":"Human-readable description of the time entry. NULL while a multi-stage generation is between the Segment commit (skeleton row created) and the Narrate fill-in for this entry — the desktop client renders a Skeleton component until the narrative lands."},"narrative_suggestions":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Narrative Suggestions","description":"AI-generated narrative alternatives"},"activity_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Activity Code","description":"Code of the activity"},"matter_segment":{"anyOf":[{"$ref":"#/components/schemas/MatterSegmentSummaryDto"},{"type":"null"}],"description":"Matter and segment"},"is_billable":{"type":"boolean","title":"Is Billable","description":"Whether the time entry is billable"},"is_released":{"type":"boolean","title":"Is Released","description":"Whether the time entry has been released","default":false},"released_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Released At","description":"When the time entry was released"},"legal_time_minutes":{"type":"integer","title":"Legal Time Minutes","description":"Legal time in minutes. Computed from tracked_duration_minutes and the matter's billing rounding unless a person typed it or the entry is released — legal_time_is_computed says which."},"legal_time_is_computed":{"type":"boolean","title":"Legal Time Is Computed","description":"True when legal_time_minutes was computed from tracked time and the matter's billing rounding, False when it is a stored figure (typed by a person, or frozen at release)."},"tracked_duration_minutes":{"type":"integer","title":"Tracked Duration Minutes","description":"Actual tracked time in minutes (sum of block durations without overlaps)"},"legal_time_pinned":{"type":"boolean","title":"Legal Time Pinned","description":"True iff legal_time_minutes was typed by a person (manual create or an API PATCH that supplied legal_time_minutes), False iff it is computed. Gates the regen sweep's manual-entry preservation. Cleared by a PATCH sending legal_time_minutes: null.","default":false},"legal_time_resnapped_from":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Legal Time Resnapped From","description":"The typed legal time this entry held immediately before a move re-rounded it onto another matter's billing rounding, or null when nothing has. Presentation only — legal_time_minutes is already the figure. When it clears is stated once, on the TimeEntry model column"},"is_user_created":{"type":"boolean","title":"Is User Created","description":"True iff a person created this entry through the API — the manual Add-entry dialog, or the drag that files captured blocks into a new entry. False for generation output. Read by the desktop's rebuild warning so its impact counts match the server's preserved set.","default":false},"created_at":{"type":"string","format":"date-time","title":"Created At","description":"When the time entry was created"},"updated_at":{"type":"string","format":"date-time","title":"Updated At","description":"When the time entry was last updated"}},"type":"object","required":["id","date","is_billable","legal_time_minutes","legal_time_is_computed","tracked_duration_minutes","created_at","updated_at"],"title":"TimeEntryDto","description":"Data transfer object for a time entry."},"TimeEntryListResponse":{"properties":{"time_entries":{"items":{"$ref":"#/components/schemas/TimeEntryDto"},"type":"array","title":"Time Entries","description":"List of time entries for the specified date"}},"type":"object","required":["time_entries"],"title":"TimeEntryListResponse","description":"Response model for a list of time entries."},"TimeEntryReleaseSyncDto":{"properties":{"id":{"type":"integer","title":"Id"},"organization_id":{"type":"string","title":"Organization Id"},"user_id":{"type":"string","title":"User Id"},"time_entry_id":{"type":"integer","title":"Time Entry Id"},"released_at":{"type":"string","format":"date-time","title":"Released At"},"export_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Export Id"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"version":{"type":"integer","title":"Version"}},"type":"object","required":["id","organization_id","user_id","time_entry_id","released_at","created_at","updated_at","version"],"title":"TimeEntryReleaseSyncDto","description":"A time entry release as seen by the cursor sync feed.\n\nInternal hint-reinforcement bookkeeping (``hints_processed_at``,\n``artifacts_processed_at``, ``hints_reinforced_count``,\n``artifacts_touched_count``) is deliberately excluded — the\nspecialized DB trigger also suppresses ``version`` bumps when only\nthose columns change so the cursor doesn't surface a no-op replay."},"TimeEntrySyncDto":{"properties":{"id":{"type":"integer","title":"Id"},"organization_id":{"type":"string","title":"Organization Id"},"user_id":{"type":"string","title":"User Id"},"date":{"type":"string","format":"date","title":"Date"},"legal_time_minutes":{"type":"integer","title":"Legal Time Minutes"},"legal_time_is_computed":{"type":"boolean","title":"Legal Time Is Computed"},"tracked_duration_minutes":{"type":"integer","title":"Tracked Duration Minutes"},"narrative":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Narrative"},"narrative_suggestions":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Narrative Suggestions"},"activity_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Activity Code"},"segment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Segment Id"},"matter_segment":{"anyOf":[{"$ref":"#/components/schemas/MatterSegmentSummaryDto"},{"type":"null"}]},"is_billable":{"type":"boolean","title":"Is Billable"},"legal_time_pinned":{"type":"boolean","title":"Legal Time Pinned"},"legal_time_resnapped_from":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Legal Time Resnapped From"},"is_user_created":{"type":"boolean","title":"Is User Created"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"deleted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Deleted At"},"version":{"type":"integer","title":"Version"}},"type":"object","required":["id","organization_id","user_id","date","legal_time_minutes","legal_time_is_computed","tracked_duration_minutes","narrative","is_billable","legal_time_pinned","is_user_created","created_at","updated_at","version"],"title":"TimeEntrySyncDto","description":"A time entry as seen by the cursor sync feed.\n\n``matter_segment`` is a denormalized snapshot — see\n``TimeBlockSyncDto`` for rename-staleness semantics. ``version`` is\nthe trigger-bumped cursor counter; ``updated_at`` is informational."},"TokenResponse":{"properties":{"access_token":{"type":"string","title":"Access Token","description":"JWT access token"},"token_type":{"type":"string","title":"Token Type","description":"Token type","default":"bearer"},"expires_in_seconds":{"type":"integer","title":"Expires In Seconds","description":"Token expiration time in seconds"},"scope":{"type":"string","title":"Scope","description":"OAuth2 scopes","default":""}},"type":"object","required":["access_token","expires_in_seconds"],"title":"TokenResponse"},"TokumaEnvironment":{"type":"string","enum":["local","dev","demo","staging","prod"],"title":"TokumaEnvironment"},"UpdateFailureReport":{"properties":{"device_id":{"type":"string","maxLength":64,"minLength":1,"title":"Device Id"},"from_version":{"type":"string","maxLength":32,"minLength":1,"title":"From Version"},"to_version":{"type":"string","maxLength":32,"minLength":1,"title":"To Version"},"target":{"type":"string","maxLength":32,"minLength":1,"title":"Target"},"error":{"type":"string","maxLength":5000,"minLength":1,"title":"Error"},"attempts":{"type":"integer","maximum":100.0,"minimum":1.0,"title":"Attempts"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"would_quarantine":{"type":"boolean","title":"Would Quarantine","default":false}},"type":"object","required":["device_id","from_version","to_version","target","error","attempts","occurred_at"],"title":"UpdateFailureReport","description":"Fire-and-forget report from the desktop app when ``download_and_install`` fails.\n\nUnauthenticated — identity is carried as ``device_id`` because the update check\nruns before login and while logged out."},"UpdateTargetMinutesRequest":{"properties":{"target_per_day_minutes":{"type":"integer","maximum":1440.0,"minimum":1.0,"title":"Target Per Day Minutes","description":"Daily target minutes for utilization calculation (1-1440)"}},"type":"object","required":["target_per_day_minutes"],"title":"UpdateTargetMinutesRequest"},"UpdateTargetMinutesResponse":{"properties":{"target_per_day_minutes":{"type":"integer","title":"Target Per Day Minutes","description":"Updated daily target minutes for utilization calculation"}},"type":"object","required":["target_per_day_minutes"],"title":"UpdateTargetMinutesResponse"},"UpdateTimeBlockRequest":{"properties":{"time_entry_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Time Entry Id","description":"Target time entry ID. Set to null to detach from entry."}},"type":"object","required":["time_entry_id"],"title":"UpdateTimeBlockRequest","description":"Request model for moving a time block to a different entry or detaching it."},"UpdateTimeEntryRequest":{"properties":{"narrative":{"anyOf":[{"type":"string","maxLength":5000,"minLength":1},{"type":"null"}],"title":"Narrative","description":"Description of the work performed"},"activity_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Activity Code","description":"Activity code for the work"},"segment_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Segment Id","description":"Segment ID to associate with this time entry"},"is_billable":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Billable","description":"Whether the time entry is billable"},"legal_time_minutes":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Legal Time Minutes","description":"Legal time in minutes, snapped to the matter's billing increment before it is stored. Sending an explicit null is the 'use tracked time' action: it clears the typed figure and returns the entry to a computed legal time. Refused with TIME_ENTRY_NO_BLOCKS on an entry with no live time blocks."},"acknowledge_legal_time_resnap":{"type":"boolean","title":"Acknowledge Legal Time Resnap","description":"Clear the flag that a move re-rounded this entry's typed legal time, keeping the figure as it stands. The lawyer's 'yes, that is fine' — it settles the note without changing any number","default":false}},"type":"object","title":"UpdateTimeEntryRequest","description":"Request model for updating an existing time entry."},"UpdateTimezoneRequest":{"properties":{"timezone":{"type":"string","title":"Timezone","description":"IANA timezone identifier (e.g., 'America/New_York', 'Asia/Tokyo')","examples":["UTC","America/New_York","Europe/London","Asia/Tokyo"]}},"type":"object","required":["timezone"],"title":"UpdateTimezoneRequest"},"UpdateTimezoneResponse":{"properties":{"timezone":{"type":"string","title":"Timezone","description":"Updated timezone (IANA timezone identifier)"}},"type":"object","required":["timezone"],"title":"UpdateTimezoneResponse"},"UpdateWeekStartDayRequest":{"properties":{"week_start_day":{"type":"integer","maximum":6.0,"minimum":0.0,"title":"Week Start Day","description":"First day of the week for calendar display: 0 = Sunday .. 6 = Saturday, matching JavaScript's Date.getDay(). Display-only; no server-side logic reads it.","examples":[0,1]}},"type":"object","required":["week_start_day"],"title":"UpdateWeekStartDayRequest"},"UpdateWeekStartDayResponse":{"properties":{"week_start_day":{"type":"integer","title":"Week Start Day","description":"Updated first day of the week (0 = Sunday)"}},"type":"object","required":["week_start_day"],"title":"UpdateWeekStartDayResponse"},"UploadClientLogRequest":{"properties":{"log_date":{"type":"string","format":"date","title":"Log Date","description":"Date of the log file (YYYY-MM-DD)"},"content":{"type":"string","maxLength":5000000,"title":"Content","description":"Log file content"},"app_version":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"App Version","description":"App version string"},"platform":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Platform","description":"Platform identifier (e.g. macos/aarch64)"}},"type":"object","required":["log_date","content"],"title":"UploadClientLogRequest"},"UploadClientLogResponse":{"properties":{"log_date":{"type":"string","format":"date","title":"Log Date","description":"Date of the uploaded log"},"status":{"type":"string","title":"Status","description":"Upload status","default":"ok"}},"type":"object","required":["log_date"],"title":"UploadClientLogResponse"},"UserSettingsSyncDto":{"properties":{"user_id":{"type":"string","title":"User Id"},"organization_id":{"type":"string","title":"Organization Id"},"timezone":{"type":"string","title":"Timezone"},"target_per_day_minutes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Target Per Day Minutes"},"week_start_day":{"type":"integer","title":"Week Start Day"},"fiscal_year_start_month":{"type":"integer","title":"Fiscal Year Start Month"},"fiscal_year_start_day":{"type":"integer","title":"Fiscal Year Start Day"},"time_display_format":{"type":"string","title":"Time Display Format"},"activity_codes_enabled":{"type":"boolean","title":"Activity Codes Enabled"},"default_billing_increment_minutes":{"type":"integer","title":"Default Billing Increment Minutes","default":6},"default_billing_rounding_rule":{"type":"string","title":"Default Billing Rounding Rule","default":"up"},"decimal_places":{"type":"integer","title":"Decimal Places","default":1},"updated_at":{"type":"string","format":"date-time","title":"Updated At"},"version":{"type":"integer","title":"Version"}},"type":"object","required":["user_id","organization_id","timezone","week_start_day","fiscal_year_start_month","fiscal_year_start_day","time_display_format","activity_codes_enabled","updated_at","version"],"title":"UserSettingsSyncDto","description":"A synthetic per-user settings row.\n\nJoins ``users`` and ``organizations`` on read; not backed by a\ndedicated table. ``version`` is ``GREATEST(u.version, o.version)``\nso any change to either source replays the row through the cursor\nfeed. Cursor ``id`` slot is the user_id. ``updated_at`` is\ninformational only (also a GREATEST over the two sources)."},"UserStatus":{"properties":{"user_id":{"type":"string","title":"User Id","description":"User ID"},"username":{"type":"string","title":"Username","description":"User username"},"timezone":{"type":"string","title":"Timezone","description":"User timezone (IANA timezone identifier)"},"display_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Display Name","description":"User display name"},"organization_id":{"type":"string","title":"Organization Id","description":"Organization ID"},"organization_name":{"type":"string","title":"Organization Name","description":"Organization name"},"office_id":{"type":"string","title":"Office Id","description":"Office ID"},"office_name":{"type":"string","title":"Office Name","description":"Office name"},"office_timezone":{"type":"string","title":"Office Timezone","description":"Office timezone (IANA timezone identifier)"},"department_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department Id","description":"Department ID (if user belongs to a department)"},"department_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department Name","description":"Department name (if user belongs to a department)"},"position_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Position Id","description":"Position ID (if user has a position)"},"position_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Position Name","description":"Position name (if user has a position)"},"region_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Region Id","description":"Region ID (if office belongs to a region)"},"region_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Region Name","description":"Region name (if office belongs to a region)"},"target_per_day_minutes":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Target Per Day Minutes","description":"Daily target minutes for utilization calculation"},"fiscal_year_start_month":{"type":"integer","title":"Fiscal Year Start Month","description":"Organization fiscal year start month (1-12)"},"fiscal_year_start_day":{"type":"integer","title":"Fiscal Year Start Day","description":"Organization fiscal year start day (1-31)"},"time_display_format":{"$ref":"#/components/schemas/TimeDisplayFormat","description":"Duration display format: 'time' (1:30) or 'decimal' (1.5)"},"activity_codes_enabled":{"type":"boolean","title":"Activity Codes Enabled","description":"Whether activity codes feature is enabled for the organization"},"default_billing_increment_minutes":{"type":"integer","title":"Default Billing Increment Minutes","description":"The firm's default billing increment in minutes — what a new matter starts with, and what an entry with no segment is computed with. A matter's own increment overrides it.","default":6},"default_billing_rounding_rule":{"type":"string","title":"Default Billing Rounding Rule","description":"The firm's default rounding rule: 'up', 'nearest' or 'down'","default":"up"},"feature_flags":{"additionalProperties":{"type":"boolean"},"type":"object","title":"Feature Flags","description":"User feature flags"}},"type":"object","required":["user_id","username","timezone","organization_id","organization_name","office_id","office_name","office_timezone","fiscal_year_start_month","fiscal_year_start_day","time_display_format","activity_codes_enabled"],"title":"UserStatus"},"UserSyncResponse":{"properties":{"status":{"$ref":"#/components/schemas/SyncStatus","description":"Sync status"},"user_id":{"type":"string","title":"User Id","description":"User ID that was queued for sync"}},"type":"object","required":["status","user_id"],"title":"UserSyncResponse","description":"Response from triggering a manual user sync."},"UserType":{"type":"string","enum":["MANUAL","MS365"],"title":"UserType"},"_AppFocusChanged":{"properties":{"type":{"type":"string","const":"app_focus_changed","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/AppFocusChangedPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_AppFocusChanged"},"_AppStarted":{"properties":{"type":{"type":"string","const":"app_started","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/AppStartedPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_AppStarted"},"_AppStopped":{"properties":{"type":{"type":"string","const":"app_stopped","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/AppStoppedPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_AppStopped"},"_BackgroundTaskError":{"properties":{"type":{"type":"string","const":"background_task_error","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/BackgroundTaskErrorPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_BackgroundTaskError"},"_ConnectivityChanged":{"properties":{"type":{"type":"string","const":"connectivity_changed","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/ConnectivityChangedPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_ConnectivityChanged"},"_FeatureUsed":{"properties":{"type":{"type":"string","const":"feature_used","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/FeatureUsedPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_FeatureUsed"},"_LoginFailed":{"properties":{"type":{"type":"string","const":"login_failed","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/LoginFailedPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_LoginFailed"},"_LoginSucceeded":{"properties":{"type":{"type":"string","const":"login_succeeded","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/LoginSucceededPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_LoginSucceeded"},"_Logout":{"properties":{"type":{"type":"string","const":"logout","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/LogoutPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_Logout"},"_PageView":{"properties":{"type":{"type":"string","const":"page_view","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/PageViewPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_PageView"},"_PermissionEntitlementMissing":{"properties":{"type":{"type":"string","const":"permission_entitlement_missing","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/PermissionEntitlementMissingPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_PermissionEntitlementMissing"},"_PermissionRequestDenied":{"properties":{"type":{"type":"string","const":"permission_request_denied","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/PermissionRequestDeniedPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_PermissionRequestDenied"},"_PermissionStateChange":{"properties":{"type":{"type":"string","const":"permission_state_change","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/PermissionStateChangePayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_PermissionStateChange"},"_RecordingError":{"properties":{"type":{"type":"string","const":"recording_error","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/RecordingErrorPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_RecordingError"},"_RecordingExtractionOutcome":{"properties":{"type":{"type":"string","const":"recording_extraction_outcome","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/RecordingExtractionOutcomePayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_RecordingExtractionOutcome"},"_RecordingForceClose":{"properties":{"type":{"type":"string","const":"recording_force_close","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/RecordingForceClosePayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_RecordingForceClose"},"_RecordingQuotaChanged":{"properties":{"type":{"type":"string","const":"recording_quota_changed","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/RecordingQuotaChangedPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_RecordingQuotaChanged"},"_RecordingSpanSendGivenUp":{"properties":{"type":{"type":"string","const":"recording_span_send_given_up","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/RecordingSpanSendGivenUpPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_RecordingSpanSendGivenUp"},"_RecordingStartBlocked":{"properties":{"type":{"type":"string","const":"recording_start_blocked","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/RecordingStartBlockedPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_RecordingStartBlocked"},"_RecordingStarted":{"properties":{"type":{"type":"string","const":"recording_started","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/RecordingStartedPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_RecordingStarted"},"_RecordingStopped":{"properties":{"type":{"type":"string","const":"recording_stopped","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/RecordingStoppedPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_RecordingStopped"},"_RecordingUndeliverableCapture":{"properties":{"type":{"type":"string","const":"recording_undeliverable_capture","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/RecordingUndeliverableCapturePayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_RecordingUndeliverableCapture"},"_ScreenLocked":{"properties":{"type":{"type":"string","const":"screen_locked","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/ScreenLockEventPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_ScreenLocked"},"_ScreenUnlocked":{"properties":{"type":{"type":"string","const":"screen_unlocked","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/ScreenLockEventPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_ScreenUnlocked"},"_SystemSleep":{"properties":{"type":{"type":"string","const":"system_sleep","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/SystemEventPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_SystemSleep"},"_SystemWake":{"properties":{"type":{"type":"string","const":"system_wake","title":"Type"},"occurred_at":{"type":"string","format":"date-time","title":"Occurred At"},"payload":{"$ref":"#/components/schemas/SystemEventPayload"}},"type":"object","required":["type","occurred_at","payload"],"title":"_SystemWake"}},"securitySchemes":{"OAuth2PasswordBearer":{"type":"oauth2","flows":{"password":{"scopes":{},"tokenUrl":"v1/auth/token"}}}}},"servers":[{"url":"/api"}]}