dockerfiles added

This commit is contained in:
2025-11-06 13:39:42 +01:00
parent c004bcee24
commit e76cb3dc21
14 changed files with 663 additions and 20 deletions

197
UI_API_CONFIG.md Normal file
View File

@@ -0,0 +1,197 @@
# UI API Configuration
## Übersicht
Die UI-Anwendung verwendet keine hart codierten API-URLs mehr. Stattdessen wird die API-Basis-URL über die Umgebungsvariable `API_BASE` konfiguriert.
## Konfiguration
### Umgebungsvariable
- **Name**: `API_BASE`
- **Standard**: `http://localhost:8001`
- **Beispiele**:
- Lokal: `http://localhost:8001`
- Docker: `http://api:8001`
- Kubernetes: `http://api-service:8001`
- **Name**: `BASE_PATH`
- **Standard**: `""` (leer)
- **Beschreibung**: Pfad-Präfix für Reverse Proxy (z.B. `/ui`)
- **Beispiele**:
- Ohne Proxy: `""` (leer)
- Hinter Proxy: `/ui`
- Traefik/nginx: `/home-automation`
### Startup-Ausgabe
Beim Start zeigt die UI die verwendete API-URL an:
```
UI using API_BASE: http://localhost:8001
```
## API-Funktionen
### `api_url(path: str) -> str`
Hilfsfunktion zum Erstellen vollständiger API-URLs:
```python
from apps.ui.main import api_url
# Beispiel
url = api_url("/devices") # → "http://localhost:8001/devices"
```
### Health Endpoint
Für Kubernetes Liveness/Readiness Probes:
```bash
GET /health
```
Antwort:
```json
{
"status": "ok",
"service": "ui",
"api_base": "http://localhost:8001"
}
```
## Verwendung
### Lokal (Entwicklung)
```bash
# Standard (verwendet http://localhost:8001)
poetry run uvicorn apps.ui.main:app --host 0.0.0.0 --port 8002
# Mit anderer API
API_BASE=http://192.168.1.100:8001 poetry run uvicorn apps.ui.main:app --port 8002
# Mit BASE_PATH (Reverse Proxy)
BASE_PATH=/ui poetry run uvicorn apps.ui.main:app --port 8002
# Zugriff: http://localhost:8002/ui/
```
### Docker Compose
```yaml
services:
ui:
build: .
ports:
- "8002:8002"
environment:
- API_BASE=http://api:8001
- BASE_PATH="" # Leer für direkten Zugriff
depends_on:
- api
```
### Docker Compose mit Reverse Proxy
```yaml
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
ui:
build: .
environment:
- API_BASE=http://api:8001
- BASE_PATH=/ui # Pfad-Präfix für nginx
expose:
- "8002"
```
### Kubernetes
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ui
spec:
replicas: 2
selector:
matchLabels:
app: ui
template:
metadata:
labels:
app: ui
spec:
containers:
- name: ui
image: home-automation-ui:latest
env:
- name: API_BASE
value: "http://api-service:8001"
- name: BASE_PATH
value: "/ui" # Für Ingress
ports:
- containerPort: 8002
livenessProbe:
httpGet:
path: /ui/health # Mit BASE_PATH!
port: 8002
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /ui/health # Mit BASE_PATH!
port: 8002
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ui-ingress
spec:
rules:
- host: home.example.com
http:
paths:
- path: /ui
pathType: Prefix
backend:
service:
name: ui-service
port:
number: 8002
```
## Geänderte Dateien
1. **apps/ui/main.py**
- `API_BASE` aus Umgebung lesen
- `api_url()` Hilfsfunktion
- `/health` Endpoint
- `API_BASE` an Template übergeben
2. **apps/ui/api_client.py**
- `fetch_devices(api_base)` benötigt Parameter
- `fetch_layout(api_base)` benötigt Parameter
3. **apps/ui/templates/dashboard.html**
- JavaScript verwendet `{{ api_base }}` aus Backend
## Akzeptanz-Kriterien ✓
-`print(API_BASE)` zeigt korrekten Wert beim Start
- ✅ UI funktioniert lokal ohne Codeänderung
- ✅ Mit `API_BASE=http://api:8001` ruft UI korrekt den API-Service an
- ✅ Health-Endpoint für Kubernetes verfügbar
- ✅ Keine hart codierten URLs mehr
## Vorteile
1. **Flexibilität**: API-URL per ENV konfigurierbar
2. **Docker/K8s Ready**: Service Discovery unterstützt
3. **Health Checks**: Monitoring-Integration möglich
4. **Abwärtskompatibel**: Bestehende Deployments funktionieren weiter
5. **Clean Code**: Zentrale Konfiguration statt verteilte Hardcodes

