Tools

The local log, stat and game views are built from data the client mirrors down from the server over a token-protected API on the server's IP. Like the sync, but read-only: these endpoints serve the Caddy access logs, the server-health statistics, and the game-telemetry log tree, and the client pulls them incrementally into your content root.

GET /api/v1/logs
GET /api/v1/logs/file
GET /api/v1/stats
GET /api/v1/stats/file
GET /api/v1/stats/now
GET /api/v1/gamelogs
GET /api/v1/gamelogs/file
The read-only tools API: list and stream the access-log, server-health and game-telemetry files.

Private API

Every call is authenticated with a bearer token — the gamehoster-config-apiToken from your local tool configuration, sent as Authorization: Bearer <token>. These routes are read-only and identical across every hoster (they live in common/server/logs-stats.js). As with the sync, a correctly authenticated call returns 200 and anything else — bad token, unknown route, wrong version — returns an identical 404; the routes are versioned under /api/v1/, discovered from /api/version. Files are exposed by timestamp (logs), exact validated name (stats) or validated tree-relative path (game logs) only, so a request can never escape the served directory and there is no path-traversal surface.

EndpointPurpose
GET /api/v1/logsList the access-log files: the active log's size and each rotated file's timestamp.
GET /api/v1/logs/fileStream one access-log file; a Range fetches only the newly-appended tail.
GET /api/v1/statsList the server-health day-files and the sampling interval.
GET /api/v1/stats/fileStream one stat day-file by name; a Range fetches only the growing tail.
GET /api/v1/stats/nowA single live server-health snapshot, for debugging.
GET /api/v1/gamelogsList the game-telemetry log tree: every stats/snapshot file by its tree-relative path and size.
GET /api/v1/gamelogs/fileStream one game-log file by path; a Range fetches only the growing tail.

GET /api/v1/logs

Lists the Caddy access logs so the client can mirror them. The active log is reported by size only; each rotated file is keyed by its rotation timestamp, never its name — so the server can't influence where the client writes, and a newly-appeared timestamp is how the client detects that the active log rotated.

Request

MethodGET
Path/api/v1/logs
AuthAuthorization: Bearer <token>
ParametersNone.

Response

A 200 JSON object:

{
  "active": { "size": 12345 },
  "rotated": [
    { "ts": 1784370489663, "size": 67108864 },
    { "ts": 1784514159284, "size": 41943040 }
  ]
}
FieldTypeMeaning
activeobjectThe live log as { size } in bytes, or null if there is none.
rotatedarrayOne entry per rotated file, sorted oldest-first.
rotated[].tsintegerThe file's rotation time in milliseconds — how the client names it (access-<ts>.log) and de-duplicates it.
rotated[].sizeintegerIts size in bytes; a size change means the client must re-fetch it.

GET /api/v1/logs/file

Streams the bytes of one access-log file. A Range header is honoured so the client fetches only the tail appended since last time; an immutable rotated file is fetched once, whole. See pull-logs.js.

Request

GET /api/v1/logs/file?which=active HTTP/1.1
Authorization: Bearer <token>
Range: bytes=12345-
Fetch only the tail of the active log appended since last time.
ParameterInMeaning
whichqueryactive for the live log, or a rotated file's timestamp (the ts from /logs).
RangeheaderOptional. bytes=N- fetches only from byte N — the client passes its local size to append just the new tail.

Plus Authorization: Bearer <token>.

Response

The raw log bytes — Caddy's JSON access lines, one per request — Content-Type: application/octet-stream with Accept-Ranges: bytes.

  • 206 Partial Content with a Content-Range header when a Range was sent; 200 otherwise.
  • An absent file, or a Range at or past the end, returns an empty 200 body — never an error.

GET /api/v1/stats

Lists the server-health day-files and the sampling interval, so the client can mirror them. Files are named <type>-YYYY-MM-DD.log; a past day is frozen and mirrored once, today's file grows.

Request

MethodGET
Path/api/v1/stats
AuthAuthorization: Bearer <token>
ParametersNone.

Response

A 200 JSON object:

{
  "interval": 15000,
  "files": [
    { "name": "cpu-2026-07-20.log",    "type": "cpu",    "date": "2026-07-20", "size": 82944 },
    { "name": "memory-2026-07-20.log", "type": "memory", "date": "2026-07-20", "size": 61200 }
  ]
}
FieldTypeMeaning
intervalintegerMilliseconds between samples (15000 = every 15 seconds).
filesarrayOne entry per day-file, sorted by name.
files[].namestringThe exact filename to request from /stats/file.
files[].typestringThe statistic type: cpu, memory, disk, diskio or netio.
files[].datestringThe file's UTC date, YYYY-MM-DD.
files[].sizeintegerIts size in bytes.

GET /api/v1/stats/file

Streams one stat day-file by its exact name, which is validated against <type>-YYYY-MM-DD.log — the only thing that reaches the filesystem, so there is no path-traversal surface. A Range fetches only the growing tail of the current day. See pull-stats.js.

Request

GET /api/v1/stats/file?name=cpu-2026-07-20.log HTTP/1.1
Authorization: Bearer <token>
Range: bytes=61200-
Fetch only today's newly-appended samples.
ParameterInMeaning
namequeryThe exact day-file name from /stats, e.g. cpu-2026-07-20.log.
RangeheaderOptional. bytes=N- to fetch only from byte N.

