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