# Server detail page redesign ## Context `server_detail.html` today renders six tall sections stacked vertically: server-info (config), actions, server log (SSE), live state (HTMX poll), console (transcript + input), and files (inline tree). The page reads as a chronological dump of every available subsystem rather than a focused control surface. Three of those sections — config, actions, live state — are conceptually one "what is this server right now" cluster but are visually disconnected. Log, console, and files are all inspection/debugging surfaces but compete for vertical real estate and force long scrolls. The redesign groups state at the top, demotes inspection surfaces into a single tabbed strip at the bottom, and reuses the existing modal infrastructure to host the full versions of each surface on demand. ## Goals - Make "is the server running, who's on it, what map?" visible without scrolling. - Treat Log, Console, and Files as peer inspection surfaces. - Keep the page short enough that Delete/Rename remain reachable without scrolling on a typical desktop viewport. - Minimize new components: reuse `_overlay_file_tree.html`, `_console_line.html`, and the existing `` + `modals.js` pattern. `_server_actions.html` and `_live_state.html` get targeted edits (described below) rather than rewrites. Non-goals: changing the server lifecycle, routes, or partial contracts. This is a template-level refactor with small JS/CSS additions. ## New page structure ``` ┌─ Heading: Server ─────────────────────────────┐ │ │ │ ┌─ State cluster (single panel) ────────────────────┐ │ │ │ [● Running] Start Stop Reset │ │ │ │ ──────────────────────────────────────────────── │ │ │ │ Players 4/8 Map c1m1_hotel Hibernating: no │ │ │ │ (player avatars row from _live_state.html) │ │ │ │ ──────────────────────────────────────────────── │ │ │ │ Port Blueprint │ │ │ │ 27015 survivor-mix │ │ │ │ RCON Hostname │ │ │ │ ••• (show) [my-server____] │ │ │ └───────────────────────────────────────────────────┘ │ │ │ │ ┌─ Inspection strip (tabs) ─────────────────────────┐ │ │ │ [ Log ] [ Console ] [ Files ] [ ⛶ ] │ │ │ │ ──────────────────────────────────────────────── │ │ │ │ (active tab pane, fixed-height scrollable) │ │ │ └───────────────────────────────────────────────────┘ │ │ │ │ [Delete server] Rename │ └───────────────────────────────────────────────────────┘ ``` ## State cluster One `
` replaces today's three separate sections. Three sub-blocks separated by horizontal rules: 1. **Lifecycle** — state badge, start/stop/reset, drift warning, and a one-line "latest job" indicator. The inline job-log SSE stream that currently appears while a job is running (the `
` block at the bottom of `_server_actions.html`) is **removed
   from this subblock entirely** — it pops in and out as HTMX re-polls,
   shifting the rest of the page each time. Users who want the live job log
   click the latest-job link to open `#job-log-modal` (see Modals below).
   **Polling is preserved.** The HTMX 2s poll on `#server-actions` while a
   job is running stays in place — it's what keeps the state badge,
   start/stop/reset buttons, and the "starting since 4s ago" phrase
   current. The only thing the periodic swap no longer carries is the
   `
` SSE element. Net result: the subblock keeps reflecting reality
   every 2s but its height stops changing, so the rest of the page
   doesn't jump.
