left4me/docs/superpowers/plans/2026-05-13-server-hostname-v1.md

13 KiB

Server Hostname (Source hostname cvar) Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add a hostname column to the Server model so users can set the Source hostname cvar (server browser/MOTD name), with an ephemeral "<username> <server.name>" fallback resolved at deploy time.

Architecture: New hostname VARCHAR(128) column on servers table (default ""). Empty = auto-generate at deploy. The build_server_spec_payload() function gains a resolved_hostname kwarg; initialize_server() resolves the fallback ephemerally. The server detail page shows an inline form under RCON password. Same POST /servers/<id> endpoint handles saving.

Tech Stack: Python 3.12+, Flask, SQLAlchemy, Alembic, pytest.


Task 1: Add hostname column to Server model and migration

Files:

  • Modify: l4d2web/models.py

  • Create: l4d2web/alembic/versions/0011_server_hostname.py

  • Step 1: Add hostname column to model

Add to the Server class in l4d2web/models.py:131:

hostname: Mapped[str] = mapped_column(String(128), default="", nullable=False)

Place it after rcon_password (line 148) and before created_at (line 149).

  • Step 2: Create the migration

Create l4d2web/alembic/versions/0011_server_hostname.py:

"""add hostname column to servers

Revision ID: 0011_server_hostname
Revises: 0010_server_live_state
Create Date: 2026-05-13
"""
from __future__ import annotations

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op


revision: str = "0011_server_hostname"
down_revision: Union[str, Sequence[str], None] = "0010_server_live_state"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
    with op.batch_alter_table("servers") as batch_op:
        batch_op.add_column(
            sa.Column("hostname", sa.String(length=128), nullable=False, server_default="")
        )


def downgrade() -> None:
    with op.batch_alter_table("servers") as batch_op:
        batch_op.drop_column("hostname")
  • Step 3: Verify migration applies cleanly

Run: cd l4d2web && alembic upgrade head Expected: runs 0011_server_hostname migration, adds the column.

Run: cd l4d2web && alembic downgrade -1 Expected: drops the column.

Run: cd l4d2web && alembic upgrade head Expected: re-adds the column.

  • Step 4: Commit model + migration
git add l4d2web/models.py l4d2web/alembic/versions/0011_server_hostname.py
git commit -m "feat(l4d2-web): add hostname column to Server model"

Task 2: Accept and save hostname on server update

Files:

  • Modify: l4d2web/routes/server_routes.py

  • Test: l4d2web/tests/test_servers.py

  • Step 1: Write failing hostname update tests

Add to l4d2web/tests/test_servers.py:

def test_create_server_hostname_defaults_empty(user_client_with_blueprints) -> None:
    from sqlalchemy import select
    from l4d2web.models import Server

    client, data = user_client_with_blueprints
    response = client.post(
        "/servers",
        data={"name": "alpha", "port": "27015", "blueprint_id": str(data["blueprint_id"])},
        headers={"X-CSRF-Token": "test-token"},
    )
    assert response.status_code == 302

    with session_scope() as session:
        server = session.scalar(select(Server).where(Server.name == "alpha"))
        assert server is not None
        assert server.hostname == ""


def test_update_server_hostname_via_form(user_client_with_blueprints) -> None:
    from sqlalchemy import select
    from l4d2web.models import Server

    client, data = user_client_with_blueprints
    create = client.post(
        "/servers",
        data={"name": "alpha", "port": "27015", "blueprint_id": str(data["blueprint_id"])},
        headers={"X-CSRF-Token": "test-token"},
    )
    server_id = create.headers["Location"].rsplit("/", 1)[1]

    update = client.post(
        f"/servers/{server_id}",
        data={"name": "alpha", "hostname": "My Cool Server"},
        headers={"X-CSRF-Token": "test-token"},
    )
    assert update.status_code == 302

    with session_scope() as session:
        server = session.scalar(select(Server).where(Server.name == "alpha"))
        assert server is not None
        assert server.hostname == "My Cool Server"


