# Config reference

Every setting recognized by the manager, what it does, default value, and where it lives.

The manager loads config in this order (later sources override earlier ones):

1. `appsettings.json` (committed defaults — public, no secrets)
2. `appsettings.{Environment}.json` (typically `Development`, used during `dotnet run`)
3. `appsettings.Local.json` (gitignored — your secrets and per-machine overrides)
4. Environment variables prefixed `WSM_` (e.g. `WSM__AppSettings__Rcon__Password=foo`)
5. Command-line args

For production (release zip), `appsettings.Local.json` is where you set your secrets. The example file shows every field with a placeholder.

---

## Top-level

| Key            | Type    | Default                  | Notes                                                                       |
|----------------|---------|--------------------------|-----------------------------------------------------------------------------|
| `Urls`         | string  | `http://localhost:8080`  | What the manager binds to. Set `http://0.0.0.0:8080` to bind all interfaces (only if you trust your LAN). |
| `AllowedHosts` | string  | `*`                      | Standard ASP.NET Core host filter. Leave default unless you specifically want host validation. |

---

## `AppSettings.ServerInstall`

Where the dedicated server lives — used to compute the path to `R5.log` and to safely supervise the process.

| Key           | Type        | Default                                                                       | Notes |
|---------------|-------------|-------------------------------------------------------------------------------|-------|
| `InstallRoot` | string      | `C:\Program Files (x86)\Steam\steamapps\common\Windrose Dedicated Server`     | Override if your server is somewhere else. |
| `R5LogPath`   | string?     | `null` (computed from InstallRoot)                                            | Override only if your log is in an unusual place. |

The supervision logic uses `InstallRoot` as a path-prefix safety check — the manager will refuse to start/stop a process whose exe path isn't under `InstallRoot`. This prevents accidentally restarting a different game.

---

## `AppSettings.Rcon`

| Key                   | Type    | Default        | Notes                                                              |
|-----------------------|---------|----------------|--------------------------------------------------------------------|
| `Host`                | string  | `127.0.0.1`    | RCON host. `127.0.0.1` = manager + server on same box.             |
| `Port`                | int     | `25575`        | Default Source RCON port.                                          |
| `Password`            | string  | (placeholder)  | **Required.** Set in `Local.json`. Must match WindroseRCON config. |
| `PollIntervalSeconds` | int     | `30`           | How often to poll for player list / server info.                   |
| `Thresholds`          | int[]   | `[0, 1]`       | Player counts to fire `PlayerCountThresholdCrossed` events on.    |

`Thresholds` example: `[0, 1, 8, 15, 16]` fires events when crossing 0, 1, 8, 15, or 16 players in either direction. Useful for "wake up oncall when server hits cap" Discord notifications.

---

## `AppSettings.Process`

Restart supervision behavior.

| Key                          | Type    | Default | Notes |
|------------------------------|---------|---------|-------|
| `Arguments`                  | string  | `""`    | Extra command-line args appended when starting the dedicated server. |
| `CrashloopWindowSeconds`     | int     | `60`    | Detection window for crashloop (multiple unexpected exits inside this window). |
| `CrashloopMaxRestarts`       | int     | `3`     | If the server crashes this many times within the window, the manager stops auto-restarting and emits `ServerCrashloop`. |
| `MinMinutesBetweenRestarts`  | int     | `30`    | Throttles scheduled restarts. Manual restarts (button-driven or hub-driven) bypass this. |

---

## `AppSettings.Auth`

Web-UI sign-in.

| Key                  | Type   | Default       | Notes                                                                  |
|----------------------|--------|---------------|------------------------------------------------------------------------|
| `Required`           | bool   | `false`       | Set `true` to require login at `/login` for every page.                |
| `Password`           | string | (placeholder) | Single shared password (no per-user accounts at this layer).           |
| `CookieLifetimeDays` | int    | `7`           | How long a logged-in cookie lasts.                                     |

When `Required=false`, the UI is wide open to anyone who can reach `Urls` — fine for `localhost` only, dangerous if you bound to `0.0.0.0` or exposed via tunnel.

---

## `AppSettings.Agent`

Manager runtime mode.

| Key        | Type   | Default | Notes                                                                                          |
|------------|--------|---------|------------------------------------------------------------------------------------------------|
| `Headless` | bool   | `false` | When `true`, suppresses optional Discord webhook init logging — useful for unattended deploys. |

---

## `AppSettings.Hub`

Outbound WebSocket agent that streams events to a hub and accepts commands.

