# Platform Administration

Cross-cutting admin operations that don't belong to a specific domain.

### User Management

```
GET    /admin/users                  → { "users": [...], "total": N }
POST   /admin/users                  ← { "username", "password", "email", "role" }
PUT    /admin/users/:id/role         ← { "role": "admin|user" }
PUT    /admin/users/:id/active       ← { "is_active": false }
DELETE /admin/users/:id
POST   /admin/users/:id/reset-password  ← { "password": "..." }
POST   /admin/users/:id/vault/reset     → destroys user's UEK (BYOK keys lost)
```

`PUT /admin/users/:id/role` refuses to demote the last remaining admin
(returns 409). `DELETE /admin/users/:id` also refuses if the target is
the last admin, and destroys the user's vault before deleting the row.

### Global Settings & Policies

Settings are key-value pairs in `global_settings`. Policies are
boolean flags that gate features.

```
GET /admin/settings                  → all settings
GET /admin/settings/:key             → single setting
PUT /admin/settings/:key             ← { "value": ... }
```

The handler auto-detects: if the value is a string and the key is a
known policy name, it writes to the `policies` table. Otherwise it
writes to `global_config` as JSON.

**Public settings** (non-admin, for FE bootstrapping):

```
GET /settings/public
```

```json
{
  "banner": { "enabled": true, "text": "DEVELOPMENT", "bg": "#007a33", "fg": "#ffffff", "position": "both" },
  "branding": { "instance_name": "Switchboard", "logo_url": "...", "tagline": "..." },
  "has_admin_prompt": true,
  "storage_configured": true,
  "paste_to_file_chars": 2000,
  "policies": {
    "allow_registration": "true",
    "allow_user_byok": "true",
    "allow_user_personas": "true",
    "retention_ttl_days": 0
  }
}
```

Note: policy values are strings (`"true"` / `"false"`), not booleans.
`retention_ttl_days` is an integer (days). When > 0, channels using
global/team providers are archived on delete and purged after TTL.
Personal (BYOK) provider channels are always immediately deleted.

### Banner Configuration

The environment banner is stored as a single `global_config` entry
under the key `"banner"`. It's a simple object:

```json
{
  "enabled": true,
  "text": "DEVELOPMENT",
  "position": "both|top|bottom",
  "bg": "#007a33",
  "fg": "#ffffff"
}
```

Set via `PUT /admin/settings/banner` with `{ "value": { ... } }`.
The admin UI provides a color picker, position selector, and preset
dropdown. The Go template base layout reads the banner from
`PageData` and renders top/bottom strips with CSS custom properties.

### Platform Stats

```
GET /admin/stats
```

```json
{
  "users": 42,
  "channels": 156,
  "messages": 12847
}
```

### Storage & Vault

**Storage status:**

```
GET /admin/storage/status            → { "backend", "healthy", "file_count", "total_bytes", ... }
GET /admin/storage/orphans           → { "count": 3 }
POST /admin/storage/cleanup          → removes orphaned blobs
GET /admin/storage/extraction        → extraction queue status
```

**Vault:**

```
GET /admin/vault/status              → { "encryption_key_set", "user_vaults_count", ... }
```

### Audit Log

```
GET /admin/audit?page=1&per_page=50&action=user.create&actor_id=uuid&resource_type=...
GET /admin/audit/actions             → { "actions": ["user.create", "policy.update", ...] }
```

The list endpoint returns a paginated envelope:

```json
{
  "data": [...],
  "total": 128,
  "page": 1,
  "per_page": 50
}
```

Audit entry shape:

```json
{
  "id": "uuid",
  "actor_id": "uuid",
  "actor_name": "admin",
  "action": "user.create",
  "resource_type": "user",
  "resource_id": "uuid",
  "metadata": "{}",
  "ip_address": "10.0.0.1",
  "created_at": "..."
}
```

### Usage & Pricing

```
GET /admin/usage                     → { "totals", "results" }
GET /admin/usage/teams/:id           → { "results": [...] }
GET /admin/usage/users/:id           → { "results": [...] }
GET /admin/pricing                   → [ ... ]
PUT /admin/pricing                   ← { "provider_config_id", "model_id", "input_per_m", "output_per_m" }
DELETE /admin/pricing/:provider_config_id/:model_id
```

`GET /admin/pricing` returns a bare array (no envelope).

`PUT /admin/pricing` accepts the full `PricingEntry` shape:

```json
{
  "provider_config_id": "uuid",
  "model_id": "gpt-4o",
  "input_per_m": 2.50,
  "output_per_m": 10.00,
  "cache_create_per_m": 1.25,
  "cache_read_per_m": 0.50,
  "currency": "USD"
}
```

Pricing cannot be set for personal BYOK providers (403).

**Personal usage** (non-admin):

```
GET /usage                           → { "totals", "results" }
```

Scoped to the user's BYOK provider usage only.

### Roles (Platform)

```
GET    /admin/roles                  → { "roles": [...] }
GET    /admin/roles/:role
PUT    /admin/roles/:role            ← { "permissions": {...} }
POST   /admin/roles/:role/test       ← test role permissions
```

### Email Test

```
POST /admin/notifications/test-email
```

No request body required. Sends a test email to the authenticated
admin's own email address using the configured SMTP settings.

---

### Archived Channels

```
GET    /admin/channels/archived      → { "channels": [...], "total", "page", "per_page", "retention_ttl_days" }
DELETE /admin/channels/:id/purge     → permanent delete (ignores retention)
```

Both endpoints require platform admin auth. `GET` accepts
`?page=1&per_page=50` query parameters (max 100 per page).
`DELETE` verifies the channel is archived before purging and cleans
up associated file storage blobs.

### Surfaces Admin

See [surfaces.md](surfaces.md) for the full surface management API.

```
GET    /admin/surfaces               → all surfaces (including disabled)
GET    /admin/surfaces/:id           → surface with manifest
POST   /admin/surfaces/install       ← multipart .surface archive
PUT    /admin/surfaces/:id/enable
PUT    /admin/surfaces/:id/disable
DELETE /admin/surfaces/:id
```

### Tasks Admin

See [tasks.md](tasks.md) for the full task management API.

```
GET    /admin/tasks                  → all tasks across all users/teams
POST   /admin/tasks/:id/run          → force run
POST   /admin/tasks/:id/kill         → cancel active run
DELETE /admin/tasks/:id              → delete task
```

### Notifications Admin (v0.28.6)

```
POST   /admin/notifications/broadcast   ← { "title", "message", "level?" }
                                         → { "message": "broadcast sent", "count": N }
```

Sends a `system.announcement` notification to all active users.
Also emits a `system.broadcast` WebSocket event for real-time delivery.
`level` is optional (`info` | `warning` | `critical`, default `info`).
See [notifications.md](notifications.md#admin-broadcast-v0286) for full details.