def test_update_server_clears_hostname(user_client_with_blueprints) -> None:
    from sqlalchemy import select
    from l4d2web.models import Server

    client, data = user_client_with_blueprints
    create = client.post(
        "/servers",
        data={"name": "alpha", "port": "27015", "blueprint_id": str(data["blueprint_id"])},
        headers={"X-CSRF-Token": "test-token"},
    )
    server_id = create.headers["Location"].rsplit("/", 1)[1]

    # Set hostname first
    client.post(
        f"/servers/{server_id}",
        data={"name": "alpha", "hostname": "My Cool Server"},
        headers={"X-CSRF-Token": "test-token"},
    )

    # Clear it
    client.post(
        f"/servers/{server_id}",
        data={"name": "alpha", "hostname": ""},
        headers={"X-CSRF-Token": "test-token"},
    )

    with session_scope() as session:
        server = session.scalar(select(Server).where(Server.name == "alpha"))
        assert server is not None
        assert server.hostname == ""
  • Step 2: Run tests to verify failure

Run: pytest l4d2web/tests/test_servers.py -k "hostname" -v Expected: FAIL — Server object has no attribute hostname.

  • Step 3: Save hostname from form in update route

In l4d2web/routes/server_routes.py, modify update_server_form (around line 130) to save hostname:

        server.name = name
        server.hostname = request.form.get("hostname", "")
  • Step 4: Run tests to verify pass

Run: pytest l4d2web/tests/test_servers.py -k "hostname" -v Expected: PASS.

  • Step 5: Commit hostname update support
git add l4d2web/routes/server_routes.py l4d2web/tests/test_servers.py
git commit -m "feat(l4d2-web): accept hostname on server update, default empty on create"

Task 3: Emit hostname in spec payload with ephemeral fallback

Files:

  • Modify: l4d2web/services/l4d2_facade.py

  • Test: l4d2web/tests/test_l4d2_facade.py

  • Step 1: Write failing hostname spec tests

Add to l4d2web/tests/test_l4d2_facade.py:

def test_build_server_spec_payload_injects_hostname() -> None:
    from l4d2web.services.l4d2_facade import build_server_spec_payload

    bp = Blueprint(id=1, user_id=1, name="bp", arguments="[]", config='["sv_consistency 1"]')
    srv = Server(id=1, user_id=1, blueprint_id=1, name="alpha", port=27015, rcon_password="sekret")
    spec = build_server_spec_payload(srv, bp, [], resolved_hostname="My Server")
    cfg = spec["config"]
    assert "hostname \"My Server\"" in cfg
    assert cfg[-1] == "rcon_password \"sekret\""


def test_build_server_spec_payload_omits_hostname_when_empty() -> None:
    from l4d2web.services.l4d2_facade import build_server_spec_payload

    bp = Blueprint(id=1, user_id=1, name="bp", arguments="[]", config="[]")
    srv = Server(id=1, user_id=1, blueprint_id=1, name="alpha", port=27015, rcon_password="sekret")
    spec = build_server_spec_payload(srv, bp, [])
    for line in spec["config"]:
        assert not line.startswith("hostname ")


def test_initialize_server_resolves_fallback_hostname(
    monkeypatch: pytest.MonkeyPatch, server_with_blueprint,
) -> None:
    """When server.hostname is empty, deploy emits hostname "<username> <server.name>"."""
    from l4d2web.services.l4d2_facade import initialize_server

    spec_contents: list[str] = []

    def fake_run_command(cmd, **kwargs):
        nonlocal spec_contents
        spec_path = cmd[cmd.index("-f") + 1]
        spec_contents.append(Path(spec_path).read_text())
        return CommandResult(returncode=0, stdout="", stderr="")

    monkeypatch.setattr("l4d2web.services.host_commands.run_command", fake_run_command)

    server_id, _ = server_with_blueprint
    initialize_server(server_id)

    assert len(spec_contents) == 1
    assert "hostname" in spec_contents[0]
    # The fixture creates user "alice" and server named "alpha"
    assert '"alice alpha"' in spec_contents[0]
  • Step 2: Run tests to verify failure

