- docs/ARCHITECTURE.md - Clean Architecture patterns and conventions - docs/API_INTEGRATION.md - Core API and Tatlock API endpoints - docs/DEPLOYMENT.md - Docker, NPM, Portainer setup - docs/DATAGRID.md - DataGrid component specification - docs/THEMING.md - Material 3 theming guide Also update seed color to teal for consistency. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
425 lines
7.1 KiB
Markdown
425 lines
7.1 KiB
Markdown
# 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<ChatCompletionChunk> 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
|