# Configuration

The agent reads one JSON file, `client.config.json`. It says which plugins to run, how to configure each one, and where to send the results. Enrollment writes a working file for you. This page covers the file when you want to change it.

## Where the file lives

The agent looks for `client.config.json` in this order and uses the first one that parses:

1. The path given with `--config`. If you name a file and it can't be read, the agent exits instead of falling back to defaults.
2. The current working directory.
3. The agent's state directory, usually `~/.config/daemonitor`. [Getting started](./getting-started.md#install-the-agent-with-the-enrollment-command) lists the full order.
4. `/etc/daemonitor/client.config.json`.

A file that exists but isn't valid JSON is skipped with a warning. If nothing is found, the agent runs with `{ "plugins": ["os"] }`.

The startup banner prints which file it loaded:

```
daemonitor-client 1.2.1 on web-01
  endpoint  https://daemonitor.com/api
  config    /home/deploy/.config/daemonitor/client.config.json
  plugins   os, docker, web
```

Restart the agent after editing the file. It does not watch for changes.

## Structure

```json
{
  "connectors": ["rest-api"],
  "plugins": ["os", "docker", "web"],

  "os": {
    "name": "os",
    "uniqueId": "os",
    "refreshInterval": 30000
  },
  "docker": {
    "name": "docker",
    "uniqueId": "docker",
    "refreshInterval": 60000,
    "dockerBin": "docker",
    "projects": [],
    "exclude": []
  },
  "web": {
    "name": "web",
    "uniqueId": "web",
    "refreshInterval": 60000,
    "endpoints": [
      {
        "name": "Marketing site",
        "url": "https://www.example.com/",
        "expectedStrings": ["Example Domain"],
        "expectedStatusCode": 200,
        "timeout": 5000
      }
    ]
  }
}
```

There are three kinds of top-level key.

| Key | What it does |
|---|---|
| `plugins` | Array of plugin names to run. A plugin not named here does not run, even if it has a block below. |
| `connectors` | Where reports go. Optional. Defaults to `rest-api`. |
| `<plugin name>` | One object per plugin, keyed by the same name used in `plugins`. |

The system key and endpoint do not belong in this file. The agent takes them from `credentials.json`, a flag or an environment variable, and passes them to the `rest-api` connector itself.

## The plugins array

Names are matched case-insensitively. The agent in `@daemonitor/client` 1.2.x loads these plugins:

| Name | Watches |
|---|---|
| `os` | CPU, memory, disk, network, uptime |
| `pm2` | Processes managed by pm2 |
| `docker` | Docker containers, one report per compose project |
| `lxc` | LXC, LXD or Incus containers |
| `web` | HTTP checks against URLs |
| `wordpress` | WordPress security posture through WP-CLI |
| `logwatch` | Web access logs for attack patterns |
| `netscan` | Ping reachability of LAN devices |
| `snmp` | SNMP devices such as printers and NAS units |
| `tplink` | TP-Link extenders and access points |
| `3cx` | A 3CX phone system |
| `patroni` | A Patroni Postgres node. Needed for [cluster](./clusters.md) leader tracking |
| `adsense` | Google AdSense earnings |
| `ga4` | Google Analytics 4 properties |

When enrollment builds your config and you picked no plugins, it uses what the agent found on the machine. It enables `pm2` if the binary is on `PATH`, `docker` if the socket is readable by the user the service will run as, `lxc` if an LXD socket exists, and always `os`.

## Per-plugin blocks

Every plugin reads the block named after it. Three fields are common to all of them.

| Field | Meaning |
|---|---|
| `name` | Display name for the plugin's row in the dashboard. |
| `uniqueId` | The identity of the row on the server. Reports with the same `uniqueId` update the same service. Defaults to `<name>-<n>`, where `n` depends on plugin load order, so set it explicitly. Cluster rules match on it. |
| `refreshInterval` | Milliseconds between reports. |

Defaults when `refreshInterval` is absent:

| Plugin | Default |
|---|---|
| `pm2` | 5000 |
| `os`, `patroni`, `netscan` | 30000 |
| `docker`, `lxc`, `web`, `logwatch`, `3cx` | 60000 |
| `tplink` | 120000 |
| `wordpress`, `snmp` | 300000 |
| `adsense`, `ga4` | 900000 |

Don't set an interval longer than the dashboard's staleness window. A service that hasn't reported for 5 minutes shows as offline. `wordpress` gets 15 minutes and `adsense` and `ga4` get 30. See [How status works](./how-status-works.md#offline).

The plugin-specific fields, such as `endpoints` for `web` or `sites` for `wordpress`, are documented on each plugin's own page.

### Web checks managed from the dashboard

Web checks can live in the dashboard under Settings, Web monitors, instead of in this file. The `web` plugin fetches them when its block has both of these fields:

```json
"web": {
  "uniqueId": "web",
  "refreshInterval": 60000,
  "configUrl": "https://daemonitor.com/api/monitors/for-client",
  "systemKey": "<this system's key>",
  "endpoints": []
}
```

The plugin merges the two lists by name and URL. A check defined in the dashboard wins over a local one with the same name and URL. If a fetch fails, the plugin keeps running the last list it fetched.

## Connectors

A connector decides where reports go. Two exist.

| Type | What it does |
|---|---|
| `rest-api` | Sends each report to the server with `PUT /api/clientstate/update`. This is the one you want. |
| `console` | Prints a line per report to stdout. Set `"verbose": true` in its config to print the full payload. Useful for testing a plugin without sending anything. |

The short form is an array of names:

```json
"connectors": ["rest-api", "console"]
```

The long form takes a config object:

```json
"connectors": [
  { "type": "rest-api", "name": "REST API", "config": {} },
  { "type": "console", "config": { "verbose": true } }
]
```

`rest-api` always inherits the endpoint and system key the agent resolved at startup, so its config can stay empty. A value you set in its `config`, such as `apiUrl` or `systemKey`, overrides the inherited one.

Each push has a 15 second timeout. A failed push is logged and dropped. The next interval sends a fresh report, so nothing queues up.

## Command-line flags and environment variables

Every flag has an environment variable. A flag wins over the variable.

| Flag | Variable | Meaning |
|---|---|---|
| `--token <token>` | `DAEMONITOR_TOKEN` | Enrollment token. Only used when no key is stored yet. |
| `--key <key>` | `DAEMONITOR_KEY`, or `SYSTEM_KEY` | System key. Skips enrollment. |
| `--endpoint <url>` | `DAEMONITOR_ENDPOINT`, or `API_BASE_URL` | API base URL. Default `https://daemonitor.com/api`. |
| `--config <path>` | | Path to `client.config.json`. |
| `--dir <path>` | `DAEMONITOR_DIR` | State directory for `credentials.json` and the config. |
| `--plugins <list>` | | Comma-separated plugin list. Replaces the `plugins` array from the file. |
| `--install-service` | | Install and start the systemd unit, then exit. Needs root. |
| `-h`, `--help` | | Print usage. |
| `-v`, `--version` | | Print the agent version. |

The agent also loads a `.env` file from its working directory, so these variables can live there.

Where the agent gets its key, in order:

1. `--key` or `DAEMONITOR_KEY`.
2. `credentials.json` from an earlier enrollment.
3. Enrollment with `--token`.

The endpoint follows the same idea. With no `--endpoint`, the agent uses the endpoint stored in `credentials.json`, so an agent enrolled against a self-hosted server keeps talking to it.

## A minimal file for a web server

```json
{
  "plugins": ["os", "web"],
  "os": { "name": "os", "uniqueId": "os" },
  "web": {
    "name": "web",
    "uniqueId": "web",
    "endpoints": [
      { "name": "Shop", "url": "https://shop.example.com/", "expectedStatusCode": 200 },
      { "name": "API health", "url": "https://api.example.com/health", "expectedStrings": ["ok"] }
    ]
  }
}
```

Run `daemonitor-client` once in the foreground and check that the banner lists both plugins, then restart the service.
