# Configuration

> psLens reads a config.yaml file from the same directory as the binary.

---

Site [llms.txt](/llms.txt)

---

## Configuration

psLens reads a `config.yaml` file from the same directory as the binary. There is no external database server to set up. An embedded NATS data store saves persistent state (configuration history, report results, and alert state).

### Configuration Synchronization

psLens synchronizes active configuration automatically between `config.yaml` and the embedded NATS KV store:

- **Bare-Minimum Boot**: The server can boot with zero configured database connections. Administrators can add database connections later directly through the Settings UI at `/settings`.
- **Disk Edit Recognition**: On boot, psLens checks modification timestamps. If `config.yaml` is newer than the NATS KV snapshot, psLens syncs file changes into NATS KV automatically.
- **UI Auto Write-Back**: Changes made via the Settings UI update NATS KV and automatically write back to `config.yaml` on disk.

### Password & Secret Security Options

psLens provides three secure methods for passing database and server credentials:

1. **1Password Secret References (`op://...`)**:
   Pass 1Password Secret URIs directly in `config.yaml` or JSON configurations:

   ```yaml
   databases:
     - name: "PROD"
       username: "PSLENS_API"
       password: "op://Employee-Vault/PeopleSoft-Prod-API/password"
   
   smtp:
     host: "op://Employee-Vault/SMTP-Mailtrap/host"
     port: "op://Employee-Vault/SMTP-Mailtrap/port"
     username: "op://Employee-Vault/SMTP-Mailtrap/username"
     password: "op://Employee-Vault/SMTP-Mailtrap/password"
     fromName: "op://Employee-Vault/SMTP-Mailtrap/from_name"
     fromEmail: "op://Employee-Vault/SMTP-Mailtrap/from_email"

   auth:
     oidc:
       issuerUrl: "op://Employee-Vault/OIDC-Okta/issuer_url"
       clientId: "op://Employee-Vault/OIDC-Okta/client_id"
       clientSecret: "op://Employee-Vault/OIDC-Okta/client_secret"
       redirectUrl: "op://Employee-Vault/OIDC-Okta/redirect_url"
   ```

   Set `OP_SERVICE_ACCOUNT_TOKEN` in the environment. Secrets resolve in-memory at boot using the 1Password SDK and are never written back to disk or KV storage in plaintext.

2. **Master Key Encryption (`PSLENS_MASTER_KEY`)**:
   Set `PSLENS_MASTER_KEY` (a 64-character hex AES-256 key). psLens saves passwords in NATS KV and YAML as encrypted strings (`ENC[...]`).

3. **Environment Variable Overrides**:
   Override passwords dynamically via environment variables without editing files:

   ```bash
   export PSLENS_DB_PROD_PASSWORD="YourPasswordHere"
   ```

### Bare Minimum Configuration Example

To start psLens with zero pre-configured database connections, use the following minimal `config.yaml`:

```yaml
server:
  port: 8080
```

### Full Configuration Examples

psLens supports configuration in either YAML (`config.yaml`) or JSON (`config.json`) format. The application detects the format automatically on boot.

#### YAML Configuration Example (`config.yaml`)

