left4me/docs/superpowers/specs/2026-05-13-server-hostname-design.md
2026-05-13 14:19:57 +02:00

77 lines
3.5 KiB
Markdown

# Server Hostname (Source `hostname` cvar) Design
**Goal:** Allow users to set the L4D2 server name (`hostname` cvar) that players see in the server browser and MOTD, with an ephemeral auto-generated fallback.
**Architecture:** A new `hostname VARCHAR(128)` column on the `servers` table. Empty string means "auto-generate at deploy time." The fallback is resolved ephemerally in `initialize_server` — computed fresh from `user.username + server.name` on each deploy, never persisted. Explicit overrides are stored and emitted verbatim.
---
## Model
Add one column to `Server` in `l4d2web/models.py`:
```python
hostname: Mapped[str] = mapped_column(String(128), default="", nullable=False)
```
Default `""` means auto-generate. Non-empty means explicit override.
## Behavior
| `hostname` value | Deploy result |
|---|---|
| `""` (empty) | Emit `hostname "<username> <server.name>"` — computed fresh each deploy, never written to DB |
| `"My Server"` | Emit `hostname "My Server"` verbatim |
| User clears the field | Resets to `""`, next deploy auto-generates |
The fallback is ephemeral — `initialize_server` resolves it in-memory for the spec YAML. The DB row stays empty. This means renames auto-propagate to the hostname on the next deploy without manual updates.
## Spec Payload
`build_server_spec_payload()` gains an optional `resolved_hostname: str = ""` keyword parameter. When non-empty, a `hostname "..."` line is inserted into the config array, before the `rcon_password` line (so rcon remains last-wins).
`initialize_server()` resolves the hostname:
```python
with session_scope() as db:
user = db.get(User, server.user_id)
resolved = server.hostname or f"{user.username} {server.name}"
```
## UI
On `server_detail.html`, a new row in the info `<dl>` block, placed after the RCON password row:
```
Hostname: [ _______________ ] [Save]
Leave empty for auto: "alice alpha"
```
- Input `name="hostname"`, `maxlength="128"`
- `value="{{ server.hostname }}"` (empty when not set)
- `placeholder="{{ user.username }} {{ server.name }}"` (previews auto-generated value)
- Form submits to `POST /servers/<id>` — same endpoint as the rename form
- No hostname field in the create-server modal; new servers always start with `hostname=""`
## Routes
**`POST /servers/<int:server_id>`** (update_server_form) — unchanged signature; just also saves `request.form.get("hostname", "")` to `server.hostname`.
**`POST /servers`** (create_server) — unchanged; `hostname` defaults to `""` from the model default.
## Files Touched
| File | Change |
|---|---|
| `l4d2web/models.py` | Add `hostname` column to `Server` |
| `l4d2web/alembic/versions/0011_server_hostname.py` | Migration — `ADD COLUMN hostname VARCHAR(128) NOT NULL DEFAULT ''` |
| `l4d2web/routes/server_routes.py` | `update_server_form` saves `hostname` from form |
| `l4d2web/services/l4d2_facade.py` | `build_server_spec_payload` accepts `resolved_hostname=`, emits `hostname "..."` line. `initialize_server` resolves fallback. |
| `l4d2web/templates/server_detail.html` | Hostname form row in info `<dl>` |
| `l4d2web/tests/test_servers.py` | Tests for create default, update, clear |
| `l4d2web/tests/test_l4d2_facade.py` | Tests for hostname in spec, fallback resolution |
## Open / Closed
- **Explicit vs ephemeral:** Explicit overrides persist; empty means auto at deploy time. No toggle, no "locked" mode needed in v1.
- **No hostname in create modal:** Simplifies the form. Hostname is configured post-creation on the detail page.