View File

@@ -0,0 +1,44 @@
# Abstraction Layer Dockerfile
# MQTT ↔ Device Protocol Translation Worker
FROM python:3.14-alpine
# Prevent Python from writing .pyc files and enable unbuffered output
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
MQTT_BROKER=172.16.2.16 \
MQTT_PORT=1883 \
REDIS_HOST=localhost \
REDIS_PORT=6379 \
REDIS_DB=0
# Create non-root user
RUN addgroup -g 10001 -S app && \
adduser -u 10001 -S app -G app
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apk add --no-cache \
gcc \
musl-dev \
linux-headers
# Install Python dependencies
COPY apps/abstraction/requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY apps/__init__.py /app/apps/__init__.py
COPY apps/abstraction/ /app/apps/abstraction/
COPY packages/ /app/packages/
# Change ownership to app user
RUN chown -R app:app /app
# Switch to non-root user
USER app
# Run application
CMD ["python", "-m", "apps.abstraction.main"]

View File

@@ -0,0 +1,5 @@
pydantic>=2
aiomqtt==2.0.1
redis==5.0.1
pyyaml==6.0.1
tenacity==8.2.3

53
apps/api/Dockerfile Normal file
View File

@@ -0,0 +1,53 @@
# API Service Dockerfile
# FastAPI + Redis + MQTT Gateway
FROM python:3.14-alpine
# Prevent Python from writing .pyc files and enable unbuffered output
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
MQTT_BROKER=172.16.2.16 \
MQTT_PORT=1883 \
REDIS_HOST=localhost \
REDIS_PORT=6379 \
REDIS_DB=0 \
REDIS_CHANNEL=ui:updates
# Create non-root user
RUN addgroup -g 10001 -S app && \
adduser -u 10001 -S app -G app
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apk add --no-cache \
curl \
gcc \
musl-dev \
linux-headers
# Install Python dependencies
COPY apps/api/requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY apps/__init__.py /app/apps/__init__.py
COPY apps/api/ /app/apps/api/
COPY packages/ /app/packages/
# Change ownership to app user
RUN chown -R app:app /app
# Switch to non-root user
USER app
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8001/health || exit 1
# Expose port
EXPOSE 8001
# Run application
CMD ["python", "-m", "uvicorn", "apps.api.main:app", "--host", "0.0.0.0", "--port", "8001"]

View File

@@ -104,10 +104,12 @@ def load_devices() -> list[dict[str, Any]]:
def get_mqtt_settings() -> tuple[str, int]: def get_mqtt_settings() -> tuple[str, int]:
"""Get MQTT broker settings from environment. """Get MQTT broker settings from environment.
Supports both MQTT_BROKER and MQTT_HOST for compatibility.
Returns: Returns:
tuple: (host, port) tuple: (host, port)
""" """
host = os.environ.get("MQTT_HOST", "172.16.2.16") host = os.environ.get("MQTT_BROKER") or os.environ.get("MQTT_HOST", "172.16.2.16")
port = int(os.environ.get("MQTT_PORT", "1883")) port = int(os.environ.get("MQTT_PORT", "1883"))
return host, port return host, port
@@ -115,9 +117,24 @@ def get_mqtt_settings() -> tuple[str, int]:
def get_redis_settings() -> tuple[str, str]: def get_redis_settings() -> tuple[str, str]:
"""Get Redis settings from configuration. """Get Redis settings from configuration.
Prioritizes environment variables over config file:
- REDIS_HOST, REDIS_PORT, REDIS_DB → redis://host:port/db
- REDIS_CHANNEL → pub/sub channel name
Returns: Returns:
tuple: (url, channel) tuple: (url, channel)
""" """
# Check environment variables first
redis_host = os.getenv("REDIS_HOST")
redis_port = os.getenv("REDIS_PORT", "6379")
redis_db = os.getenv("REDIS_DB", "0")
redis_channel = os.getenv("REDIS_CHANNEL", "ui:updates")
if redis_host:
url = f"redis://{redis_host}:{redis_port}/{redis_db}"
return url, redis_channel
# Fallback to config file
config_path = Path(__file__).parent.parent.parent / "config" / "devices.yaml" config_path = Path(__file__).parent.parent.parent / "config" / "devices.yaml"
if config_path.exists(): if config_path.exists():