```yaml
# Schema validation pointer (optional, for IDE support)
# yaml-language-server: $schema=http://localhost:8080/static/config-schema.json

server:
  port: 8080
  host: "0.0.0.0"
  natsStoreDir: "./data/nats"
  projectStoreDir: "./data/projects"
  dmsStoreDir: "./data/dms"
  traceStoreDir: "./data/traces"
  appBaseURL: "http://localhost:8080"
  recentlyViewed:
    maxItems: 20

databases:
  - name: "PROD"
    description: "Production PeopleSoft HR"
    baseURL: "https://psft.example.com:8000/PSIGW/RESTListeningConnector/PSFT_HR/CHG_PSLENS_SWSPQL/"
    username: "PSLENS_API"
    password: "your-api-password"
    piaURL: "https://psft.example.com/psp/ps/"
    timezone: "America/Chicago"
    production: true
    alerts:
      enabled: true
      intervalMinutes: 10

  - name: "DEV"
    description: "Development Environment"
    baseURL: "https://psftdev.example.com:8000/PSIGW/RESTListeningConnector/PSFT_HR/CHG_PSLENS_SWSPQL/"
    username: "PSLENS_API"
    password: "dev-api-password"
    timezone: "America/Chicago"
    production: false
    downtimes:
      - name: "Nightly Downtime"
        enabled: true
        startTime: "22:00"
        endTime: "06:00"
        daysOfWeek: [1, 2, 3, 4, 5]
      - name: "Weekend Maintenance"
        enabled: true
        startAt: "2026-07-04T00:00:00Z"
        endAt: "2026-07-05T23:59:59Z"

alerts:
  enabled: true
  intervalMinutes: 5
  checks:
    long_running_processes:
      enabled: true
      thresholdMinutes: 20
      anomalyMultiplier: 4.0
      anomalyMinBaselineMinutes: 10
    process_errors:
      enabled: true
      lookbackHours: 24
    ib_operation_errors:
      enabled: true
      lookbackHours: 24
    ib_pub_contract_errors:
      enabled: true
      lookbackHours: 24
    ib_sub_contract_errors:
      enabled: true
      lookbackHours: 24
    ib_operation_stalled:
      enabled: true
      thresholdMinutes: 30
    ib_pub_contract_stalled:
      enabled: true
      thresholdMinutes: 30
    ib_sub_contract_stalled:
      enabled: true
      thresholdMinutes: 30
  genericSWSAlerts:
    - id: "stale_users"
      name: "Stale User Accounts"
      enabled: true
      severity: "warning"
      alertOn: "row_found"
      message: "Warning: Stale user accounts detected"
      query:
        records:
          - recordName: "PSOPRDEFN"
            sqlWhereClause: "LASTUPDDTTM < CAST('2026-01-01' AS TIMESTAMP) AND ACCTLOCK = 0"
        rowLimit: 5

auth:
  enabled: true
  authorizedUsers:
    - "admin@example.com"
    - "auditor@example.com"

smtp:
  host: "smtp.mailtrap.io"
  port: "2525"
  username: "smtp-user"
  password: "smtp-password"
  fromName: "psLens Alerts"
  fromEmail: "alerts@pslens.example.com"

notifications:
  subscriptions:
    - id: "team-email"
      enabled: true
      alertTypes: ["*"]
      databases: ["PROD"]
      severityMin: "warning"
      type: "email"
      target: "psoft-alerts@example.com"
    - id: "slack-webhook"
      enabled: true
      alertTypes: ["process_errors", "ib_operation_errors"]
      databases: ["*"]
      type: "webhook"
      target: "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
    - id: "teams-webhook"
      enabled: true
      alertTypes: ["*"]
      databases: ["*"]
      type: "webhook"
      target: "https://example.webhook.office.com/webhookb2/..."
    - id: "google-chat-webhook"
      enabled: true
      alertTypes: ["*"]
      databases: ["*"]
      type: "webhook"
      target: "https://chat.googleapis.com/v1/spaces/AAAA.../messages?key=...&token=..."

reports:
  - id: "security-full-access-permlists"
    enabled: true
    dbNames: ["PROD"]
    schedule:
      interval: "daily"
      timeOfDay: "02:00"
    emailTarget: "psoft-alerts@example.com"
```

#### JSON Configuration Example (`config.json`)

