Zelthr Monitor is a Node.js and TypeScript homelab monitoring foundation for 1 to 10 systems. Phase 1 provides the core API server, WebSocket server, MariaDB access through Prisma ORM, agent registration, agent heartbeat ingestion, basic metric ingestion, authentication, and health checks.
No Docker is required.
- Node.js 22 or newer.
- npm.
- MariaDB running locally or on your LAN.
- A MariaDB database created for Zelthr Monitor.
The root .env.example and apps/core/.env.example contain the same required settings.
Copy the root example for root commands:
copy .env.example .envPrisma workspace commands may execute with apps/core as the working directory. If Prisma cannot find DATABASE_URL, also copy the core example:
copy apps\core\.env.example apps\core\.envSet both files to the same MariaDB URL when both are present:
DATABASE_URL=mysql://zelthr:zelthr@localhost:3306/zelthr_monitorThe Prisma datasource uses provider = "mysql", which targets MariaDB through Prisma's MySQL/MariaDB connector.
npm install
npm run generate
npm run migratenpm run migrate applies committed Prisma migrations to MariaDB. It does not require Docker.
npm run devThe core server listens on PORT from .env, defaulting to 3000.
npm run generate
npm run migrate
npm run build
npm test
npm run lintGET /healthPOST /api/v1/auth/tokenGET /api/v1/statusPOST /api/v1/agents/registerGET /api/v1/agentsPOST /api/v1/agents/:id/heartbeatPOST /api/v1/metrics/:id- WebSocket:
ws://localhost:3000/ws
Open a WebSocket client:
npx wscat -c ws://localhost:3000/wsThe server sends an initial connection message:
{"type":"connected","service":"zelthr-core"}Keep wscat open in one terminal, then register an agent or run the Phase 2 agent from another terminal. Realtime events are broadcast as JSON:
{"type":"agent_registered","agent_id":"...","data":{}}{"type":"heartbeat","agent_id":"...","data":{}}{"type":"metrics","agent_id":"...","data":{"samples":[]}}For Phase 3 probe testing, keep wscat open and watch for probe events:
{"type":"probe","agent_id":"...","data":{"results":[]}}All Phase 2.5 query endpoints require an admin bearer token.
Get a token:
$tokenResponse = Invoke-RestMethod `
-Method Post `
-Uri "http://localhost:3000/api/v1/auth/token" `
-ContentType "application/json" `
-Body (@{
username = "admin"
password = "change-me"
} | ConvertTo-Json)
$token = $tokenResponse.data.tokenLatest metric samples for all agents:
Invoke-RestMethod `
-Method Get `
-Uri "http://localhost:3000/api/v1/metrics" `
-Headers @{ Authorization = "Bearer $token" }Recent metric samples for one agent:
Invoke-RestMethod `
-Method Get `
-Uri "http://localhost:3000/api/v1/metrics/$agentId?limit=100&metric_name=cpu.usage_percent" `
-Headers @{ Authorization = "Bearer $token" }Newest sample for each metric name for one agent:
Invoke-RestMethod `
-Method Get `
-Uri "http://localhost:3000/api/v1/metrics/$agentId/latest" `
-Headers @{ Authorization = "Bearer $token" }Latest heartbeat for one agent:
Invoke-RestMethod `
-Method Get `
-Uri "http://localhost:3000/api/v1/agents/$agentId/heartbeat" `
-Headers @{ Authorization = "Bearer $token" }Online status for one agent:
Invoke-RestMethod `
-Method Get `
-Uri "http://localhost:3000/api/v1/agents/$agentId/status" `
-Headers @{ Authorization = "Bearer $token" }Probe ingestion uses the agent API key:
Invoke-RestMethod `
-Method Post `
-Uri "http://localhost:3000/api/v1/probes/$agentId" `
-Headers @{ "X-Agent-Key" = $agentApiKey } `
-ContentType "application/json" `
-Body (@{
results = @(
@{
target = "8.8.8.8"
reachable = $true
latency_ms = 21.4
packet_loss_percent = 0
jitter_ms = 1.2
reported_at = (Get-Date).ToUniversalTime().ToString("o")
}
)
} | ConvertTo-Json -Depth 5)Latest probe result per target for one agent:
Invoke-RestMethod `
-Method Get `
-Uri "http://localhost:3000/api/v1/probes/$agentId/latest" `
-Headers @{ Authorization = "Bearer $token" }Recent probe history:
Invoke-RestMethod `
-Method Get `
-Uri "http://localhost:3000/api/v1/probes/$agentId?limit=100" `
-Headers @{ Authorization = "Bearer $token" }Recent probe history for one target:
Invoke-RestMethod `
-Method Get `
-Uri "http://localhost:3000/api/v1/probes/$agentId?target=8.8.8.8&limit=100" `
-Headers @{ Authorization = "Bearer $token" }The Phase 2 agent runs on Windows or Linux, collects local system metrics every 15 seconds by default, sends a heartbeat to the core, and uploads metrics through the Phase 1 ingestion endpoint.
Start the core server first:
npm run devRequest an admin token:
$tokenResponse = Invoke-RestMethod `
-Method Post `
-Uri "http://localhost:3000/api/v1/auth/token" `
-ContentType "application/json" `
-Body (@{
username = "admin"
password = "change-me"
} | ConvertTo-Json)
$token = $tokenResponse.data.tokenRegister the agent:
$agentResponse = Invoke-RestMethod `
-Method Post `
-Uri "http://localhost:3000/api/v1/agents/register" `
-Headers @{ Authorization = "Bearer $token" } `
-ContentType "application/json" `
-Body (@{
name = "local-agent"
hostname = $env:COMPUTERNAME
os_family = "windows"
} | ConvertTo-Json)
$agentResponse.data.agent.id
$agentResponse.data.api_keySave the returned agent ID and API key.
Copy the example:
copy apps\agent\.env.example apps\agent\.envSet:
AGENT_SERVER_URL=http://localhost:3000
AGENT_ID=<agent id from registration>
AGENT_API_KEY=<agent api key from registration>
AGENT_REPORT_INTERVAL=15
AGENT_PROBE_TARGETS=8.8.8.8,1.1.1.1
AGENT_PROBE_INTERVAL=60
AGENT_PROBE_COUNT=3
AGENT_PROBE_TIMEOUT_MS=1000
AGENT_NAME=local-agent
AGENT_VERSION=0.1.0npm run dev:agentExpected cycle logs:
[agent] heartbeat sent
[metrics] CPU 12.4% | RAM 45.1% | Disk 60.2% | Net RX 123456789 | TX 987654321
[probes] 8.8.8.8 reachable=true latency=21.4ms loss=0% jitter=1.2ms
Confirm ingestion with the Phase 2.5 query APIs, by watching the agent logs for successful cycles, checking core logs for request errors, or inspecting the MariaDB heartbeats and metrics tables.
Example MariaDB checks:
select id, agent_id, status, received_at from heartbeats order by received_at desc limit 5;
select agent_id, metric_name, value, reported_at from metrics order by received_at desc limit 10;Dev Control Mode is for local development testing only. It is disabled by default, only runs when NODE_ENV=development, and rejects non-local requests when DEV_CONTROL_LOCAL_ONLY=true. It does not run arbitrary shell commands, expose credentials, delete files, or provide remote desktop access.
Enable it in .env and restart the core server:
NODE_ENV=development
DEV_CONTROL_ENABLED=true
DEV_CONTROL_LOCAL_ONLY=trueOpen:
http://localhost:3000/dev/control
Get an admin JWT token:
$tokenResponse = Invoke-RestMethod `
-Method Post `
-Uri "http://localhost:3000/api/v1/auth/token" `
-ContentType "application/json" `
-Body (@{
username = "admin"
password = "change-me"
} | ConvertTo-Json)
$token = $tokenResponse.data.token
$tokenPaste the token into the page before clicking actions. The API endpoint is:
Invoke-RestMethod `
-Method Post `
-Uri "http://localhost:3000/api/v1/dev/control" `
-Headers @{ Authorization = "Bearer $token" } `
-ContentType "application/json" `
-Body (@{ action = "system_info" } | ConvertTo-Json)Supported actions are system_info, metrics_snapshot, collect_now, pause_agent, resume_agent, set_interval, test_heartbeat, test_metrics, and test_ws_event.
Disable it by setting:
DEV_CONTROL_ENABLED=falseThen restart the core server.
These are temporary development-only pages, not a dashboard. They are served only when NODE_ENV=development. They require you to paste an admin JWT and agent ID, then they call the authenticated APIs from the browser. They do not bypass authentication and do not expose secrets.
Open the probe test viewer:
http://localhost:3000/dev/probes
Open the combined monitor viewer:
http://localhost:3000/dev/monitor
Get a JWT token with the auth command in the previous section. Get an agent ID from registration:
$agentResponse = Invoke-RestMethod `
-Method Post `
-Uri "http://localhost:3000/api/v1/agents/register" `
-Headers @{ Authorization = "Bearer $token" } `
-ContentType "application/json" `
-Body (@{
name = "local-agent"
hostname = $env:COMPUTERNAME
os_family = "windows"
} | ConvertTo-Json)
$agentId = $agentResponse.data.agent.id
$agentIdPaste the JWT token and agent ID into /dev/probes or /dev/monitor. The probe viewer refreshes GET /api/v1/probes/:agentId/latest every 5 seconds and listens for realtime probe events.
Verify realtime events with wscat:
npx wscat -c ws://localhost:3000/wsIncluded:
- Core Express server.
- WebSocket server.
- MariaDB + Prisma connection.
- Prisma migrations.
- Authentication endpoint.
- Agent registration endpoint.
- Agent heartbeat endpoint.
- Basic metrics ingestion endpoint.
- Health check endpoint.
- Zod validation.
- Consistent JSON error handling.
Not included:
- Dashboard.
- Discord monitoring.
- Alerts.
- Prometheus.
- Docker.