View File

@@ -0,0 +1,6 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
pydantic>=2
redis==5.0.1
aiomqtt==2.0.1
pyyaml==6.0.1

49
apps/simulator/Dockerfile Normal file
View File

@@ -0,0 +1,49 @@
# Simulator Service Dockerfile
# FastAPI Web UI + MQTT Device Simulator
FROM python:3.14-alpine
# Prevent Python from writing .pyc files and enable unbuffered output
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
MQTT_BROKER=172.16.2.16 \
MQTT_PORT=1883 \
SIM_PORT=8010
# Create non-root user
RUN addgroup -g 10001 -S app && \
adduser -u 10001 -S app -G app
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apk add --no-cache \
curl \
gcc \
musl-dev \
linux-headers
# Install Python dependencies
COPY apps/simulator/requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY apps/__init__.py /app/apps/__init__.py
COPY apps/simulator/ /app/apps/simulator/
# Change ownership to app user
RUN chown -R app:app /app
# Switch to non-root user
USER app
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8010/health || exit 1
# Expose port
EXPOSE 8010
# Run the simulator
CMD ["python", "-m", "uvicorn", "apps.simulator.main:app", "--host", "0.0.0.0", "--port", "8010"]

View File

@@ -0,0 +1,4 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
aiomqtt==2.0.1
jinja2==3.1.2

49
apps/ui/Dockerfile Normal file
View File

@@ -0,0 +1,49 @@
# UI Service Dockerfile
# FastAPI + Jinja2 + HTMX Dashboard
FROM python:3.14-alpine
# Prevent Python from writing .pyc files and enable unbuffered output
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
UI_PORT=8002 \
API_BASE=http://api:8001 \
BASE_PATH=""
# Create non-root user
RUN addgroup -g 10001 -S app && \
adduser -u 10001 -S app -G app
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apk add --no-cache \
curl \
gcc \
musl-dev \
linux-headers
# Install Python dependencies
COPY apps/ui/requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY apps/__init__.py /app/apps/__init__.py
COPY apps/ui/ /app/apps/ui/
# Change ownership to app user
RUN chown -R app:app /app
# Switch to non-root user
USER app
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:${UI_PORT}/health || exit 1
# Expose port
EXPOSE 8002
# Run application
CMD ["python", "-m", "uvicorn", "apps.ui.main:app", "--host", "0.0.0.0", "--port", "8002"]

171
apps/ui/README_DOCKER.md Normal file
View File