```json
{
  "$schema": "http://localhost:8080/static/config-schema.json",
  "server": {
    "port": 8080,
    "host": "0.0.0.0",
    "natsStoreDir": "./data/nats",
    "projectStoreDir": "./data/projects",
    "dmsStoreDir": "./data/dms",
    "traceStoreDir": "./data/traces",
    "appBaseURL": "http://localhost:8080",
    "recentlyViewed": {
      "maxItems": 20
    }
  },
  "databases": [
    {
      "name": "PROD",
      "description": "Production PeopleSoft HR",
      "baseURL": "https://psft.example.com:8000/PSIGW/RESTListeningConnector/PSFT_HR/CHG_PSLENS_SWSPQL/",
      "username": "PSLENS_API",
      "password": "your-api-password",
      "piaURL": "https://psft.example.com/psp/ps/",
      "timezone": "America/Chicago",
      "production": true,
      "alerts": {
        "enabled": true,
        "intervalMinutes": 10
      }
    },
    {
      "name": "DEV",
      "description": "Development Environment",
      "baseURL": "https://psftdev.example.com:8000/PSIGW/RESTListeningConnector/PSFT_HR/CHG_PSLENS_SWSPQL/",
      "username": "PSLENS_API",
      "password": "dev-api-password",
      "timezone": "America/Chicago",
      "production": false
    }
  ],
  "alerts": {
    "enabled": true,
    "intervalMinutes": 5,
    "checks": {
      "long_running_processes": {
        "enabled": true,
        "thresholdMinutes": 20,
        "anomalyMultiplier": 4.0,
        "anomalyMinBaselineMinutes": 10
      },
      "process_errors": {
        "enabled": true,
        "lookbackHours": 24
      },
      "ib_operation_errors": {
        "enabled": true,
        "lookbackHours": 24
      },
      "ib_pub_contract_errors": {
        "enabled": true,
        "lookbackHours": 24
      },
      "ib_sub_contract_errors": {
        "enabled": true,
        "lookbackHours": 24
      },
      "ib_operation_stalled": {
        "enabled": true,
        "thresholdMinutes": 30
      },
      "ib_pub_contract_stalled": {
        "enabled": true,
        "thresholdMinutes": 30
      },
      "ib_sub_contract_stalled": {
        "enabled": true,
        "thresholdMinutes": 30
      }
    },
    "genericSWSAlerts": [
      {
        "id": "stale_users",
        "name": "Stale User Accounts",
        "enabled": true,
        "severity": "warning",
        "alertOn": "row_found",
        "message": "Warning: Stale user accounts detected",
        "query": {
          "records": [
            {
              "recordName": "PSOPRDEFN",
              "sqlWhereClause": "LASTUPDDTTM < CAST('2026-01-01' AS TIMESTAMP) and ACCTLOCK = 0"
            }
          ],
          "rowLimit": 5
        }
      }
    ]
  },
  "auth": {
    "enabled": true,
    "authorizedUsers": [
      "admin@example.com",
      "auditor@example.com"
    ]
  },
  "smtp": {
    "host": "smtp.mailtrap.io",
    "port": "2525",
    "username": "smtp-user",
    "password": "smtp-password",
    "fromName": "psLens Alerts",
    "fromEmail": "alerts@pslens.example.com"
  },
  "notifications": {
    "subscriptions": [
      {
        "id": "team-email",
        "enabled": true,
        "alertTypes": ["*"],
        "databases": ["PROD"],
        "severityMin": "warning",
        "type": "email",
        "target": "psoft-alerts@example.com"
      },
      {
        "id": "slack-webhook",
        "enabled": true,
        "alertTypes": ["process_errors", "ib_operation_errors"],
        "databases": ["*"],
        "type": "webhook",
        "target": "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
      },
      {
        "id": "teams-webhook",
        "enabled": true,
        "alertTypes": ["*"],
        "databases": ["*"],
        "type": "webhook",
        "target": "https://example.webhook.office.com/webhookb2/..."
      },
      {
        "id": "google-chat-webhook",
        "enabled": true,
        "alertTypes": ["*"],
        "databases": ["*"],
        "type": "webhook",
        "target": "https://chat.googleapis.com/v1/spaces/AAAA.../messages?key=...&token=..."
      }
    ]
  },
  "reports": [
    {
      "id": "security-full-access-permlists",
      "enabled": true,
      "dbNames": ["PROD"],
      "schedule": {
        "interval": "daily",
        "timeOfDay": "02:00"
      },
      "emailTarget": "psoft-alerts@example.com"
    }
  ]
}
```

---

### Server Settings

The `server` section controls how psLens listens for incoming connections and where it stores data.

|        Setting        |      Default      |                                         Description                                          |
| --------------------- | ----------------- | -------------------------------------------------------------------------------------------- |
| `port`                | `8080`            | TCP port psLens listens on                                                                   |
| `host`                | `0.0.0.0`         | Network interface to bind (use `127.0.0.1` to restrict to localhost)                         |
| `natsStoreDir`        | `./data/nats`     | Directory for persistent NATS data (report results, alert history)                           |
| `projectStoreDir`     | `./data/projects` | Directory for uploaded PeopleSoft project XML definitions                                    |
| `dmsStoreDir`         | `./data/dms`      | Directory for imported Data Mover (DMS) script libraries                                     |
| `traceStoreDir`       | `./data/traces`   | Directory for uploaded PeopleSoft trace files (`.tracesql`, `.trc`, `.aet`)                  |
| `outageRetentionDays` | `90`              | Retention period in days for historical downtime and outage events stored in NATS JetStream. |

> **Tip:** Storage directories (`natsStoreDir`, `traceStoreDir`, etc.) can also be configured via environment variables (`PSLENS_TRACE_STORE_DIR`, `PSLENS_PROJECT_STORE_DIR`, etc.) and should be placed on persistent storage.

---

### Database Connections

You can configure one or more PeopleSoft databases under the `databases` list. psLens monitors the health of each connection and shows status on the dashboard.

