feat: Service cards with inline toggles + updated docs

Service cards now have:
- Public/Private toggle (switches reach)
- Subdomains toggle (include *.domain)
- TLS badge (passthrough vs terminate, read-only for now)
- Inline status details (DNS, Proxy, TLS status)
- Deregister button
- Add Service form with toggles instead of dropdowns

The card speaks the user's language (public/private, subdomains on/off)
not implementation details (tcp-passthrough vs http, reach: internal).

Also updated docs/registrations.md:
- Added "User-facing concepts" section mapping API fields to toggles
- Added batch deregister endpoint
- Added backend.health field
- Cleaner examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-09 23:50:05 +00:00
parent c9732ffa7f
commit 98385449b4
2 changed files with 146 additions and 171 deletions

View File

@@ -10,6 +10,7 @@ GET /api/v1/services List all registrations
GET /api/v1/services/{domain} Get registration details
PATCH /api/v1/services/{domain} Update a registration
DELETE /api/v1/services/{domain} Remove a registration
DELETE /api/v1/services/deregister?source=X&backend=Y Batch cleanup
```
The domain is the unique key. One registration per domain.
@@ -22,48 +23,51 @@ The domain is the unique key. One registration per domain.
| `source` | string | no | `"manual"` | Who registered this: `wild-cloud`, `wild-works`, `manual`. |
| `backend.address` | string | yes | — | Target host:port (e.g., `192.168.8.240:443`). |
| `backend.type` | string | yes | — | `tcp-passthrough` or `http`. |
| `backend.health` | string | no | — | Health check path for L7 services (e.g., `/health`). |
| `subdomains` | bool | no | `false` | If true, also routes `*.domain` traffic to this backend. |
| `reach` | string | yes | — | `internal` or `public`. |
| `tls` | string | no | inferred | `passthrough` or `terminate`. Defaults based on type. |
| `tls` | string | no | inferred | `passthrough` or `terminate`. Defaults based on backend type. |
### `backend.type`
## User-facing concepts
**`tcp-passthrough`** — Layer 4. Central passes TLS traffic through to the backend without terminating it. The backend handles its own TLS (e.g., a k8s cluster with traefik and its own wildcard cert). HAProxy routes by SNI.
The API fields map to three user-visible controls:
**`http`** — Layer 7. Central terminates TLS (using a certbot-provisioned certificate) and reverse-proxies HTTP to the backend. HAProxy routes by Host header.
### Public / Private
### `subdomains`
`reach: "public"` or `reach: "internal"`
Controls whether `*.domain` traffic is also routed to this backend.
- **Private** (internal): Domain resolves only on the LAN. No public DNS record. Accessible only from your local network or VPN.
- **Public**: Domain resolves on the LAN AND has a public DNS A record. Accessible from the internet through Central's HAProxy.
- **`false`** (default): Only exact-match `domain` is routed. Use for custom domains like `payne.io` or `civilsociety.dev` where you only want that specific hostname.
- **`true`**: Both `domain` and `*.domain` are routed. Use for instance primary domains like `cloud.payne.io` where apps are served at subdomains (`matrix.cloud.payne.io`, `vaultwarden.cloud.payne.io`).
### Subdomains
### `reach`
`subdomains: true` or `subdomains: false`
Controls DNS visibility and external access.
- **Off** (default): Only the exact domain is routed (`payne.io` routes, but `foo.payne.io` does not).
- **On**: Both the domain and all subdomains are routed (`cloud.payne.io` AND `matrix.cloud.payne.io`, `vaultwarden.cloud.payne.io`, etc.).
- **`internal`**: Domain resolves only on the LAN (dnsmasq `local=/` + `address=/`). Not added to DDNS. Not routable from the internet.
- **`public`**: Domain resolves on the LAN (dnsmasq `address=/`) AND externally via DDNS A record. Routable from the internet through Central's HAProxy.
### TLS handling
### `tls`
`tls: "passthrough"` or `tls: "terminate"`
- **`passthrough`**: Central does not terminate TLS — the backend handles it. Default for `tcp-passthrough`.
- **`terminate`**: Central provisions a TLS certificate (via certbot + Cloudflare DNS-01) and terminates TLS at HAProxy. Default for `http`.
- **Passthrough**: Central forwards encrypted traffic directly to the backend. The backend handles its own TLS certificates (e.g., a k8s cluster with traefik). Maps to `backend.type: "tcp-passthrough"`.
- **Terminate**: Central provisions a TLS certificate and handles HTTPS. Traffic is decrypted at Central and proxied as HTTP to the backend. Maps to `backend.type: "http"`.
## What Central does per registration
| | `tcp-passthrough` | `http` |
|---|---|---|
| **Internal DNS** | `address=/<domain>/<backend-IP>` — LAN clients connect directly to the backend | `address=/<domain>/<central-IP>` — LAN clients connect to Central's HAProxy |
| **DNS (internal reach)** | Also `local=/<domain>/` — prevents upstream DNS forwarding | Same |
| **External DNS (public reach)** | DDNS A record at Cloudflare | Same |
| **HAProxy** | L4 SNI → backend. `subdomains: true` adds `*.domain` matching. | L7 Host header → reverse proxy to backend. Always exact match. |
| **TLS** | Passthrough — backend handles TLS | Terminate — Central provisions cert via certbot |
When a service is registered, Central automatically:
| Effect | Passthrough | Terminate |
|--------|-------------|-----------|
| **LAN DNS** | `address=/<domain>/<backend-IP>` — direct to backend | `address=/<domain>/<central-IP>` — through Central |
| **LAN DNS (private)** | Also `local=/<domain>/` — prevents upstream forwarding | Same |
| **Public DNS** | DDNS A record if public | Same |
| **Proxy** | L4 SNI passthrough. Subdomains adds `*.domain` matching. | L7 HTTP reverse proxy by Host header. |
| **TLS** | Backend handles — Central passes through | Central provisions cert via Let's Encrypt |
## Examples
### Wild Cloud k8s instance (primary domain with subdomains)
### k8s cluster (passthrough, public, with subdomains)
```json
{
@@ -75,30 +79,22 @@ Controls DNS visibility and external access.
}
```
Result:
- DNS: `cloud.payne.io` → 192.168.8.240, `*.cloud.payne.io` → 192.168.8.240
- HAProxy: SNI `cloud.payne.io` and `*.cloud.payne.io` → passthrough to 192.168.8.240:443
- DDNS: A record for `cloud.payne.io` → public IP
- TLS: handled by k8s traefik (passthrough)
Central creates: LAN DNS → 192.168.8.240, public DNS A record, HAProxy L4 SNI + wildcard *.cloud.payne.io, TLS passthrough.
### Wild Cloud k8s instance (internal-only domain)
### Internal web app (terminate, private)
```json
{
"domain": "internal.cloud.payne.io",
"domain": "wild-cloud.payne.io",
"source": "wild-cloud",
"backend": {"address": "192.168.8.240:443", "type": "tcp-passthrough"},
"subdomains": true,
"backend": {"address": "127.0.0.1:5055", "type": "http"},
"reach": "internal"
}
```
Result:
- DNS: `local=/internal.cloud.payne.io/` + `address=/internal.cloud.payne.io/192.168.8.240`
- No DDNS record (internal only)
- HAProxy: SNI matching for internal domain (in case traffic comes through Central)
Central creates: LAN DNS → Central IP (with `local=/`), HAProxy L7 reverse proxy, TLS cert via certbot. No public DNS.
### Custom app domain (exact match, no subdomains)
### Custom domain (passthrough, public, exact match)
```json
{
@@ -110,36 +106,14 @@ Result:
}
```
Result:
- DNS: `address=/payne.io/192.168.8.240`
- HAProxy: SNI exact match `payne.io` only (NOT `*.payne.io`)
- DDNS: A record for `payne.io` → public IP
### Wild Cloud app (HTTP reverse proxy, internal only)
```json
{
"domain": "wild-cloud.payne.io",
"source": "wild-cloud",
"backend": {"address": "127.0.0.1:5055", "type": "http"},
"reach": "internal"
}
```
Result:
- DNS: `local=/wild-cloud.payne.io/` + `address=/wild-cloud.payne.io/192.168.8.151`
- HAProxy: L7 Host `wild-cloud.payne.io` → reverse proxy to 127.0.0.1:5055
- TLS: Central provisions cert via certbot
- No DDNS (internal only)
Central creates: LAN DNS → 192.168.8.240, public DNS A record, HAProxy L4 SNI exact match only. No wildcard.
## What is NOT a registration
Central's own services are driven by Central config, not registrations:
Central's own services come from config, not the registration API:
- **Central's UI domain** (`cloud.central.domain` in config)
- **VPN** (`cloud.vpn.*` in config) — WireGuard endpoint, firewall port
- **Firewall rules** (`cloud.nftables.*` in config)
- **DHCP** (`cloud.dnsmasq.dhcp.*` in config)
- **CrowdSec**, certbot credentials, DDNS provider config
These are managed through Central's own UI and config file. They don't go through the registration API.
- Central's UI domain (`cloud.central.domain`)
- VPN (`cloud.vpn.*`)
- Firewall rules (`cloud.nftables.*`)
- DHCP (`cloud.dnsmasq.dhcp.*`)
- CrowdSec, certbot credentials, DDNS provider config