@@ -0,0 +1,171 @@
# UI Service - Docker
FastAPI + Jinja2 + HTMX Dashboard für Home Automation
## Build
```bash
docker build -t ui:dev -f apps/ui/Dockerfile .
```
## Run
### Lokal
```bash
docker run --rm -p 8002:8002 -e API_BASE=http://localhost:8001 ui:dev
```
### Docker Compose
```yaml
services:
ui:
build:
context: .
dockerfile: apps/ui/Dockerfile
ports:
- "8002:8002"
environment:
- API_BASE=http://api:8001
depends_on:
- api
```
### Kubernetes
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ui
spec:
replicas: 2
selector:
matchLabels:
app: ui
template:
metadata:
labels:
app: ui
spec:
containers:
- name: ui
image: ui:dev
ports:
- containerPort: 8002
env:
- name: API_BASE
value: "http://api-service:8001"
livenessProbe:
httpGet:
path: /health
port: 8002
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8002
initialDelaySeconds: 3
periodSeconds: 5
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: ui-service
spec:
selector:
app: ui
ports:
- protocol: TCP
port: 8002
targetPort: 8002
type: LoadBalancer
```
## Umgebungsvariablen
| Variable | Default | Beschreibung |
|----------|---------|--------------|
| `API_BASE` | `http://api:8001` | URL des API-Services |
| `UI_PORT` | `8002` | Port der UI-Anwendung |
| `PYTHONDONTWRITEBYTECODE` | `1` | Keine .pyc Files |
| `PYTHONUNBUFFERED` | `1` | Unbuffered Output |
## Endpoints
- `GET /` - Dashboard
- `GET /health` - Health Check
- `GET /dashboard` - Dashboard (alias)
## Security
- Container läuft als **non-root** User `app` (UID: 10001)
- Minimales Python 3.11-slim Base Image
- Keine unnötigen System-Pakete
- Health Check integriert
## Features
- ✅ FastAPI Backend
- ✅ Jinja2 Templates
- ✅ HTMX für reactive UI
- ✅ Server-Sent Events (SSE)
- ✅ Responsive Design
- ✅ Docker & Kubernetes ready
- ✅ Health Check Endpoint
- ✅ Non-root Container
- ✅ Configurable API Backend
## Entwicklung
### Lokales Testing
```bash
# Build
docker build -t ui:dev -f apps/ui/Dockerfile .
# Run
docker run -d --name ui-test -p 8002:8002 -e API_BASE=http://localhost:8001 ui:dev
# Logs
docker logs -f ui-test
# Health Check
curl http://localhost:8002/health
# Cleanup
docker stop ui-test && docker rm ui-test
```
### Tests
```bash
bash /tmp/test_ui_dockerfile.sh
```
## Troubleshooting
### Container startet nicht
```bash
docker logs ui-test
```
### Health Check schlägt fehl
```bash
docker exec ui-test curl http://localhost:8002/health
```
### API_BASE nicht korrekt
```bash
docker logs ui-test 2>&1 | grep "UI using API_BASE"
```
### Non-root Verifizieren
```bash
docker exec ui-test id
# Sollte zeigen: uid=10001(app) gid=10001(app)
```

View File