|     Setting     | Required |                                                                 Description                                                                 |
| --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`          | Yes      | Short identifier shown in the UI (e.g., `PROD`, `DEV`)                                                                                      |
| `description`   | Yes      | Human-readable label for the database                                                                                                       |
| `baseURL`       | Yes      | Full URL to the SWS psoftQL endpoint, including the service name                                                                            |
| `username`      | Yes      | PeopleSoft operator ID for API authentication                                                                                               |
| `password`      | Yes      | Password for the operator ID                                                                                                                |
| `piaURL`        | No       | Base URL for PeopleSoft Internet Architecture (used for deep links to PeopleSoft pages, if supported)                                       |
| `timezone`      | No       | IANA timezone name for the database server (e.g., `America/Chicago`). Defaults to `UTC` if not set. Used to interpret timestamps correctly. |
| `downtimes`     | No       | List of scheduled downtime windows to suppress alerts and monitoring.                                                                       |
| `notifications` | No       | Database-specific notification delivery settings (bypasses global subscriptions).                                                           |

#### Scheduled Downtimes

Downtimes allow silencing alerts and pausing connection health checks for non-production environments that go offline regularly (e.g., overnight or on weekends).

Each entry in the `downtimes` list supports the following settings:

|   Setting    | Required |                                                                                Description                                                                                 |
| ------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`       | Yes      | Identifier for the downtime window.                                                                                                                                        |
| `enabled`    | Yes      | Whether the downtime rule is active.                                                                                                                                       |
| `startTime`  | No       | Start time of day in `HH:MM` format (e.g., `22:00`). Set together with `endTime`.                                                                                          |
| `endTime`    | No       | End time of day in `HH:MM` format (e.g., `06:00`). An end time earlier than the start crosses midnight. `24:00` means end of day, so an all-day window is `00:00`–`24:00`. |
| `daysOfWeek` | No       | Array of integers for days of week. `0` is Sunday, `1` is Monday, ..., `6` is Saturday. Omit to apply every day. An overnight window belongs to the day it starts.         |
| `startAt`    | No       | Specific start timestamp in RFC3339 format (e.g., `2026-07-04T00:00:00Z`) for one-off maintenance.                                                                         |
| `endAt`      | No       | Specific end timestamp in RFC3339 format (e.g., `2026-07-05T23:59:59Z`) for one-off maintenance.                                                                           |

Values for `startTime` and `endTime` use wall-clock times in the connection `timezone` (UTC if unset). psLens checks rules when saving configuration. A rule must define a recurring window (`startTime` and `endTime`) or a one-off range (`startAt` and `endAt`).

During scheduled downtime, the connection manager sets database status to `Downtime` on the dashboard. psLens skips background alert checks and suppresses notification dispatches. The database does not count against overall health. When the window ends, psLens checks the connection immediately. It treats the first failed check as an outage. Size windows to cover system boot time.

A weekend window that spans from Saturday evening to Monday morning requires two rules. An overnight window belongs to its start day:

```yaml
downtimes:
  - name: "Weekend nights"
    enabled: true
    startTime: "22:00"
    endTime: "06:00"
    daysOfWeek: [6, 0]   # Sat night -> Sun morning, Sun night -> Mon morning
  - name: "Sunday all day"
    enabled: true
    startTime: "00:00"
    endTime: "24:00"
    daysOfWeek: [0]
```

#### The baseURL Format

The `baseURL` is the Integration Broker REST endpoint for the SWS service. It follows this pattern:

```text
https://{igw-host}:{port}/PSIGW/RESTListeningConnector/{database-name}/CHG_PSLENS_SWSPQL/
```

You must include the trailing slash.

#### Environment Variable Overrides

Sensitive settings like passwords can be overridden with environment variables to avoid storing them in `config.yaml`. The override format is:

```text
PSLENS_DB_{NAME}_{FIELD}
```

Where `{NAME}` matches the `name` field of the database in `config.yaml`.

For Fly.io and Docker container compatibility, non-alphanumeric characters (such as dashes `-` or dots `.`) in database names are automatically converted to underscores (`_`) and uppercased.

For example, for a database named `COOL-DB-FOR-DEV`, you can use:

```bash
export PSLENS_DB_COOL_DB_FOR_DEV_PASSWORD="my-secure-password"
```

The raw name `PSLENS_DB_COOL-DB-FOR-DEV_PASSWORD` remains supported for backward compatibility where supported by your shell.

#### 1Password Secret References

You can place 1Password secret references (`op://...`) directly in `config.yaml` or through the Settings UI. Supported fields include:

- **Database Connections**: `baseURL`, `username`, `password`, `piaURL`
- **Database Notifications**: `notifications.emailTarget`, `notifications.webhookTarget`
- **Database WebLib Health Checks**: `alerts.checks[].weblibTestTargets[].url`, `username`, `password`
- **SMTP Mailer**: `smtp.host`, `smtp.port`, `smtp.username`, `smtp.password`, `smtp.fromName`, `smtp.fromEmail`
- **Authentication (OIDC)**: `auth.oidc.issuerUrl`, `auth.oidc.clientId`, `auth.oidc.clientSecret`, `auth.oidc.redirectUrl`
- **Notifications & Subscriptions**: `notifications.email_lists[].emails`, `notifications.webhooks[].url`, `notifications.subscriptions[].target`

```yaml
databases:
  - name: "PROD"
    baseURL: "op://VaultName/ItemName/base_url"
    username: "PSLENS_API"
    password: "op://VaultName/ItemName/password"
```