2. **Live state** — player count, map, hibernation, and compact player cards.
   Still HTMX-polled every 5s.

   **Current players grid.** Fixed 4-column grid
   (`grid-template-columns: repeat(4, 1fr)`), one row per player, max 2 rows
   (L4D2 caps at 8). No section header — the player cards follow the summary
   line directly; the avatars + ping meta make the section self-identifying.
   Each card is a single horizontal item: 22px avatar + two-line text block
   (name on top, terse meta below). Meta drops prefix words —
   `joined 4m ago · ping 22-31ms` becomes `4m · 22-31ms`. The unit
   `m`/`s`/`h`/`d` plus the `ms` suffix carry the meaning.

   **Recent players chips.** Fixed 5-column grid
   (`grid-template-columns: repeat(5, 1fr)`), capped to 10 entries inline.
   Each chip is a single line: 16px avatar + name + `· Xd`. The "last seen"
   prefix is dropped — the trailing time unit is unambiguous. Long names get
   `text-overflow: ellipsis` and a `title` attribute holding the full name.

   **Header doubles as the modal trigger.** The recent section's header is
   `N Recent` (count first, e.g. `13 Recent`). When `N > 10` it renders as a
   link/button that opens `#recent-players-modal`; when `N ≤ 10` it renders
   as plain text (the modal would be redundant). No separate "view all" link.
   The route exposes `recent_players_overview` (sliced to 10) and
   `recent_players_total_count` for this header.

   **Narrow-viewport rule.** Below ~600px viewport width, both grids drop to
   `repeat(auto-fit, minmax(110px, 1fr))` so chips don't crush. One media
   query, applied to both grids.

   **`#recent-players-modal`.** New modal alongside log/console/files. Body
   renders the same chip format in a single column, scrollable, full list.
   Driven by `recent_players` (the full unsliced list already in context).
   Header: `Recent players (N)`. No new route — same context.
3. **Config** — port, blueprint, RCON, hostname. Rendered into a flat auto-fit CSS
   grid (`grid-template-columns: repeat(auto-fit, minmax(220px, 1fr))`) so the
   sub-block wraps to one or two columns based on available width, and accepts new
   fields with no layout decisions. Each field is one cell: small uppercase label
   above the value/control. Hostname keeps its existing inline edit form; other
   fields render as static text.

   **Field macro.** Define `{% macro config_field(label, value, editable=false) %}`
   in `_macros.html` (create if absent) and call it once per row. Adding a future
   field becomes a one-line change in the template — no new CSS, no new markup
   patterns to learn.

   **Expandability path (not in v1).** When the field count crosses ~6, or when a
   natural category emerges (e.g. a second auth credential beyond RCON), promote
   that subset into a `
` inside the same outer grid. The grid scaffold is unchanged; this is a CSS-and-grouping migration, not a rewrite. Document this trigger here so future-us doesn't redesign blindly. No changes to the partials themselves — only the wrapper template changes. The dashed horizontal rules are a new CSS rule on `.state-cluster > * + *`. ## Inspection strip One `
` containing: - **Tab bar**: three `` reads `strip.dataset.activeTab` and opens the matching modal via the existing `data-inline-modal-open` plumbing in `modals.js`. Implementation detail: the button has no static `data-inline-modal-open`; a small JS handler synthesizes the open call based on the active tab. ## New modals Five new `` instances are added alongside the existing rename/delete/reset modals. All follow the existing pattern — no new CSS for chrome, only the body content differs. ### `#log-modal` - Body: a taller `
` plus a small toolbar
  (Download .log link → `/servers/{id}/logs/download` if it exists today; otherwise
  drop — do not invent a new route in v1).
- The inline log pane and modal log pane both stream from the same SSE endpoint.
  They're independent EventSource connections; that's acceptable (low volume).

### `#console-modal`

- Body: a taller `
` and a copy of the console form, posting to the same `/servers/{id}/console` endpoint. - **Two console forms exist** (inline + modal). To avoid them swapping into each other's transcripts, each form's `hx-target` references its own transcript ID: `#console-transcript-inline-{id}` and `#console-transcript-modal-{id}`. - New POSTs go to the form the user submitted; the other transcript stays as it was at last render. Acceptable for v1 — both transcripts derive from the same server history on page load, divergence only occurs within a session. ### `#files-modal` - Body: `_overlay_file_tree.html` with `files_base_url = "/servers/" ~ server.id`, rendered in a much taller container. No new behavior. ### `#recent-players-modal` - Body: full list of recent players in single-column chip format, scrollable. Triggered by the "N Recent" header (when N > 10) in the live-state subblock. - Driven by the full `recent_players` list already in the route context. ### `#job-log-modal` - Body: `
`
  — same SSE endpoint that drives the current inline stream. Header shows the
  job phrase ("starting", "stopping", "resetting") and a "open full job →"
  link to `/jobs/{id}` for users who want history or related metadata.
- **Trigger.** The `latest_job_phrase` text in `_server_actions.html`
  (currently ``) becomes a `