| Key           | Type   | Default                                          | Notes                                                                                          |
|---------------|--------|--------------------------------------------------|------------------------------------------------------------------------------------------------|
| `Enabled`     | bool   | `false`                                          | Master switch. When `false`, the hub-agent module doesn't start at all.                       |
| `Url`         | string | `wss://windrose.certifriedmultitool.com/api/agent` | The hub's agent WebSocket URL. Use `wss://` for production, `ws://` for local dev hubs.      |
| `AgentToken`  | string | (placeholder)                                    | Generated by the hub at `/agents/new`. Shown plaintext once; SHA-256 hashed in the hub DB.    |
| `AgentName`   | string | `MyServer`                                       | Display name shown in the hub dashboard. Doesn't have to be unique.                           |

---

## `AppSettings.DiscordChannels`

Logical channel names → webhook URLs. Routing rules (next section) decide which events go where.

```json
"DiscordChannels": [
  { "Name": "server-status",   "WebhookUrl": "https://discord.com/api/webhooks/..." },
  { "Name": "player-activity", "WebhookUrl": "https://discord.com/api/webhooks/..." },
  { "Name": "announcements",   "WebhookUrl": "https://discord.com/api/webhooks/..." }
]
```

You can name channels anything — `oncall-alerts`, `general`, `crash-only`. If you delete an entry, any routing rule pointing at the deleted name simply has no effect (no errors, just nothing posted). Edit / add / remove channels live from `/integrations` in the web UI; the changes persist to `data/discord-channels.json`.

---

## `AppSettings.Routing`

Maps `EventType` → `Channels[]`. Multiple channels per event are fine (broadcasts to all). Editable live at `/routing`; persists to `data/routing-rules.json`. The values in `appsettings.json` are the seed defaults used when `data/routing-rules.json` doesn't exist yet.

Full event list (these are the strings to use in `EventType`):

| Category | Events |
|----------|--------|
| **Manager lifecycle** | `ManagerStarted`, `ManagerStopped`, `ManagerError` |
| **Server lifecycle**  | `ServerStarted`, `ServerReady`, `ServerStopped`, `ServerCrashed`, `ServerCrashloop`, `ServerStartFailed`, `ServerUnresponsive` |
| **RCON**              | `RconConnected`, `RconAuthFailed`, `RconConnectionLost`, `RconReconnected` |
| **Players**           | `PlayerJoined`, `PlayerLeft`, `PlayerCountThresholdCrossed`, `PlayerNameObserved`, `PlayerObservedAtBaseline` |
| **Restarts**          | `RestartScheduled`, `RestartCountdown`, `RestartTriggered`, `RestartCompleted`, `RestartFailed`, `RestartAborted` |
| **Schedules**         | `ScheduledAnnouncementFired`, `ScheduledTaskFailed` |
| **Audit**             | `AdminActionPerformed` |

`PlayerNameObserved` and `PlayerObservedAtBaseline` are not routed by default — they're internal events used to populate the friendly-name lookup, not user-facing notifications.

---

## `AppSettings.Schedules`

Cron-driven actions. Each entry is one of:

```json
"Schedules": [
  {
    "Type": "Announcement",
    "Name": "Hourly help link",
    "Cron": "0 0 * * * *",
    "Message": "Need help? Type /help in chat."
  },
  {
    "Type": "Restart",
    "Name": "Daily 4am restart",
    "Cron": "0 0 4 * * *",
    "WarnMinutesBefore": 10
  }
]
```

Cron uses **6-field** Cronos syntax (seconds-precision): `sec min hour day month weekday`. Schedules are also editable live from `/schedules`.

---

## `Serilog`

Standard Serilog config. The defaults log Information+ to a rolling daily file under `logs/wsm-*.log` and to stdout. Don't touch unless you specifically want different log levels or sinks.

---

## Environment variable overrides

ASP.NET Core's standard config-binding rules apply. Use `__` (double underscore) to descend into nested keys, prefixed with `WSM_`:

```bash
WSM__AppSettings__Rcon__Password=secret
WSM__AppSettings__Hub__Enabled=true
WSM__AppSettings__Hub__AgentToken=xyz
```

Useful when running in containers / CI where you don't want secrets in a config file.

---

## Editing config while the manager is running

- `/integrations` (Discord channels) and `/routing` (event → channel rules) are **live-editable** through the UI. Changes persist to `data/*.json` and take effect immediately — no restart needed.
- `/schedules` is **live-editable** for the same reason.
- Everything else (RCON password, hub token, ports, paths) requires a restart of the manager service.

When the manager service restarts, in-flight Discord posts complete first (graceful shutdown waits up to 5 seconds), then the process exits and Windows Service Manager respawns it.