To resolve 1Password secret references at boot, set a 1Password Service Account token in the environment:

```bash
export OP_SERVICE_ACCOUNT_TOKEN="op_service_account_token_here"
```

When you provide `OP_SERVICE_ACCOUNT_TOKEN` (or `PSLENS_OP_SERVICE_ACCOUNT_TOKEN`), psLens resolves all `op://` references on boot using the 1Password SDK. Without this variable, psLens skips 1Password resolution.

#### Database-Specific Notifications

By default, global subscription rules route alert notifications for all databases (see [Notifications & Webhooks Settings](#notifications--webhooks-settings)).

You can configure database-specific notifications under the `notifications` property of a database entry. This setting acts as an override. Database-level targets receive all alerts and resolutions for that database, bypassing global subscriptions.

Each entry in the `notifications` block supports the following settings:

|     Setting      |  Type   |                                     Description                                      |
| ---------------- | ------- | ------------------------------------------------------------------------------------ |
| `emailEnabled`   | Boolean | Activates email notifications for this database.                                     |
| `emailTarget`    | String  | Comma-separated list of recipient email addresses.                                   |
| `webhookEnabled` | Boolean | Activates webhook notifications for this database.                                   |
| `webhookTarget`  | String  | Webhook ID or destination URL (Slack, MS Teams, Google Chat, or a generic endpoint). |

Example database connection configuration with inline notifications:

```yaml
databases:
  - name: "PROD"
    description: "Production Environment"
    baseURL: "https://psft.example.com:8000/PSIGW/RESTListeningConnector/PSFT_HR/CHG_PSLENS_SWSPQL/"
    username: "PSLENS_API"
    password: "securepassword"
    notifications:
      emailEnabled: true
      emailTarget: "prod-alerts@example.com"
      webhookEnabled: true
      webhookTarget: "https://hooks.slack.com/services/..."
```

---

### Alerts Configuration

The `alerts` section controls the background alert checking system. See the [Alerts](/docs/alerts/) section for details on what each alert detects.

#### Top-Level Alert Settings

|      Setting       | Default |                                                    Description                                                    |
| ------------------ | ------- | ----------------------------------------------------------------------------------------------------------------- |
| `enabled`          | `false` | Whether to run background alert checks                                                                            |
| `intervalMinutes`  | `5`     | How often (in minutes) to run all alert checks                                                                    |
| `genericSWSAlerts` | `[]`    | List of queryable SWS alerts. See [Generic SWS Alerts](/docs/alerts/generic-alerts/) for details. |

#### Alert Check Settings

Each alert type under `checks` supports some or all of the following settings:

|           Setting           |                                                                            Description                                                                            |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`                   | Whether this check is active                                                                                                                                      |
| `thresholdMinutes`          | For stalled/long-running checks: how many minutes before flagging (default varies by check). For `long_running_processes`, this is the fallback static threshold. |
| `lookbackHours`             | For error checks: how many hours back to look for failures (default varies by check)                                                                              |
| `excludeProcesses`          | List of process names to skip (for process-related checks)                                                                                                        |
| `excludeOperations`         | List of IB operation names to skip (for Integration Broker checks)                                                                                                |
| `anomalyMultiplier`         | Multiplier applied to the rolling median duration to calculate the dynamic threshold (default: `4.0`)                                                             |
| `anomalyMinBaselineMinutes` | Minimum baseline duration in minutes. psLens caps dynamic thresholds to at least this value to prevent false alerts on very fast processes (default: `10`)        |

#### Available Alert Checks

|         Check Key         |               Name                |                                                               Description                                                               |
| ------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `long_running_processes`  | Long-Running Processes            | Flags processes running longer than their rolling median-based expected runtime (or `thresholdMinutes` fallback)                        |
| `process_errors`          | Process Errors                    | Finds processes that failed within `lookbackHours` (default: 24 hours)                                                                  |
| `ib_operation_errors`     | IB Operation Errors               | Finds async IB operations in Error or Timeout status within `lookbackHours` (default: 24 hours)                                         |
| `ib_pub_contract_errors`  | IB Publication Contract Errors    | Finds pub contracts in Error or Timeout status within `lookbackHours` (default: 24 hours)                                               |
| `ib_sub_contract_errors`  | IB Subscription Contract Errors   | Finds sub contracts in Error or Timeout status within `lookbackHours` (default: 24 hours)                                               |
| `ib_operation_stalled`    | IB Operations Stalled             | Finds async IB operations stuck in New or Working status longer than `thresholdMinutes` (default: 30 min)                               |
| `ib_pub_contract_stalled` | IB Publication Contracts Stalled  | Finds pub contracts stuck in New or Working status longer than `thresholdMinutes` (default: 30 min)                                     |
| `ib_sub_contract_stalled` | IB Subscription Contracts Stalled | Finds sub contracts stuck in New or Working status longer than `thresholdMinutes` (default: 30 min)                                     |
| `locked_oprid_processes`  | Locked OPRID Scheduled Processes  | Finds queued or scheduled processes whose submitting OPRID has a locked account                                                         |
| `backlogged_processes`    | Backlogged Processes              | Detects processes currently stuck in a Queued or Blocked state longer than `thresholdMinutes` (default: 30 min)                         |
| `queue_latency`           | Queue Latency                     | Detects processes that experienced a start delay (BEGINDTTM - RUNDTTM) greater than `thresholdMinutes` (default: 15 min)                |
| `failed_logins`           | Failed Logins                     | Detects users with excessive failed login attempts in `PSPTLOGINAUDIT` (defaults to > `thresholdCount` of 5)                            |
| `process_run_check`       | Process Run Check                 | Monitors configured critical processes and alerts when they haven't run successfully within their configured time window                |
| `ib_operation_volume`     | Abnormal IB Operation Volume      | Detects when IB operation instance volume exceeds the historical average by a percentage specified in `thresholdCount` (default: 50)    |
| `ib_pub_contract_volume`  | Abnormal IB Pub Contract Volume   | Detects when IB publication contract volume exceeds the historical average by a percentage specified in `thresholdCount` (default: 50)  |
| `ib_sub_contract_volume`  | Abnormal IB Sub Contract Volume   | Detects when IB subscription contract volume exceeds the historical average by a percentage specified in `thresholdCount` (default: 50) |
| `ib_sync_exceptions`      | IB Sync Operation Exceptions      | Detects synchronous service operations with errors in `PSIBLOGHDR` within `lookbackHours` (default: 24 hours). Disabled by default.     |
| `no_process_completed`    | No Process Completed              | Fires when no process has successfully completed within the `lookbackHours` (default: 1 hour)                                           |
| `ib_down`                 | Integration Broker Down           | Alerts when SWS REST endpoint connection failures indicate the Integration Broker is down                                               |
| `weblib_down`             | Web Server / WebLib Down          | Alerts when PeopleSoft Web Server is down or configured WebLib URLs fail to respond                                                     |
| `ib_no_active_domain`     | IB No Active Domain               | Alerts when there is no active domain found in `PSAPMSGDOMSTAT`                                                                         |
| `ib_dispatcher_down`      | IB Dispatcher Down                | Alerts when an Integration Broker dispatcher process is inactive or has not updated status within `thresholdMinutes` (default: 10 min)  |
| `ib_nodes_down`           | IB Nodes Down                     | Alerts when there are entries in `PSNODESDOWN` indicating message nodes are down                                                        |

---

### Authentication Settings

The `auth` section configures native authentication (email-based magic link or native OpenID Connect SSO). psLens enables authentication by default. To disable authentication, set `enabled: false` explicitly in `config.yaml`. Authentication settings are file-authoritative. You cannot change or turn off authentication from the web interface.

|          Setting           |             Default              |                                     Description                                      |
| -------------------------- | -------------------------------- | ------------------------------------------------------------------------------------ |
| `enabled`                  | `true`                           | Defaults to `true`. To disable, set to `false` explicitly in `config.yaml`.          |
| `mode`                     | `magic_link`                     | Authentication mode: `magic_link` or `oidc`.                                         |
| `authorizedUsers`          | `[]`                             | Whitelist of email addresses allowed to log in (case-insensitive).                   |
| `oidc.issuerUrl`           | -                                | OpenID Connect Issuer URL (supports `op://` 1Password references).                   |
| `oidc.clientId`            | -                                | OIDC Application Client ID (supports `op://` 1Password references).                  |
| `oidc.clientSecret`        | -                                | OIDC Application Client Secret (supports `op://` 1Password references).              |
| `oidc.redirectUrl`         | -                                | Optional custom OAuth2 callback URL (supports `op://` 1Password references).         |
| `oidc.scopes`              | `["openid", "profile", "email"]` | OIDC authorization scopes requested from the Identity Provider.                      |
| `oidc.allowedGroups`       | `[]`                             | Optional list of required group claims to restrict access.                           |
| `oidc.groupClaim`          | `groups`                         | Claim key in the ID token used for group membership verification.                    |
| `ipAllowlist.enabled`      | `false`                          | Enable IP allowlist enforcement. Fails closed (denies all) if list is empty.         |
| `ipAllowlist.allowedCIDRs` | `[]`                             | List of allowed CIDR blocks or IP addresses (e.g. `["10.0.0.0/8", "192.168.1.50"]`). |

---

### SMTP Settings

Configure email server settings under **Settings → Notifications** (Email Server card). Changes apply to alert and report emails immediately. Login (magic link) emails use new settings after the next server restart. A blank password on save keeps the stored password.

The `smtp` block in `config.yaml` seeds the email server on first boot only. After initial boot, settings saved in the UI remain authoritative. `PSLENS_SMTP_*` environment variables override both sources and make the UI card read-only.

|   Setting   | Default  |                               Description                                |
| ----------- | -------- | ------------------------------------------------------------------------ |
| `host`      | -        | SMTP server hostname/IP (supports `op://` 1Password references)          |
| `port`      | -        | SMTP port (e.g. `25`, `465`, `587`, `2525`, supports `op://` references) |
| `username`  | -        | Username for SMTP auth (supports `op://` 1Password references)           |
| `password`  | -        | Password for SMTP auth (supports `op://` 1Password references)           |
| `fromName`  | `psLens` | Sender name shown in emails (supports `op://` 1Password references)      |
| `fromEmail` | -        | Sender email address for SMTP. Required: no email is sent without it (supports `op://` 1Password references) |

Test outbound email on the same card: enter a recipient address under Send Test Email and click Send Test. psLens records the test result in Alert Delivery History.

---

### Notifications & Webhooks Settings

Notification rules determine where psLens dispatches alert messages. Manage rules in the app under **Settings → Notifications**. There, you configure and test email distribution lists, webhook destinations, and subscription routing rules. For step-by-step instructions on creating webhooks in Google Chat, Slack, and Microsoft Teams, see [Webhook Integrations](/docs/getting-started/webhook-integrations/).

#### Email Distribution Lists (`notifications.email_lists`)

Predefined recipient lists referenced by notifications and reports:

```yaml
notifications:
  email_lists:
    ops_team:
      name: "Operations Team"
      emails: "ops@company.com, pager@company.com"
```

#### Webhooks (`notifications.webhooks`)

Predefined incoming webhook URLs for Slack, Microsoft Teams Workflows (Adaptive Cards), Google Chat (`cardsV2`), or custom JSON receivers:

```yaml
notifications:
  webhooks:
    slack_alerts:
      name: "Slack Alert Channel"
      url: "https://hooks.slack.com/services/..."
    teams_ops:
      name: "Teams Operations Channel"
      url: "https://default...powerautomate.../invoke"
    gchat_ops:
      name: "Google Chat Space"
      url: "https://chat.googleapis.com/v1/spaces/.../messages?key=...&token=..."
```

To create a Google Chat incoming webhook URL, open the target Space in [Google Chat](https://chat.google.com), click the **Space name** header → **Apps & integrations** → **+ Add webhooks**, save the webhook, and copy the `https://chat.googleapis.com/v1/spaces/.../messages?key=...&token=...` link. See [Webhook Integrations](/docs/getting-started/webhook-integrations/) for full setup instructions.

#### Subscriptions (`notifications.subscriptions`)

Routing subscriptions matching fired alerts and dispatching them to email or webhook targets:

|   Property    |      Type       |                                        Description                                         |
| ------------- | --------------- | ------------------------------------------------------------------------------------------ |
| `id`          | String          | Unique identifier for the subscription                                                     |
| `enabled`     | Boolean         | Activates or silences the subscription                                                     |
| `alertTypes`  | List of Strings | Alert check keys to match (e.g. `["*"]` for all, or `["process_errors"]`)                  |
| `databases`   | List of Strings | Database names to match (e.g. `["*"]` or `["PROD"]`)                                       |
| `severityMin` | String          | Minimum alert severity (`"info"`, `"warning"`, `"critical"`)                               |
| `type`        | String          | Dispatch protocol: `"email"` or `"webhook"`                                                |
| `target`      | String          | Target destination: email address, distribution list key, or webhook destination URL / key |

*Note: psLens automatically detects Slack, MS Teams Adaptive Card, and Google Chat webhook URLs and formats native payloads accordingly.*

---

### Reports Scheduling Settings

The `reports` section allows you to define static scheduling configs and completion notifications directly in the configuration file.

#### Report Properties

Under `reports`, define a list of scheduled report objects:

|    Property     |      Type       |                                       Description                                        |
| --------------- | --------------- | ---------------------------------------------------------------------------------------- |
| `id`            | String          | The unique report definition ID (e.g. `security-full-access-permlists`)                  |
| `enabled`       | Boolean         | Enables or disables the scheduled execution of this report (defaults to `true`)          |
| `dbNames`       | List of Strings | Database names to target (e.g., `["PROD"]`). If omitted, targets all active databases.   |
| `schedule`      | Object          | Timing recurrence properties (see below)                                                 |
| `emailTarget`   | String          | Optional email address to send report completion notifications to                        |
| `webhookTarget` | String          | Optional Slack, MS Teams, Google Chat, or JSON webhook endpoint to send notifications to |

#### Schedule Recurrence Properties

Under `reports.schedule`, define when the job should run:

|   Property   |  Type   |                                            Description                                            |
| ------------ | ------- | ------------------------------------------------------------------------------------------------- |
| `interval`   | String  | Recurrence frequency: `"daily"`, `"weekly"`, or `"monthly"`                                       |
| `timeOfDay`  | String  | 24-hour format execution time (`"HH:MM"`, e.g., `"02:00"`) in server local time                   |
| `dayOfWeek`  | Integer | Optional. Day of the week for weekly runs (`0` for Sunday, `1` for Monday, ..., `6` for Saturday) |
| `dayOfMonth` | Integer | Optional. Day of the month for monthly runs (`1` to `28`)                                         |

---

### JSON Schema Validation

To enable autocompletion, tooltips, and real-time schema validation within your IDE (such as VS Code), use the built-in JSON schema:

#### Option A: Live URL (Online)

If your psLens server is running locally (e.g., on port 8080), you can add the `$schema` parameter to the top of your JSON config file:

```json
{
  "$schema": "http://localhost:8080/static/config-schema.json",
  "server": {
    "port": 8080
  }
}
```

#### Option B: Local File Reference (Offline)

To configure your workspace offline, reference the schema file directly. In VS Code, add this to your `.vscode/settings.json`:

```json
{
  "json.schemas": [
    {
      "fileMatch": ["config.json"],
      "url": "./static/config-schema.json"
    }
  ],
  "yaml.schemas": {
    "./static/config-schema.json": ["config.yaml"]
  }
}
```

---

### Securing psLens

For production deployments, you should restrict access to the psLens interface.

#### Recommended Security Controls

1. **Enable Built-In Magic Link Auth**:
   Turn on `auth.enabled` and configure SMTP credentials and `authorizedUsers` to require code validation on login.
2. **Setup a Master Key (`PSLENS_MASTER_KEY`)**:
   Provide a 32-byte (64 hex characters) key in the `PSLENS_MASTER_KEY` environment variable. All database and SMTP passwords entered in the UI or configuration are then encrypted at rest using AES-256-GCM.
3. **Reverse Proxy / VPN**:
   Place psLens behind a reverse proxy (e.g., Cloudflare Access, oauth2-proxy, nginx, Tailscale) to delegate authentication to your company's Identity Provider (SAML/OIDC). When using an external SSO proxy, you can keep `auth.enabled` disabled and restrict the psLens binary to bind only on `127.0.0.1` or internal networks.

> **Warning:** Never expose psLens to the public internet without authentication. Enable built-in magic-link auth or place an authenticated reverse proxy in front of psLens. Unprotected instances expose read access to PeopleSoft metadata.

---

### Active Configuration and Hot-Reloading

psLens keeps its settings in two places:

1. **Active configuration** (embedded key-value store): Web interface edits save here and take effect immediately without server restarts. psLens records each change as a numbered revision under **Settings → Change History**. You can view or restore any revision.
2. **`config.yaml` on disk**: the bootstrap file that seeds the active configuration on first boot. Saved changes are also written back to it as a backup on the persistent storage volume.

#### Configuration Seeding

On initial boot, or when you set the environment variable `PSLENS_FORCE_SEED_CONFIG=true`, psLens seeds active configuration from `config.yaml`.

If the active configuration drifts from `config.yaml`, the **Settings → Advanced** page shows a "Settings differ from the config.yaml file on disk" notice. To resolve it:

- Click **Load settings from config.yaml** on the Advanced page. This overwrites active configuration with file contents and discards changes made in the UI.
- If the file path is writable, click **Overwrite config.yaml on disk** to write active configuration to disk. On ephemeral deployment models without persistent volumes, restarting or re-deploying the container discards local file changes.
- Alternatively, copy the Active Configuration YAML from the Advanced page into your `config.yaml` file to bring the disk file in sync.

---

### Secret Key Management (Encryption at Rest)

When you configure the `PSLENS_MASTER_KEY` environment variable with a 32-byte hex-encoded key, psLens encrypts database and SMTP passwords at rest. Without this master key, psLens saves passwords in plaintext.

#### Rotating the Master Key

To rotate the cryptographic key used for credential encryption:

1. Enter a new 32-byte hex key in the **Rotate Encryption Key** field.
2. Click **Rotate Master Key**. The server decrypts all stored credentials using the old key and re-encrypts them with the new key in NATS KV.
3. Update the `PSLENS_MASTER_KEY` environment variable in your deployment configuration (e.g., Fly.io secrets or `.env` file) to match the new key before restarting the container. If the container restarts with a mismatched key, it cannot decrypt the configuration.
