openapi: 3.0.3 info: title: Family Assistant v1.5 API description: | API contract for Family Assistant v1.5 — trust and reliability features. **Host:** `api.hoffdesk.com` or `hoffdesk.com/api/v1` **Base Path:** `/api/v1` **Authentication:** Bearer token in `Authorization: Bearer ` version: 1.5.0 contact: name: Socrates (Backend) servers: - url: https://hoffdesk.com/api/v1 description: Production - url: http://titanium-butler:8000/api/v1 description: Local development (Tailscale) security: - bearerAuth: [] paths: # Undo Stack /undo: post: summary: List, undo, or redo operations description: | **TTL:** 10 minutes from execution. Only original sender can undo. Stack depth: 5 operations max. operationId: undoOperation requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UndoRequest' responses: '200': description: Successful operation content: application/json: schema: $ref: '#/components/schemas/UndoResponse' '400': description: Invalid request content: application/json: schema: $ref: '#/components/schemas/Error' '401': description: Unauthorized '410': description: Operation expired or not found # Conflict Registry /conflicts: get: summary: List outstanding conflicts description: | Returns all conflicts with status `unacknowledged` or `acknowledged`. Nag escalation: Matt DM after 3 alerts; group escalation if EOD unaddressed. operationId: listConflicts responses: '200': description: List of conflicts content: application/json: schema: $ref: '#/components/schemas/ConflictList' '401': description: Unauthorized /conflicts/{conflictId}/acknowledge: post: summary: Acknowledge a conflict (stops nagging) description: Marks conflict as acknowledged — stops alerts but keeps in registry. operationId: acknowledgeConflict parameters: - name: conflictId in: path required: true schema: type: string format: uuid responses: '200': description: Conflict acknowledged '404': description: Conflict not found /conflicts/{conflictId}/resolve: post: summary: Resolve a conflict with chosen action description: Records resolution action and optionally executes calendar changes. operationId: resolveConflict parameters: - name: conflictId in: path required: true schema: type: string format: uuid requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ConflictResolution' responses: '200': description: Conflict resolved content: application/json: schema: $ref: '#/components/schemas/ConflictResolutionResponse' '400': description: Invalid resolution action # Brain Query with Fallback /brain/query: post: summary: Query Family Brain with automatic fallback description: | Tries semantic search (embeddings) first, falls back to keyword search if Gaming PC is unreachable. Transparently reports mode used. operationId: queryBrain requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BrainQuery' responses: '200': description: Query result with mode indicator content: application/json: schema: $ref: '#/components/schemas/BrainResponse' # Calendar (Read-Only) /calendar/events: get: summary: Get calendar events for date range description: Read-only view of family calendar with travel time enrichment. operationId: listCalendarEvents parameters: - name: start in: query required: true schema: type: string format: date-time description: Start of range (ISO 8601 with timezone) - name: end in: query required: true schema: type: string format: date-time description: End of range (ISO 8601 with timezone) - name: include_travel in: query required: false schema: type: boolean default: true description: Include travel time from cached locations responses: '200': description: Events and conflicts in range content: application/json: schema: $ref: '#/components/schemas/CalendarEventsResponse' /calendar/today: get: summary: Get today's events (shortcut) operationId: getTodayEvents responses: '200': description: Today's events content: application/json: schema: $ref: '#/components/schemas/CalendarEventsResponse' /calendar/week: get: summary: Get current week's events (shortcut) description: Week starts on Sunday. Use week_offset for pagination. operationId: getWeekEvents parameters: - name: week_offset in: query required: false schema: type: integer default: 0 description: 0 = current week, -1 = last week, +1 = next week responses: '200': description: Week's events content: application/json: schema: $ref: '#/components/schemas/CalendarEventsResponse' # Digest Generation (Internal) /digest/generate: post: summary: Generate categorized newsletter digest description: | **Internal endpoint** — called by pipeline, not directly by clients. LLM-powered categorization with configurable categories. operationId: generateDigest requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DigestRequest' responses: '200': description: Categorized digest content: application/json: schema: $ref: '#/components/schemas/DigestResponse' # Dashboard Auth /dashboard/token: post: summary: Generate passwordless dashboard access token description: | Creates time-limited, revocable token for dashboard access. No password required — token is the credential. operationId: createDashboardToken requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/TokenRequest' responses: '201': description: Token created content: application/json: schema: $ref: '#/components/schemas/TokenResponse' /dashboard/token/{token}/revoke: post: summary: Revoke a dashboard token description: Immediately invalidates token — user must re-auth. operationId: revokeToken parameters: - name: token in: path required: true schema: type: string responses: '204': description: Token revoked # Health Check /health: get: summary: Service health status description: Includes embeddings reachability for Brain fallback mode. operationId: healthCheck security: [] # Public endpoint responses: '200': description: Health status content: application/json: schema: $ref: '#/components/schemas/HealthResponse' components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT # Actually Telegram bot token, but JWT-like format schemas: # Undo Stack UndoRequest: type: object required: - action properties: action: type: string enum: [list, undo, redo] description: Operation to perform operation_id: type: string format: uuid description: Required for undo/redo actions UndoResponse: type: object properties: operations: type: array items: $ref: '#/components/schemas/Operation' description: Present when action=list status: type: string enum: [undone, expired, not_found, restored] restored_event: $ref: '#/components/schemas/CalendarEvent' message: type: string example: "Sullivan Soccer restored to original time" Operation: type: object required: - id - type - summary - executed_at properties: id: type: string format: uuid type: type: string enum: [move, cancel, rename, add] summary: type: string example: "Sullivan Soccer" executed_at: type: string format: date-time reversible_until: type: string format: date-time description: 10 minute TTL from execution can_undo: type: boolean sender_id: type: string description: Telegram user ID who initiated snapshot: $ref: '#/components/schemas/EventSnapshot' EventSnapshot: type: object properties: event_id: type: string previous_state: type: object description: Full iCal VEVENT before change new_state: type: object description: Full iCal VEVENT after change # Conflicts ConflictList: type: object properties: outstanding: type: array items: $ref: '#/components/schemas/Conflict' Conflict: type: object required: - id - detected_at - event1 - event2 - overlap_minutes - status properties: id: type: string format: uuid detected_at: type: string format: date-time event1: $ref: '#/components/schemas/ConflictEvent' event2: $ref: '#/components/schemas/ConflictEvent' overlap_minutes: type: integer status: type: string enum: [unacknowledged, acknowledged, resolved] nag_count: type: integer description: Number of alerts sent (stops at 3, then escalates) resolution: $ref: '#/components/schemas/ConflictResolutionResult' ConflictEvent: type: object properties: summary: type: string start: type: string format: date-time end: type: string format: date-time ConflictResolution: type: object required: - resolution_action - choice_index properties: resolution_action: type: string enum: [split, reassign, reschedule] choice_index: type: integer description: Which option was selected (0-indexed) executed: type: boolean description: Whether calendar was actually modified ConflictResolutionResult: type: object properties: action: type: string enum: [split, reassign, reschedule] choice: type: integer executed_at: type: string format: date-time ConflictResolutionResponse: type: object properties: status: type: string enum: [resolved, already_resolved, failed] conflict: $ref: '#/components/schemas/Conflict' # Brain Query BrainQuery: type: object required: - question properties: question: type: string example: "What do the kids need for the field trip?" force_keyword: type: boolean default: false description: Bypass embeddings, use keyword search only BrainResponse: type: object properties: answer: type: string mode: type: string enum: [semantic, keyword_fallback] description: Which search mode was used sources: type: array items: $ref: '#/components/schemas/BrainSource' confidence: type: string enum: [high, medium, low] embeddings_available: type: boolean description: Whether Gaming PC was reachable BrainSource: type: object properties: type: type: string enum: [email, calendar, document] id: type: string snippet: type: string relevance_score: type: number # Calendar CalendarEventsResponse: type: object properties: events: type: array items: $ref: '#/components/schemas/CalendarEvent' conflicts: type: array items: $ref: '#/components/schemas/ConflictRef' CalendarEvent: type: object required: - id - summary - start - end properties: id: type: string format: uuid summary: type: string start: type: string format: date-time end: type: string format: date-time location: type: string nullable: true travel_time_minutes: type: integer nullable: true travel_from_home: type: boolean description: True if origin is home address status: type: string enum: [confirmed, tentative, cancelled] who: type: array items: type: string description: Family members from family.yaml color: type: string pattern: '^#[0-9a-fA-F]{6}$' description: Auto-assigned per person ConflictRef: type: object properties: id: type: string event1_id: type: string event2_id: type: string overlap_minutes: type: integer # Digest DigestRequest: type: object required: - items - style properties: items: type: array items: $ref: '#/components/schemas/NewsletterItem' style: type: string enum: [brief, detailed] NewsletterItem: type: object properties: raw_text: type: string source: type: string DigestResponse: type: object properties: sections: type: array items: $ref: '#/components/schemas/DigestSection' markdown: type: string description: Pre-formatted for Telegram DigestSection: type: object properties: category: type: string enum: [School, Sports, Medical, Community, Other] icon: type: string example: "🎒" items: type: array items: $ref: '#/components/schemas/DigestItem' DigestItem: type: object properties: summary: type: string who: type: array items: type: string action_required: type: boolean deadline: type: string format: date # Dashboard Auth TokenRequest: type: object required: - telegram_user_id properties: telegram_user_id: type: string description: Telegram user ID to create token for ttl_hours: type: integer default: 168 description: Token lifetime in hours (default 7 days) TokenResponse: type: object properties: token: type: string description: URL-safe token string dashboard_url: type: string format: uri example: "https://family.hoffdesk.com/?token=xyz123" expires_at: type: string format: date-time revoke_url: type: string format: uri # Health HealthResponse: type: object properties: status: type: string enum: [ok, degraded] version: type: string services: type: object properties: radicale: type: string enum: [up, down] chromadb: type: string enum: [up, down] embeddings: type: string enum: [up, down, unreachable] description: Gaming PC reachability # Error Error: type: object properties: error: type: string code: type: string details: type: object