Add per-server log viewer (console.log + games_sp.log)

New Logs section on the server detail page — tabbed viewer with
auto-refresh every 5s (toggleable), sticky-to-bottom scrolling, and a
manual refresh button. Two whitelisted files per server:

  /root/zombies/t5/<name>/logs/console.log
  /root/zombies/t5/<name>/logs/games_sp.log

Backend:
  - src/lib/server/fleet/logs.ts — safe path resolution + tailLog()
    that reads only the last 256 KB of the file, splits on newlines,
    drops the (potentially partial) first line if we didn't start at
    byte 0, and returns the last N lines with size + mtime + truncated.
    Two path-traversal guards: server directory must be a real
    subdirectory of SERVERS_ROOT, and the target must resolve inside
    it.
  - GET /api/servers/[name]/logs/[file]?lines=200 — whitelisted keys
    only (console, games_sp), 400 on unknown keys, ok:true+missing:
    true when the log file hasn't been written yet.

Frontend:
  - LogPanel.svelte with tabs, auto-refresh checkbox, size/mtime meta,
    and a <pre> body that snaps to bottom on refresh unless the user
    has scrolled up.
  - 7 unit tests covering path safety (including .. traversal) and
    tail behavior (missing file → null, small file, oversized file
    → truncated, trailing newline handling).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-09-12 11:50:19 -04:00
co-authored by Claude Opus 4.7
parent d21e8187ff
commit 6aae72f508
5 changed files with 559 additions and 0 deletions
+12
View File
@@ -5,6 +5,7 @@
import type { RosterPlayer, Health, RconEndpoint, RconFailure } from '$ui/stores/servers.js';
import StatusDot from '$ui/components/StatusDot.svelte';
import PlayerRoster from '$ui/components/PlayerRoster.svelte';
import LogPanel from '$ui/components/LogPanel.svelte';
import { gametypeLabel } from '$ui/gametype.js';
import { decodeRconError, rconFixHint } from '$ui/rconError.js';
import { komodoFixHint, type KomodoPortFailure } from '$ui/komodoError.js';
@@ -436,6 +437,17 @@
<PlayerRoster roster={effectiveRoster} health={effectiveHealth} />
</section>
<section class="panel">
<h2>Logs</h2>
<LogPanel
server={server.name}
tabs={[
{ key: 'console', label: 'console.log' },
{ key: 'games_sp', label: 'games_sp.log' }
]}
/>
</section>
<section class="panel">
<h2>Config</h2>
<div class="stats">