Plus Authorization: Bearer <token>.

Response

The raw bytes of the day-file — one JSON sample per line — Content-Type: application/octet-stream with Accept-Ranges: bytes.

  • 206 Partial Content with a Content-Range when a Range was sent; 200 otherwise.
  • An unknown or absent name, or a Range at or past the end, returns an empty 200 body.

GET /api/v1/stats/now

Returns a single live health snapshot for debugging — the same shape the recorder samples on its interval, but taken on demand and never stored. Counter fields (CPU core times, disk/network bytes) are cumulative; a consumer diffs two snapshots to get a rate. Metrics a host can't report (e.g. disk/network IO off Linux) are null rather than an error. Built by shared/stats.js.

Request

MethodGET
Path/api/v1/stats/now
AuthAuthorization: Bearer <token>
ParametersNone.

Response

A 200 JSON snapshot (values below are illustrative):

{
  "ts": 1784514159,
  "uptime": 864000,
  "hostname": "vps-1",
  "load": [0.12, 0.09, 0.05],
  "cpu":    { "model": "…", "cores": [ { "user": 0, "nice": 0, "sys": 0, "idle": 0, "irq": 0 } ] },
  "memory": { "total": 0, "used": 0, "available": 0, "free": 0 },
  "disk":   { "path": "/", "total": 0, "used": 0, "free": 0 },
  "diskIo": { "readBytes": 0, "writeBytes": 0 },
  "netIo":  { "rxBytes": 0, "txBytes": 0 }
}
FieldTypeMeaning
tsintegerThe sample's server Unix time, in seconds.
uptimeintegerSeconds since boot.
hostnamestringThe machine's hostname.
loadarrayThe 1, 5 and 15-minute load averages.
cpuobjectmodel plus cores: per-core cumulative times (user, nice, sys, idle, irq).
memoryobjecttotal / used / available / free bytes.
diskobjectUsage of the filesystem holding / in bytes, or null.
diskIoobjectCumulative readBytes / writeBytes, or null.
netIoobjectCumulative rxBytes / txBytes, or null.

Mirroring Process

The client mirrors both sources the same way, statelessly, deriving everything from what it already has on disk — the counterpart of how the sync diffs hashes. It negotiates the version from /api/version, then:

1

List

/logs or /stats returns what the server holds — sizes and timestamps (logs), or day-files and the interval (stats).

2

Fetch what's frozen

An immutable file — a rotated log, or a past day-file — is downloaded once, whole, if it is missing locally or its size differs.

3

Append the tail

The growing file — the active log, or today's day-file — is fetched with a Range from the local size, appending only the new bytes. A rotated log appearing resets the active log and re-fetches it.

The download itself lives in the shared toolkit (common/browse/pull-logs.js and common/browse/pull-stats.js), so every hoster mirrors the same way. The mirrored files then feed the static Web Logs and Server Logs browsers. The Game Logs browser mirrors the game-telemetry tree the same way, then builds its overview, per-game and per-instance pages from it.

GET /api/v1/gamelogs

Lists the game-telemetry log tree so the client can mirror it. Every stats and snapshot file is reported by its tree-relative path (<domain>/<game>/gamehoster-processes/<process>/<file>) and size; a past hour is frozen and mirrored once, the current hour's files grow. The path is the only addressing, and the server validates it against the served root, so a request can never escape the tree.

Request

MethodGET
Path/api/v1/gamelogs
AuthAuthorization: Bearer <token>
ParametersNone.

Response

A 200 JSON object:

{
  "files": [
    { "path": "game.asteroids.net/asteroids/gamehoster-processes/3f9c1a2b7d4e/stats-2026-07-30-18.jsonl", "size": 48210 },
    { "path": "game.asteroids.net/asteroids/gamehoster-processes/3f9c1a2b7d4e/snapshot-2026-07-30-18.jsonl", "size": 9044 }
  ]
}
FieldTypeMeaning
filesarrayOne entry per log file across every game and process, sorted by path.
files[].pathstringThe tree-relative path to request from /gamelogs/file; also where the client writes it locally.
files[].sizeintegerIts size in bytes; a size change means the client re-fetches the tail.

GET /api/v1/gamelogs/file

Streams the bytes of one game-log file, addressed by its tree-relative path. A Range header is honoured so the client fetches only the tail appended since last time; a frozen past-hour file is fetched once, whole.

Request

GET /api/v1/gamelogs/file?path=game.asteroids.net/asteroids/gamehoster-processes/3f9c1a2b7d4e/stats-2026-07-30-18.jsonl HTTP/1.1
Authorization: Bearer <token>
Range: bytes=48210-
Fetch only the tail of the current hour's stats file appended since last time.
ParameterInMeaning
pathqueryThe exact tree-relative path from /gamelogs. Validated against the served root; a path that escapes it is rejected.
RangeheaderOptional. bytes=N- fetches only from byte N — the client passes its local size to append just the new tail.

Plus Authorization: Bearer <token>.

Response

The raw file bytes — JSON Lines, one object per line — Content-Type: application/octet-stream with Accept-Ranges: bytes.

  • 206 Partial Content with a Content-Range header when a Range was sent; 200 otherwise.
  • An absent file, or a Range at or past the end, returns an empty 200 body — never an error.