# Service Development Guide

How to write and maintain services in BanGUI.

## Error Handling Contracts

Every service method must document which error handling pattern it follows.
This lets callers know what to expect without reading the implementation.

### The Three Patterns

```python
from app.services.error_handling import ABORT_ON_ERROR, RETURN_DEFAULT, PARTIAL_RESULT
```

**ABORT_ON_ERROR** — Raise an exception, let the router convert it to HTTP.
Used for: auth, writes, state changes, any operation where partial success is meaningless.

```python
async def start_jail(socket_path: str, name: str) -> None:
    """Start a stopped fail2ban jail.

    Error contract: ABORT_ON_ERROR. Raises JailNotFoundError (404),
    JailOperationError (409), Fail2BanConnectionError (503).
    """
    ...
```

**RETURN_DEFAULT** — Return empty result and log warning. Never raises.
Used for: informational reads (list, get) where infrastructure unavailability
should not block the UI.

```python
async def get_settings(socket_path: str) -> DomainServerSettingsResult:
    """Return current fail2ban server-level settings.

    Error contract: RETURN_DEFAULT. Returns DomainServerSettingsResult
    with default values if socket is unreachable. Never raises.
    """
    ...
```

**PARTIAL_RESULT** — Return (result, errors) tuple. Errors collected, not raised.
Used for: batch operations on collections where one item failing does not
invalidate the rest.

```python
# Not yet used in codebase; define as needed for batch operations.
```

### When to Use Which

| Operation type | Pattern |
|---------------|---------|
| Auth / session | ABORT_ON_ERROR |
| Write / state change | ABORT_ON_ERROR |
| Config updates | ABORT_ON_ERROR |
| Single-item read (jail, ban) | ABORT_ON_ERROR |
| Multi-item read (list) | RETURN_DEFAULT |
| Server settings read | RETURN_DEFAULT |
| Batch / parallel fetch | PARTIAL_RESULT |

### Changing Patterns

Switching a method's error contract is a **breaking change**. Update the docstring,
add a changelog entry, and bump the major version if this is a public API.

## Service Structure

Services live in `backend/app/services/`. They contain **no** HTTP/FastAPI concerns.

```
app/services/
  ban_service.py       # ban/unban, ban history queries
  jail_service.py      # jail lifecycle, ignore lists
  server_service.py    # server-level settings
  geo_service.py       # geolocation
  ...
  error_handling.py    # contract definitions
  protocols.py         # Protocol interfaces for DI
```

## Protocols

Each service has a corresponding protocol in `protocols.py` for dependency injection.
Protocol methods include the error contract in their docstring:

```python
class JailService(Protocol):
    async def list_jails(self, socket_path: str) -> DomainJailList:
        """Error contract: ABORT_ON_ERROR."""
        ...
```

## Router Error Handling

Routers must not catch and silently swallow exceptions from services using
ABORT_ON_ERROR unless they convert to a specific HTTP response.
Let domain exceptions propagate — the global exception handlers handle them.

Exception handler registration (in `main.py`):
- `DomainError` → JSON error response
- `Fail2BanConnectionError` → HTTP 503
- `JailNotFoundError` → HTTP 404

## Logging

Log at the service layer using structlog:

```python
log.info("jail_started", jail=name)
log.warning("socket_unreachable_using_default", socket_path=socket_path)
```

Never log sensitive data (tokens, passwords, IPs in full).