@@ -8,12 +8,12 @@ import httpx
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def fetch_devices(api_base: str = "http://localhost:8001") -> list[dict]: def fetch_devices(api_base: str) -> list[dict]:
""" """
Fetch devices from the API Gateway. Fetch devices from the API Gateway.
Args: Args:
api_base: Base URL of the API Gateway (default: http://localhost:8001) api_base: Base URL of the API Gateway (e.g., "http://localhost:8001" or "http://api:8001")
Returns: Returns:
List of device dictionaries. Each device contains at least: List of device dictionaries. Each device contains at least:
@@ -56,12 +56,12 @@ def fetch_devices(api_base: str = "http://localhost:8001") -> list[dict]:
return [] return []
def fetch_layout(api_base: str = "http://localhost:8001") -> dict: def fetch_layout(api_base: str) -> dict:
""" """
Fetch UI layout from the API Gateway. Fetch UI layout from the API Gateway.
Args: Args:
api_base: Base URL of the API Gateway (default: http://localhost:8001) api_base: Base URL of the API Gateway (e.g., "http://localhost:8001" or "http://api:8001")
Returns: Returns:
Layout dictionary with structure: Layout dictionary with structure:

View File

@@ -1,10 +1,11 @@
"""UI main entry point.""" """UI main entry point."""
import logging import logging
import os
from pathlib import Path from pathlib import Path
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
@@ -12,11 +13,30 @@ from apps.ui.api_client import fetch_devices, fetch_layout
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Initialize FastAPI app # Read configuration from environment variables
API_BASE = os.getenv("API_BASE", "http://localhost:8001")
BASE_PATH = os.getenv("BASE_PATH", "") # e.g., "/ui" for reverse proxy
print(f"UI using API_BASE: {API_BASE}")
print(f"UI using BASE_PATH: {BASE_PATH}")
def api_url(path: str) -> str:
"""Helper function to construct API URLs.
Args:
path: API path (e.g., "/devices")
Returns:
Full API URL
"""
return f"{API_BASE}{path}"
# Initialize FastAPI app with optional root_path for reverse proxy
app = FastAPI( app = FastAPI(
title="Home Automation UI", title="Home Automation UI",
description="User interface for home automation system", description="User interface for home automation system",
version="0.1.0" version="0.1.0",
root_path=BASE_PATH
) )
# Setup Jinja2 templates # Setup Jinja2 templates
@@ -29,6 +49,21 @@ static_dir.mkdir(exist_ok=True)
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static") app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
@app.get("/health")
async def health() -> JSONResponse:
"""Health check endpoint for Kubernetes/Docker.
Returns:
JSONResponse: Health status
"""
return JSONResponse({
"status": "ok",
"service": "ui",
"api_base": API_BASE,
"base_path": BASE_PATH
})
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
async def index(request: Request) -> HTMLResponse: async def index(request: Request) -> HTMLResponse:
"""Redirect to dashboard. """Redirect to dashboard.
@@ -53,11 +88,11 @@ async def dashboard(request: Request) -> HTMLResponse:
HTMLResponse: Rendered dashboard template HTMLResponse: Rendered dashboard template
""" """
try: try:
# Load layout from API # Load layout from API (use configured API_BASE)
layout_data = fetch_layout() layout_data = fetch_layout(API_BASE)
# Fetch devices from API (now includes features) # Fetch devices from API (now includes features)
api_devices = fetch_devices() api_devices = fetch_devices(API_BASE)
# Create device lookup by device_id # Create device lookup by device_id
device_map = {d["device_id"]: d for d in api_devices} device_map = {d["device_id"]: d for d in api_devices}
@@ -98,7 +133,8 @@ async def dashboard(request: Request) -> HTMLResponse:
return templates.TemplateResponse("dashboard.html", { return templates.TemplateResponse("dashboard.html", {
"request": request, "request": request,
"rooms": rooms "rooms": rooms,
"api_base": API_BASE # Pass API_BASE to template
}) })
except Exception as e: except Exception as e:
@@ -106,7 +142,8 @@ async def dashboard(request: Request) -> HTMLResponse:
# Fallback to empty dashboard # Fallback to empty dashboard
return templates.TemplateResponse("dashboard.html", { return templates.TemplateResponse("dashboard.html", {
"request": request, "request": request,
"rooms": [] "rooms": [],
"api_base": API_BASE # Pass API_BASE even on error
}) })

4
apps/ui/requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
jinja2==3.1.2
httpx==0.25.1

View File

@@ -519,7 +519,14 @@
</div> </div>
<script> <script>
const API_BASE = 'http://localhost:8001'; // API_BASE injected from backend (supports Docker/K8s environments)
window.API_BASE = '{{ api_base }}';
// Helper function to construct API URLs
function api(url) {
return `${window.API_BASE}${url}`;
}
let eventSource = null; let eventSource = null;
let currentState = {}; let currentState = {};
let thermostatTargets = {}; let thermostatTargets = {};
@@ -542,7 +549,7 @@
const newState = currentState[deviceId] === 'on' ? 'off' : 'on'; const newState = currentState[deviceId] === 'on' ? 'off' : 'on';
try { try {
const response = await fetch(`${API_BASE}/devices/${deviceId}/set`, { const response = await fetch(api(`/devices/${deviceId}/set`), {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -579,7 +586,7 @@
// Set brightness // Set brightness
async function setBrightness(deviceId, brightness) { async function setBrightness(deviceId, brightness) {
try { try {
const response = await fetch(`${API_BASE}/devices/${deviceId}/set`, { const response = await fetch(api(`/devices/${deviceId}/set`), {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -612,7 +619,7 @@
const newTarget = Math.max(5.0, Math.min(30.0, currentTarget + delta)); const newTarget = Math.max(5.0, Math.min(30.0, currentTarget + delta));
try { try {
const response = await fetch(`${API_BASE}/devices/${deviceId}/set`, { const response = await fetch(api(`/devices/${deviceId}/set`), {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -645,7 +652,7 @@
const currentTarget = thermostatTargets[deviceId] || 21.0; const currentTarget = thermostatTargets[deviceId] || 21.0;
try { try {
const response = await fetch(`${API_BASE}/devices/${deviceId}/set`, { const response = await fetch(api(`/devices/${deviceId}/set`), {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@@ -777,7 +784,7 @@
// Connect to SSE // Connect to SSE
function connectSSE() { function connectSSE() {
eventSource = new EventSource(`${API_BASE}/realtime`); eventSource = new EventSource(api('/realtime'));
eventSource.onopen = () => { eventSource.onopen = () => {
console.log('SSE connected'); console.log('SSE connected');
@@ -837,7 +844,7 @@
// Optional: Load initial state from API // Optional: Load initial state from API
async function loadDevices() { async function loadDevices() {
try { try {
const response = await fetch(`${API_BASE}/devices`); const response = await fetch(api('/devices'));
const devices = await response.json(); const devices = await response.json();
console.log('Loaded devices:', devices); console.log('Loaded devices:', devices);
} catch (error) { } catch (error) {