diff --git a/docs/superpowers/specs/2026-05-17-server-detail-page-redesign-design.md b/docs/superpowers/specs/2026-05-17-server-detail-page-redesign-design.md new file mode 100644 index 0000000..f8bbf24 --- /dev/null +++ b/docs/superpowers/specs/2026-05-17-server-detail-page-redesign-design.md @@ -0,0 +1,289 @@ +# 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 `