# API Integration Guide This document describes the backend APIs that Tatlock UI integrates with. ## Backend Services | Service | URL | Purpose | |---------|-----|---------| | Core API | `https://api.schweitz.net` | Infrastructure, widgets, housekeeping | | Tatlock API | `https://tatlock.schweitz.net` | LLM chat completions, streaming | Both APIs are behind Authentik SSO - requests must include a valid Bearer token. ## Authentication ### Authentik OIDC Flow Tatlock UI uses the Authorization Code flow with PKCE: 1. User clicks "Sign In" 2. App redirects to Authentik authorization endpoint 3. User authenticates with Authentik 4. Authentik redirects back with authorization code 5. App exchanges code for tokens 6. Access token used for API requests, refresh token for renewal ### Configuration | Setting | Value | |---------|-------| | Provider | Authentik | | Client ID | `tatlock-ui` | | Client Type | Public (PKCE) | | Scopes | `openid profile email` | | Discovery URL | `https://auth.schweitz.net/application/o/tatlock-ui/.well-known/openid-configuration` | ### Token Usage ```dart // Include in all API requests headers: { 'Authorization': 'Bearer $accessToken', 'Content-Type': 'application/json', } ``` --- ## Core API Endpoints Base URL: `https://api.schweitz.net` ### Infrastructure #### Get System Metrics ``` GET /infrastructure/resources/system ``` Response: ```json { "cpu": { "percent": 23.5, "cores": 8 }, "memory": { "percent": 45.2, "total_gb": 32.0, "used_gb": 14.5 }, "disk": { "percent": 67.8, "total_gb": 500.0, "used_gb": 339.0 } } ``` #### List Containers ``` GET /infrastructure/containers ``` Query params: - `status` (optional): Filter by status (running, stopped, paused) - `search` (optional): Search by name Response: ```json [ { "id": "abc123...", "name": "tatlock-api", "status": "running", "image": "ghcr.io/jpmschweitzer/tatlock:latest", "created": "2024-12-01T10:00:00Z", "ports": ["8000:8000"] } ] ``` #### Get Container Details ``` GET /infrastructure/containers/{id} ``` #### Get Container Logs ``` GET /infrastructure/containers/{id}/logs ``` Query params: - `tail` (optional): Number of lines (default: 100) - `since` (optional): ISO timestamp Response: ```json { "logs": "2024-12-30 10:00:00 INFO Starting server...\n..." } ``` #### Container Actions ``` POST /infrastructure/containers/{id}/{action} ``` Actions: `start`, `stop`, `restart`, `pause`, `unpause` Response: ```json { "success": true, "message": "Container restarted" } ``` #### Get Container Resources ``` GET /infrastructure/resources/containers ``` Response: ```json [ { "id": "abc123...", "name": "tatlock-api", "cpu_percent": 2.5, "memory_mb": 256, "memory_limit_mb": 1024 } ] ``` ### Dashboard #### Get Widget Data ``` GET /infrastructure/widget-data ``` Response: ```json { "groups": [ { "name": "Infrastructure", "services": [ { "name": "Portainer", "url": "https://portainer.schweitz.net", "icon": "portainer", "status": "up" } ] } ] } ``` #### Health Check ``` GET /health ``` Response: ```json { "status": "healthy", "timestamp": "2024-12-30T10:00:00Z" } ``` ### Housekeeping (Home Assistant) #### List Devices ``` GET /housekeeping/devices ``` Query params: - `area` (optional): Filter by area name - `domain` (optional): Filter by domain (light, switch, climate, etc.) Response: ```json [ { "entity_id": "light.living_room", "friendly_name": "Living Room Light", "domain": "light", "state": "on", "area": "Living Room", "attributes": { "brightness": 255, "color_temp": 370 } } ] ``` #### List Areas ``` GET /housekeeping/areas ``` Response: ```json [ { "id": "living_room", "name": "Living Room", "device_count": 5 } ] ``` #### List Scenes ``` GET /housekeeping/scenes ``` Response: ```json [ { "entity_id": "scene.movie_time", "friendly_name": "Movie Time", "area": "Living Room" } ] ``` #### Control Device ``` POST /housekeeping/devices/{entity_id}/control ``` Request: ```json { "action": "turn_on", "attributes": { "brightness": 200 } } ``` #### Activate Scene ``` POST /housekeeping/scenes/{scene_id}/activate ``` --- ## Tatlock API Endpoints Base URL: `https://tatlock.schweitz.net` ### Chat Completions (Streaming) ``` POST /v1/chat/completions ``` Request: ```json { "model": "tatlock", "messages": [ {"role": "system", "content": "You are Tatlock, a helpful butler."}, {"role": "user", "content": "What's the weather like?"} ], "stream": true } ``` Response (SSE stream): ``` data: {"id":"chatcmpl-123","choices":[{"delta":{"content":"The"}}]} data: {"id":"chatcmpl-123","choices":[{"delta":{"content":" weather"}}]} data: {"id":"chatcmpl-123","choices":[{"delta":{"reasoning_content":"Checking weather API..."}}]} data: [DONE] ``` **Important fields:** - `delta.content` - Main response text - `delta.reasoning_content` - Thinking/reasoning (show in collapsible block) ### List Models ``` GET /v1/models ``` Response: ```json { "data": [ { "id": "tatlock", "object": "model", "owned_by": "local" } ] } ``` --- ## SSE Streaming Implementation For chat completions, use Server-Sent Events: ```dart // Platform-aware SSE client class SseClient { Stream streamCompletion(ChatCompletionRequest request) async* { final response = await _client.post( '/v1/chat/completions', data: request.toJson(), options: Options( responseType: ResponseType.stream, headers: {'Accept': 'text/event-stream'}, ), ); await for (final chunk in response.data.stream) { final lines = utf8.decode(chunk).split('\n'); for (final line in lines) { if (line.startsWith('data: ') && line != 'data: [DONE]') { final json = jsonDecode(line.substring(6)); yield ChatCompletionChunk.fromJson(json); } } } } } ``` --- ## Error Handling ### Standard Error Response ```json { "error": { "code": "CONTAINER_NOT_FOUND", "message": "Container with ID 'xyz' not found", "details": {} } } ``` ### HTTP Status Codes | Code | Meaning | |------|---------| | 200 | Success | | 400 | Bad Request - Invalid parameters | | 401 | Unauthorized - Token expired or invalid | | 403 | Forbidden - Insufficient permissions | | 404 | Not Found - Resource doesn't exist | | 500 | Server Error - Backend issue | ### Handling in App ```dart class ApiException implements Exception { ApiException({required this.code, required this.message}); final String code; final String message; } // In interceptor if (response.statusCode == 401) { // Trigger token refresh or re-auth throw AuthException(); } ``` --- ## Rate Limiting Currently no rate limiting on internal APIs. For LLM endpoints, be mindful of: - Concurrent requests (limit to 1 active chat stream) - Token consumption (context window limits) --- ## API Documentation Interactive API docs available at: - Core API: https://api.schweitz.net/docs - Tatlock API: https://tatlock.schweitz.net/docs