Run: pytest l4d2web/tests/test_l4d2_facade.py -k "hostname" -v Expected: FAIL — build_server_spec_payload() got unexpected keyword resolved_hostname.

  • Step 3: Add resolved_hostname kwarg and emit line

In l4d2web/services/l4d2_facade.py, modify build_server_spec_payload signature and add the hostname injection before rcon_password:

def build_server_spec_payload(
    server: Server,
    blueprint: Blueprint,
    overlay_rows: list[tuple[int, str, bool]],
    *,
    resolved_hostname: str = "",
) -> dict:

Inside the function, after building config_lines and before the if server.rcon_password: block, add:

    if resolved_hostname:
        config_lines.append(f'hostname "{resolved_hostname}"')

Then in initialize_server, resolve the fallback. Add the User import at the top:

from l4d2web.models import (
    Blueprint,
    BlueprintOverlay,
    Overlay,
    OverlayWorkshopItem,
    Server,
    User,
    WorkshopItem,
)

In initialize_server, after load_server_blueprint_bundle(server_id), add:

    # Resolve hostname — explicit override or ephemeral fallback
    if server.hostname:
        resolved_hostname = server.hostname
    else:
        with session_scope() as db:
            user = db.get(User, server.user_id)
            resolved_hostname = f"{user.username} {server.name}"

Then pass it to build_server_spec_payload:

    spec_path = write_temp_spec(build_server_spec_payload(
        server, blueprint, overlay_rows, resolved_hostname=resolved_hostname,
    ))
  • Step 4: Run tests to verify pass

Run: pytest l4d2web/tests/test_l4d2_facade.py -k "hostname" -v Expected: PASS.

Also run full suite to check nothing broken: pytest l4d2web/tests/test_l4d2_facade.py -v

  • Step 5: Commit hostname spec payload
git add l4d2web/services/l4d2_facade.py l4d2web/tests/test_l4d2_facade.py
git commit -m "feat(l4d2-web): emit hostname in spec config with ephemeral fallback"

Task 4: Add hostname form to server detail page

Files:

  • Modify: l4d2web/templates/server_detail.html

  • Step 1: Verify the current template renders correctly first

Run: pytest l4d2web/tests -q Expected: PASS (baseline).

  • Step 2: Add hostname form under RCON password

In l4d2web/templates/server_detail.html, after the RCON password <dd> block (closing </dd> at line 14) and before the closing </dl> (line 15), add:

    <div><dt>Hostname</dt>
      <dd>
        <form method="post" action="/servers/{{ server.id }}" class="inline-save">
          <input type="hidden" name="csrf_token" value="{{ session.get('csrf_token', '') }}">
          <input name="hostname" value="{{ server.hostname }}" placeholder="{{ user.username }} {{ server.name }}" maxlength="128">
          <button type="submit">Save</button>
          <span class="field-hint">Leave empty for auto: "{{ user.username }} {{ server.name }}"</span>
        </form>
      </dd>
    </div>

The user variable is already available in the template context (the server detail route passes it through the auth mechanism).

  • Step 3: Run full test suite to verify nothing broken

Run: pytest l4d2web/tests -q Expected: PASS.

  • Step 4: Commit template change
git add l4d2web/templates/server_detail.html
git commit -m "feat(l4d2-web): add hostname edit form to server detail page"

Task 5: Final integration verification

Files:

  • Run full test suites

  • Step 1: Run all tests

Run: pytest l4d2web/tests -q Expected: PASS.

Run: pytest l4d2host/tests -q (host lib must not be affected) Expected: PASS.

  • Step 2: Run alembic check to ensure migration is the latest

Run: cd l4d2web && alembic check Expected: "No new upgrade operations detected."

  • Step 3: Commit any final touches needed
git add -A
git commit -m "chore: finalize server hostname feature"

Self-Review

  • Spec coverage: model column, migration, update route saves hostname, spec payload emits hostname line, ephemeral fallback resolved in initialize_server, template has inline form.
  • Placeholder scan: no TODOs or TBDs.
  • Type/name consistency: resolved_hostname kwarg matches usage in both caller and callee.
  • Verification: each task has exact test commands and expected outcomes.