SWP.ServerAgent API

HTTP API for managing Linux services, systemd units, Caddy reverse proxy routes, and server metrics. Used by SWP.ServerService to orchestrate multiple remote servers.

Base path: /api
JSON responses
17 endpoints

System

GET /api/metrics Server resource metrics

Returns current CPU, memory, and disk usage along with hostname and OS info. Memory and disk percentages are calculated server-side.

Response 200
{
  "totalMemoryMb": 8192,
  "usedMemoryMb": 4096,
  "totalDiskMb": 102400,
  "usedDiskMb": 51200,
  "cpuUsagePercent": 25.5,
  "uptime": "5 days",
  "hostname": "jarjarbinks",
  "osVersion": "Ubuntu 24.04",
  "memoryPercent": 50.0,
  "diskPercent": 50.0
}
GET /api/metrics/history Time-series metrics

Time-series CPU and memory data for charting. Returns aggregated data points at the specified interval.

Query Parameters
NameTypeDefaultDescription
hoursint24How many hours back
intervalint5Aggregation interval in minutes
aggregationstring"max"Aggregation function
Response 200
{
  "dataPoints": [
    { "time": "08:00", "cpu": 10.0, "memory": 45.0 },
    { "time": "08:05", "cpu": 12.5, "memory": 46.2 }
  ]
}
GET /api/system/cpu CPU hardware info
Response 200
{
  "modelName": "AMD EPYC 7763",
  "architecture": "x86_64",
  "cpus": 4,
  "coresPerSocket": 2,
  "threadsPerCore": 2
}
GET /api/system/block-devices Disks and partitions
Response 200
[
  {
    "name": "sda1",
    "size": "50G",
    "type": "part",
    "mountPoint": "/",
    "fsType": "ext4"
  }
]

Services

GET /api/services List all services

Returns both managed (provisioned) and systemd-discovered services. Managed services not yet deployed show activeState: "provisioned" and subState: "awaiting-deploy".

Response 200
[
  {
    "id": 1,
    "unitName": "my-api-test.service",
    "displayName": "my-api",
    "port": 5100,
    "activeState": "active",
    "subState": "running",
    "memoryMb": 150,
    "domain": "my-api-test.swphub.dev",
    "isManaged": true,
    "isProtected": false
  },
  {
    "id": null,
    "unitName": "caddy.service",
    "displayName": "caddy",
    "port": 0,
    "activeState": "active",
    "subState": "running",
    "memoryMb": 42,
    "domain": null,
    "isManaged": false,
    "isProtected": false
  }
]
GET /api/services/{unitName}/detail Full service detail

Returns everything about a service: status, PID, resource usage, port health check, unit file content, and journal logs.

Response 200
{
  "id": 1,
  "unitName": "my-api-test.service",
  "displayName": "my-api",
  "port": 5100,
  "activeState": "active",
  "subState": "running",
  "mainPid": 1234,
  "activeSince": "2026-03-01T10:00:00Z",
  "memoryBytes": 157286400,
  "cpuTime": "1min 30s",
  "domain": "my-api-test.swphub.dev",
  "createdAt": "2026-02-25T12:00:00Z",
  "isManaged": true,
  "isProtected": false,
  "isPortListening": true,
  "unitFileContent": "[Unit]\nDescription=...",
  "logs": "Mar 01 10:00:01 ..."
}
Response 404
Unit not found and not in database.
GET /api/services/{unitName}/unitfile Systemd unit file
Response 200
{
  "content": "[Unit]\nDescription=my-api TEST\nAfter=network.target\n\n[Service]\nWorkingDirectory=/opt/my-api-test/app\nExecStart=/opt/my-api-test/app/MyApi\n..."
}
GET /api/services/{unitName}/logs Journal logs
Query Parameters
NameTypeDefaultDescription
linesint50Number of log lines
Response 200
{
  "content": "Mar 01 10:00:01 jarjarbinks my-api[1234]: Application started\n..."
}
GET /api/services/{unitName}/metrics/history Per-service metrics

Per-service CPU and memory time-series data. Same query parameters and response shape as /api/metrics/history.

