📄 family-assistant-v1.5.yaml 17,893 bytes Apr 21, 2026 📋 Raw

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 <TELEGRAM_BOT_TOKEN>`

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