Tools
The local scripts' three log browsers are built from data it mirrors down from the server over a token-protected API on the server's IP. Like the sync, but read-only: each browser reads one endpoint. The Web Logs tool reads the Caddy access log, the Server Logs tool reads the server-health statistics, and the Cron Logs tool reads the run records the runner writes. The local scripts pulls each source incrementally into a local data dir and renders a static browsable site from it.
GET /api/v2/logs
GET /api/v2/logs/file
GET /api/v2/stats
GET /api/v2/stats/file
GET /api/v2/stats/now
GET /api/v2/runs
GET /api/v2/runs/file
Private API
Every call is authenticated with a bearer token, the
cronhoster-config-apiToken from your machine
configuration, sent as Authorization: Bearer <token>.
These routes are read-only. The /logs and /stats routes are identical across
every hoster and come from the shared
localhoster-server/logs-stats.js router (the same
file, byte for byte, in sitehoster, datahoster, gamehoster and cronhoster). The /runs
routes are cronhoster's own addition in cronhoster-api.js. As with the
sync, a valid token answers 200; a version skew on
an /api/v<other> path answers 426; anything without a valid token, and
any unknown route, returns the identical 404 wall. The routes are versioned under
/api/v2/, discovered from /api/version. Files are exposed by
timestamp (logs) or exact validated name (stats and runs) only, so no filename ever
crosses the wire and there is no path-traversal surface.
| Method | Path | Serves |
|---|---|---|
GET | /api/v2/logs | Web Logs. List the access-log files: the active log's size and each rotated file's timestamp. |
GET | /api/v2/logs/file | Stream one access-log file; a Range fetches only the newly-appended tail. |
GET | /api/v2/stats | Server Logs. List the server-health day-files and the sampling interval. |
GET | /api/v2/stats/file | Stream one stat day-file by name; a Range fetches only the growing tail. |
GET | /api/v2/stats/now | A single live server-health snapshot, for debugging. |
GET | /api/v2/runs | Cron Logs. List the job-run day-files by name and date. |
GET | /api/v2/runs/file | Stream one run day-file by name; a Range fetches only the growing tail. |
GET /api/v2/logs
The data source for the Web Logs tool. Lists the Caddy access logs so the local scripts 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 cannot influence where the local scripts writes, and a newly-appeared timestamp is how the local scripts detects that the active log rotated.
Request
| Method | GET |
|---|---|
| Path | /api/v2/logs |
| Auth | Authorization: Bearer <token> |
| Parameters | None. |
Response
A 200 JSON object:
{
"active": { "size": 12345 },
"rotated": [
{ "ts": 1784370489663, "size": 67108864 },
{ "ts": 1784514159284, "size": 41943040 }
]
}
| Field | Type | Meaning |
|---|---|---|
active | object | The live log as { size } in bytes, or null if there is none. |
rotated | array | One entry per rotated file, sorted oldest-first. |
rotated[].ts | integer | The file's rotation time in milliseconds, how the local scripts names it (access-<ts>.log) and de-duplicates it. |
rotated[].size | integer | Its size in bytes; a size change means the local scripts must re-fetch it. |
GET /api/v2/logs/file
Streams the bytes of one access-log file. A Range header is honoured so the local scripts
fetches only the tail appended since last time; an immutable rotated file is fetched once, whole. The
on your machine download lives in the shared toolkit (common/browse/pull-logs.js).
Request
GET /api/v2/logs/file?which=active HTTP/1.1
Authorization: Bearer <token>
Range: bytes=12345-
| Parameter | In | Meaning |
|---|---|---|
which | query | active for the live log, or a rotated file's timestamp (the ts from /logs). |
Range | header | Optional. bytes=N- fetches only from byte N, so the local scripts 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 Contentwith aContent-Rangeheader when aRangewas sent;200otherwise.- An absent file, or a
Rangeat or past the end, returns an empty200body.
GET /api/v2/stats
The data source for the Server Logs tool. Lists the server-health day-files the
API process samples every 15 seconds through localhoster-server/stats-recorder.js, and the sampling
interval, so the local scripts can mirror them. Files are named <type>-YYYY-MM-DD.log with
one type per server metric; a past day is frozen and mirrored once, today's file grows.
Request
| Method | GET |
|---|---|
| Path | /api/v2/stats |
| Auth | Authorization: Bearer <token> |
| Parameters | None. |
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 }
]
}
| Field | Type | Meaning |
|---|---|---|
interval | integer | Milliseconds between samples (15000 = every 15 seconds). |
files | array | One entry per day-file, sorted by name. |
files[].name | string | The exact filename to request from /stats/file. |
files[].type | string | The statistic type: cpu, memory, disk, diskio or netio. |
files[].date | string | The file's UTC date, YYYY-MM-DD. |
files[].size | integer | Its size in bytes. |
GET /api/v2/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. The
on your machine download lives in the shared toolkit (common/browse/pull-stats.js).
Request
GET /api/v2/stats/file?name=cpu-2026-07-20.log HTTP/1.1
Authorization: Bearer <token>
Range: bytes=61200-
| Parameter | In | Meaning |
|---|---|---|
name | query | The exact day-file name from /stats, e.g. cpu-2026-07-20.log. |
Range | header | Optional. 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 Contentwith aContent-Rangewhen aRangewas sent;200otherwise.- An unknown or absent name, or a
Rangeat or past the end, returns an empty200body.
GET /api/v2/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 and network bytes)
are cumulative; a consumer diffs two snapshots to get a rate. Metrics a host cannot report (disk or
network IO off Linux) are null rather than an error.
Request
| Method | GET |
|---|---|
| Path | /api/v2/stats/now |
| Auth | Authorization: Bearer <token> |
| Parameters | None. |
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 }
}
| Field | Type | Meaning |
|---|---|---|
ts | integer | The sample's server Unix time, in seconds. |
uptime | integer | Seconds since boot. |
hostname | string | The machine's hostname. |
load | array | The 1, 5 and 15-minute load averages. |
cpu | object | model plus cores: per-core cumulative times (user, nice, sys, idle, irq). |
memory | object | total / used / available / free bytes. |
disk | object | Usage of the filesystem holding / in bytes, or null. |
diskIo | object | Cumulative readBytes / writeBytes, or null. |
netIo | object | Cumulative rxBytes / txBytes, or null. |
GET /api/v2/runs
Cronhoster's own endpoint, the data source for the Cron Logs tool. Lists the
job-run day-files the runner writes through
cronhoster-runner/run-recorder.js, one JSON line per finished run. Files are named
runs-YYYY-MM-DD.log; the day whose date has rolled over is frozen and mirrored once,
today's file grows. The server prunes these after about 14 days.
Request
| Method | GET |
|---|---|
| Path | /api/v2/runs |
| Auth | Authorization: Bearer <token> |
| Parameters | None. |
Response
A 200 JSON object:
{
"files": [
{ "name": "runs-2026-07-23.log", "date": "2026-07-23", "size": 48210 },
{ "name": "runs-2026-07-24.log", "date": "2026-07-24", "size": 17664 }
]
}
| Field | Type | Meaning |
|---|---|---|
files | array | One entry per run day-file, sorted by name. |
files[].name | string | The exact filename to request from /runs/file, validated as runs-YYYY-MM-DD.log. |
files[].date | string | The file's UTC date, YYYY-MM-DD. |
files[].size | integer | Its size in bytes; a size change means the local scripts must fetch the new tail. |
GET /api/v2/runs/file
Streams one run day-file by its exact name, validated against runs-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. The on your machine download lives in cronhoster's own
local/pull-runs.js.
Request
GET /api/v2/runs/file?name=runs-2026-07-24.log HTTP/1.1
Authorization: Bearer <token>
Range: bytes=17664-
| Parameter | In | Meaning |
|---|---|---|
name | query | The exact day-file name from /runs, e.g. runs-2026-07-24.log. |
Range | header | Optional. bytes=N- to fetch only from byte N. |
Plus Authorization: Bearer <token>.
Response
The raw bytes of the day-file, one JSON run record per line,
Content-Type: application/octet-stream with Accept-Ranges: bytes.
206 Partial Contentwith aContent-Rangewhen aRangewas sent;200otherwise.- An unknown or absent name, or a
Rangeat or past the end, returns an empty200body.
Each line is one completed run: the cron and run names, the run's schedule, wall-clock duration, exit code or kill signal, whether it was killed for overrunning, and how many of its own ticks it skipped while it held a slot. The Runner page describes the record in full.
How the local scripts renders
Each tool pulls its endpoint incrementally, statelessly, deriving everything from what it already has
on disk, the counterpart of how the sync compares
trees. It negotiates the version from /api/version, then:
List
/logs,
/stats or
/runs returns what the server holds:
sizes and timestamps (logs), or day-files (stats and runs).
Fetch what's frozen
An immutable file, a rotated log or a past day-file, is downloaded once, whole, when it is missing locally or its size differs.
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.
The downloads live in the shared toolkit for Web Logs and Server Logs
(common/browse/pull-logs.js and common/browse/pull-stats.js) and in
cronhoster's own local/pull-runs.js for Cron Logs, so every source mirrors the same way.
Each pull writes into a local data dir, and the tool then renders a static browsable site from it:
common/browse/web.js for Web Logs, common/browse/server.js for Server Logs,
and cronhoster's own local/browse/cron.js for Cron Logs. The server keeps only a bounded
window (run records are pruned after about 14 days, stats after 60), but the local scripts keeps every day it
ever pulled, so its history outlives the server's retention and a restart.
The Cron Logs tool is cronhoster's service-specific browser, the analog of sitehoster's Automation Logs, datahoster's Data Logs and gamehoster's Game Logs. It joins the run records with the cron directories in the content root and shows every cron with its health, and a page per cron listing its recent runs: each run's duration, whether it was killed for overrunning, and how many ticks it skipped.