Query Parameters
NameTypeDefaultDescription
hoursint24How many hours back
intervalint5Aggregation interval in minutes
aggregationstring"max"Aggregation function
Response 200
{
  "dataPoints": [
    { "time": "08:00", "cpu": 5.2, "memory": 12.0 }
  ]
}
GET /api/services/{id}/cicd Generate CI/CD YAML

Generates a Forgejo Actions workflow YAML for the service. Includes deploy steps for all environments (test/prod) that share the same service name. Parses the unit file to extract WorkingDirectory and ExecStart paths.

Path param: id is the managed service database ID (int), not the unit name.
Response 200 application/x-yaml
name: my-api-cicd

on:
  push:
    branches: [ "master" ]
  workflow_dispatch: {}

jobs:
  build_deploy:
    runs-on: dotnet10
    steps:
      - uses: actions/checkout@v4
      ...
Response 404
Service not found.

Service Actions

POST /api/services/{unitName}/start Start service

Starts the systemd unit via systemctl start. No request body.

Response 200
{ "success": true }
POST /api/services/{unitName}/stop Stop service

Stops the systemd unit via systemctl stop. No request body.

Response 200
{ "success": true }
POST /api/services/{unitName}/restart Restart service

Restarts the systemd unit via systemctl restart. No request body.

Response 200
{ "success": true }
POST /api/services/{id}/delete Delete & deprovision

Full deprovision: stops and disables the systemd unit, removes the Caddy route, deletes the app directory, releases allocated ports, and removes the database record. Protected services cannot be deleted.

Path param: id is the managed service database ID (int).
Response 200
{ "success": true }
Response 500
{ "error": "Service X is protected and cannot be deleted" }

Provisioning

POST /api/services/provision Provision new service

Creates a fully provisioned service: systemd unit file, app directory with correct ownership, Caddy reverse proxy route, port allocation, and database record. Can provision test and/or production environments in a single call.

Domain routing:
Test: {name}-test.{domain} (e.g. my-api-test.swphub.dev)
Prod: {name}.{domain} (e.g. my-api.swphub.dev) — no suffix
Request Body
FieldTypeRequiredDescription
namestringrequiredService name. Lowercase, numbers and hyphens only.
domainstringrequiredBase domain for Caddy routes (e.g. swphub.dev).
testEnabledboolProvision a test environment.
productionEnabledboolProvision a production environment.
testPortintPort for the test environment.
productionPortintPort for the production environment.
templatestringUnit file template. Default: "kestrel". Options: kestrel, static, generic.
executableNamestring?Executable/DLL name. Auto-generated from name if omitted (my-api → MyApi).
environmentVariablesstring?KEY=VALUE pairs, one per line. Added to the systemd unit.
restartPolicystringDefault: "always". Options: always, on-failure, on-abnormal, on-abort, no.
restartSecintDelay before restart in seconds. Default: 10.
Request Example
{
  "name": "my-api",
  "domain": "swphub.dev",
  "testEnabled": true,
  "productionEnabled": true,
  "testPort": 5100,
  "productionPort": 5105,
  "template": "kestrel",
  "restartPolicy": "always",
  "restartSec": 10
}
Response 200
{
  "success": true,
  "environments": ["test", "prod"]
}
Response 400
{ "error": "Service name is required" }
{ "error": "At least one environment must be selected" }

Ports

GET /api/ports/next Allocate next port

Allocates and returns the next available port from the configured range. The port is reserved immediately.

Response 200
{ "port": 5101 }
GET /api/ports/suggest Suggest port pair

Suggests a test/production port pair spaced 5 apart, without allocating them. Used by the New Service form to pre-fill port fields.

Response 200
{
  "testPort": 5100,
  "productionPort": 5105
}

Schedules

GET /api/schedules Internal scheduled jobs
Response 200
[
  {
    "name": "metrics-collector",
    "description": "System & per-service CPU/memory metrics",
    "schedule": "every 60s",
    "retention": "7 days",
    "type": "internal"
  }
]

Error Responses

All endpoints return errors in the same shape. The HTTP status code indicates the error category.

{ "error": "Human-readable error message" }
StatusMeaning
400Bad request — validation failure (missing name, no environment selected)
404Resource not found (service, unit name)
500Internal error (systemd failure, shell command failure, database error)