Ctrl K
ComunidadGuías…

config/config.schema.json

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Actual-sync Configuration",
  "description": "Configuration schema for Actual-sync service",
  "type": "object",
  "additionalProperties": false,
  "required": ["servers"],
  "$defs": {
    "messageTemplate": {
      "type": "string",
      "maxLength": 2000,
      "description": "A user-configurable notification message template (#257). Handlebars syntax: {{ variable }} plus the eq, default, upper and lower helpers, and the if/unless/each/with block helpers. Validated at startup against the consumer's documented variable list; an unknown name, an unregistered helper (lookup and log included), or literal markup Telegram would reject fails startup with the offending token and line. See docs/NOTIFICATIONS.md, section \"Message templates\"."
    },
    "scheduleAlertTemplates": {
      "type": "object",
      "additionalProperties": false,
      "description": "Overrides for one or more of the missing-payment alert's built-in messages (#258). Any key left out keeps the built-in English wording. See docs/SCHEDULE_ALERTS.md, section \"Templates\".",
      "properties": {
        "missing": { "$ref": "#/$defs/messageTemplate" },
        "wrongAmount": { "$ref": "#/$defs/messageTemplate" },
        "resolved": { "$ref": "#/$defs/messageTemplate" },
        "cannotCheck": { "$ref": "#/$defs/messageTemplate" },
        "ruleUnmatched": { "$ref": "#/$defs/messageTemplate" },
        "muteEnded": { "$ref": "#/$defs/messageTemplate" }
      }
    },
    "scheduleAlert": {
      "type": "object",
      "additionalProperties": false,
      "description": "One missing-payment alert rule (#258): watches a single schedule (schedule) or every schedule whose name starts with a prefix (schedulePrefix, expanding to one alert per match, all sharing this rule's id).",
      "oneOf": [
        { "required": ["schedule"], "not": { "required": ["schedulePrefix"] } },
        { "required": ["schedulePrefix"], "not": { "required": ["schedule"] } }
      ],
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^[a-z0-9-]{1,64}$",
          "description": "Stable alert id, used by the schedule_alerts ledger and the dashboard (#261/#262). Defaults to a slug of schedule/schedulePrefix. Must be unique per server (checked at startup, since a JSON Schema cannot enforce uniqueness across array items)."
        },
        "enabled": {
          "type": "boolean",
          "default": true,
          "description": "Set to false to switch this alert rule off in the config (#261). A rule disabled here cannot be switched on from the dashboard; it shows as Disabled in config."
        },
        "schedule": {
          "type": "string",
          "minLength": 1,
          "description": "Exact name of one Actual schedule to watch."
        },
        "schedulePrefix": {
          "type": "string",
          "minLength": 1,
          "description": "Watch every Actual schedule whose name starts with this prefix (e.g. several rentals sharing a naming convention), each as its own occurrence of this one rule."
        },
        "graceDays": {
          "type": "integer",
          "minimum": 0,
          "maximum": 60,
          "default": 3,
          "description": "Days after the occurrence date before it is considered overdue (the deadline is occurrence + graceDays)."
        },
        "earlyDays": {
          "type": "integer",
          "minimum": 0,
          "maximum": 31,
          "default": 2,
          "description": "Days before the occurrence date a matching transaction still counts as on time."
        },
        "period": {
          "type": "string",
          "enum": ["occurrence", "next"],
          "default": "occurrence",
          "description": "What the {{period}} template variable names: the occurrence's own month (occurrence), or the following month (next, for a payment due at month end for the month ahead)."
        },
        "amountTolerancePct": {
          "type": "number",
          "minimum": 0,
          "maximum": 100,
          "description": "How far a posted amount may differ from the schedule's expected amount and still count as on time, as a percentage. Defaults to the schedule's own amountOp in Actual: 0 for an exact amount, 7.5 for \"approximately\"."
        },
        "remindEveryDays": {
          "type": "integer",
          "minimum": 1,
          "maximum": 30,
          "description": "Repeat a missing alert every N days while it stays unresolved, instead of sending it once. Off (no repeat) by default."
        },
        "channels": {
          "type": "array",
          "minItems": 1,
          "uniqueItems": true,
          "items": {
            "type": "string",
            "enum": ["telegram", "email", "slack", "discord", "webhook", "ntfy"]
          },
          "description": "Notification channels this alert uses. Defaults to every channel currently configured and enabled."
        },
        "templates": { "$ref": "#/$defs/scheduleAlertTemplates" }
      }
    },
    "scheduleAlertsBlock": {
      "type": "object",
      "additionalProperties": false,
      "required": ["alerts"],
      "description": "Missing-payment alerts for this server (#258): compares Actual's schedules against posted transactions and notifies when an expected payment is late, missing, wrong-amount, or cannot be checked. See docs/SCHEDULE_ALERTS.md.",
      "properties": {
        "digest": {
          "type": "boolean",
          "default": false,
          "description": "Merge every alert event from one sync run for this server into a single message per channel, instead of one message per event."
        },
        "staleAfterDays": {
          "type": "integer",
          "minimum": 0,
          "maximum": 60,
          "default": 3,
          "description": "How many days old the account's last successful sync may be before a missing payment is reported as cannotCheck (bank connection likely broken) instead of missing. Strictly greater than this many days counts as stale."
        },
        "dateFormat": {
          "type": "string",
          "default": "D MMM YYYY",
          "description": "moment.js format string used for every date/time template variable."
        },
        "locale": {
          "type": "string",
          "default": "en",
          "description": "Locale tag used when rendering Actual's own cadence wording (the {{cadence}} variable)."
        },
        "templates": { "$ref": "#/$defs/scheduleAlertTemplates" },
        "alerts": {
          "type": "array",
          "description": "Config-defined rules. May be empty: the block then only switches the feature on for this server, and rules are added from the dashboard (#262).",
          "items": { "$ref": "#/$defs/scheduleAlert" }
        }
      }
    }
  },
  "properties": {
    "$schema": {
      "type": "string",
      "description": "Optional JSON Schema reference for editor autocomplete/validation. Ignored at runtime."
    },
    "timezone": {
      "type": "string",
      "minLength": 1,
      "description": "IANA timezone name (e.g. \"Europe/Madrid\", \"America/New_York\") used for cron schedules and for every time shown in logs and notifications. Defaults to the process timezone (TZ, else the system zone). Checked at config load.",
      "examples": ["Europe/Madrid", "America/New_York", "UTC"]
    },
    "servers": {
      "type": "array",
      "description": "Array of Actual Budget server configurations",
      "minItems": 1,
      "items": {
        "type": "object",
        "additionalProperties": false,
        "required": ["name", "url", "password", "syncId", "dataDir"],
        "properties": {
          "name": {
            "type": "string",
            "description": "Human-readable name for the server",
            "minLength": 1
          },
          "url": {
            "type": "string",
            "description": "Actual Budget server URL",
            "pattern": "^https?://.+",
            "examples": ["http://localhost:5006", "https://actual.example.com"]
          },
          "password": {
            "type": "string",
            "description": "Server password",
            "minLength": 1
          },
          "syncId": {
            "type": "string",
            "description": "Budget sync ID from Actual Budget settings",
            "minLength": 1
          },
          "dataDir": {
            "type": "string",
            "description": "Local directory for budget file cache",
            "minLength": 1
          },
          "encryptionPassword": {
            "type": "string",
            "description": "Budget file encryption password (optional, only needed if budget is encrypted with end-to-end encryption)",
            "minLength": 1
          },
          "sync": {
            "type": "object",
            "additionalProperties": false,
            "description": "Server-specific sync configuration (overrides global settings)",
            "properties": {
              "phaseTimeoutSeconds": {
                "type": "integer",
                "description": "Seconds each Actual API call (init, download, load, query, sync, shutdown) may take for this server before the sync fails (#272)",
                "minimum": 30,
                "maximum": 3600
              },
              "maxRetries": {
                "type": "integer",
                "description": "Maximum number of retry attempts for this server",
                "minimum": 0,
                "maximum": 10
              },
              "baseRetryDelayMs": {
                "type": "integer",
                "description": "Base delay in milliseconds for exponential backoff for this server",
                "minimum": 1000,
                "maximum": 10000
              },
              "schedule": {
                "type": "string",
                "description": "Cron expression for sync schedule for this server (5 fields, or 6 with a leading seconds field)",
                "pattern": "^[0-9A-Za-z*,/?#-]+(\\s+[0-9A-Za-z*,/?#-]+){4,5}$",
                "examples": ["0 2 * * *", "*/30 * * * *", "0 2 * * MON"]
              },
              "autoRetry": {
                "type": "object",
                "additionalProperties": false,
                "description": "Automatic retry for failed automated syncs for this server (overrides global; does NOT apply to manual syncs)",
                "properties": {
                  "enabled": {
                    "type": "boolean",
                    "description": "Enable automatic retry for failed automated syncs"
                  },
                  "maxAttempts": {
                    "type": "integer",
                    "description": "Maximum number of retry attempts after initial failure",
                    "minimum": 0,
                    "maximum": 5
                  },
                  "delayMinutes": {
                    "type": "integer",
                    "description": "Delay in minutes between retry attempts",
                    "minimum": 1,
                    "maximum": 1440
                  }
                }
              }
            }
          },
          "logging": {
            "type": "object",
            "additionalProperties": false,
            "description": "Server-specific logging configuration (overrides global settings)",
            "properties": {
              "level": {
                "type": "string",
                "description": "Log level for this server (DEBUG, INFO, WARN, ERROR)",
                "enum": ["DEBUG", "INFO", "WARN", "ERROR", "debug", "info", "warn", "error"]
              },
              "format": {
                "type": "string",
                "description": "Deprecated and ignored: the format is set per sink, not per server",
                "enum": ["pretty", "json"]
              }
            }
          },
          "scheduleAlerts": { "$ref": "#/$defs/scheduleAlertsBlock" }
        }
      }
    },
    "sync": {
      "type": "object",
      "additionalProperties": false,
      "description": "Global sync behavior configuration (can be overridden per server)",
      "properties": {
        "phaseTimeoutSeconds": {
          "type": "integer",
          "description": "Seconds each Actual API call (init, download, load, query, sync, shutdown) may take before the sync fails and the queue moves on (#272)",
          "minimum": 30,
          "maximum": 3600,
          "default": 300
        },
        "maxRetries": {
          "type": "integer",
          "description": "Maximum number of retry attempts",
          "minimum": 0,
          "maximum": 10,
          "default": 5
        },
        "baseRetryDelayMs": {
          "type": "integer",
          "description": "Base delay in milliseconds for exponential backoff",
          "minimum": 1000,
          "maximum": 10000,
          "default": 3000
        },
        "schedule": {
          "type": "string",
          "description": "Cron expression for sync schedule (5 fields, or 6 with a leading seconds field)",
          "pattern": "^[0-9A-Za-z*,/?#-]+(\\s+[0-9A-Za-z*,/?#-]+){4,5}$",
          "default": "03 03 */2 * *",
          "examples": ["0 2 * * *", "*/30 * * * *", "0 2 * * MON"]
        },
        "autoRetry": {
          "type": "object",
          "additionalProperties": false,
          "description": "Automatic retry configuration for failed automated syncs (does NOT apply to manual syncs)",
          "properties": {
            "enabled": {
              "type": "boolean",
              "description": "Enable automatic retry for failed automated syncs",
              "default": true
            },
            "maxAttempts": {
              "type": "integer",
              "description": "Maximum number of retry attempts after initial failure",
              "minimum": 0,
              "maximum": 5,
              "default": 1
            },
            "delayMinutes": {
              "type": "integer",
              "description": "Delay in minutes between retry attempts",
              "minimum": 1,
              "maximum": 1440,
              "default": 31
            }
          }
        }
      }
    },
    "logging": {
      "type": "object",
      "additionalProperties": false,
      "description": "Logging configuration",
      "properties": {
        "level": {
          "type": "string",
          "description": "Global log level (DEBUG, INFO, WARN, ERROR)",
          "enum": ["DEBUG", "INFO", "WARN", "ERROR", "debug", "info", "warn", "error"],
          "default": "INFO"
        },
        "format": {
          "type": "string",
          "description": "Console log output format",
          "enum": ["pretty", "json"],
          "default": "pretty"
        },
        "fileFormat": {
          "type": "string",
          "description": "Log file output format. JSON (single line per entry) is recommended for production so logs are machine-parseable and shippable. Independent of the console 'format'. Global only: log files are a single shared stream, so this is not a per-server setting.",
          "enum": ["pretty", "json"],
          "default": "json"
        },
        "redact": {
          "type": "array",
          "description": "Additional metadata key names to mask as [REDACTED] in all log output, on top of the always-on defaults. The defaults are matched as case-insensitive substrings: password, passwd, token, secret, apikey, api_key, authorization, credential, chatid (so encryptionPassword, botToken, etc. are covered).",
          "items": { "type": "string", "minLength": 1 }
        },
        "actualApiVerbose": {
          "type": "boolean",
          "description": "UNSAFE. When true, the @actual-app/api library prints its own verbose output, which includes transactions (payees, amounts, notes), to the console. Default false: Actual-sync passes verbose:false to api.init. Global only.",
          "default": false
        },
        "includeAccountNames": {
          "type": "boolean",
          "description": "When true, Actual account names appear in the logs. Default false: logs carry the account id only, and the name is omitted. Server names you choose are always logged. Notifications are not logs and keep account names. Global only.",
          "default": false
        },
        "logDir": {
          "type": ["string", "null"],
          "description": "Directory for log files (set to null to disable file logging). For Docker deployments, use /app/logs. Defaults to './logs' if not specified.",
          "default": "./logs"
        },
        "rotation": {
          "type": "object",
          "additionalProperties": false,
          "description": "Log rotation settings (enabled by default with 30-day retention)",
          "properties": {
            "enabled": {
              "type": "boolean",
              "description": "Enable log rotation (recommended for production)",
              "default": true
            },
            "maxSize": {
              "type": "string",
              "description": "Maximum size per log file before rotation: a positive integer plus a unit B/K/M/G, optionally suffixed with B (e.g., '10M', '10MB', '512B', '1GB'). Rotation fires on whichever trigger comes first: this size or the interval.",
              "pattern": "^[1-9][0-9]*(B|[KMG]B?)$",
              "default": "10M"
            },
            "interval": {
              "type": "string",
              "description": "Time-based rotation interval, as a positive integer plus a unit: s/m/h/d or M for months (e.g., '1d', '12h', '1M'). Note m = minutes, M = months. Ensures logs rotate even at low volume, so maxFiles behaves as documented (days of retention).",
              "pattern": "^[1-9][0-9]*[smhdM]$",
              "default": "1d"
            },
            "maxFiles": {
              "type": "integer",
              "description": "Maximum number of rotated log files to keep. With the default '1d' interval this is days of retention.",
              "minimum": 1,
              "maximum": 1000,
              "default": 30
            },
            "compress": {
              "type": "string",
              "description": "Compression format for archived logs (gzip saves ~70% disk space)",
              "enum": ["gzip", "none"],
              "default": "gzip"
            }
          }
        },
        "syslog": {
          "type": "object",
          "additionalProperties": false,
          "description": "Syslog centralized logging configuration",
          "properties": {
            "enabled": {
              "type": "boolean",
              "description": "Enable syslog output",
              "default": false
            },
            "host": {
              "type": "string",
              "description": "Syslog server hostname",
              "minLength": 1,
              "default": "localhost"
            },
            "port": {
              "type": "integer",
              "description": "Syslog server port",
              "minimum": 1,
              "maximum": 65535,
              "default": 514
            },
            "protocol": {
              "type": "string",
              "description": "Transport: udp (plaintext, not an audit log), tcp (plaintext, RFC 6587 framing) or tls (RFC 5425, certificate verified)",
              "enum": ["udp", "tcp", "tls"],
              "default": "udp"
            },
            "ca": {
              "type": "string",
              "description": "Path to a PEM CA certificate to trust for protocol tls (added to the default trust store; verification is never disabled)"
            },
            "facility": {
              "type": "integer",
              "description": "Syslog facility code (0-23)",
              "minimum": 0,
              "maximum": 23,
              "default": 16
            }
          }
        },
        "performance": {
          "type": "object",
          "additionalProperties": false,
          "description": "Performance logging settings",
          "properties": {
            "enabled": {
              "type": "boolean",
              "description": "Enable performance metrics logging",
              "default": false
            },
            "thresholds": {
              "type": "object",
              "additionalProperties": false,
              "description": "Performance thresholds in milliseconds",
              "properties": {
                "slow": {
                  "type": "integer",
                  "description": "Threshold for slow operations (ms)",
                  "minimum": 100,
                  "maximum": 3600000,
                  "default": 1000
                },
                "verySlow": {
                  "type": "integer",
                  "description": "Threshold for very slow operations (ms)",
                  "minimum": 100,
                  "maximum": 3600000,
                  "default": 5000
                }
              }
            }
          }
        }
      }
    },
    "healthCheck": {
      "type": "object",
      "additionalProperties": false,
      "description": "Health check HTTP endpoint configuration",
      "properties": {
        "port": {
          "type": "integer",
          "description": "HTTP port for health check endpoints",
          "minimum": 1024,
          "maximum": 65535,
          "default": 3000
        },
        "host": {
          "type": "string",
          "description": "Host to bind health check server",
          "minLength": 1,
          "default": "0.0.0.0",
          "examples": ["0.0.0.0", "127.0.0.1", "localhost"]
        },
        "trustProxy": {
          "description": "Trust X-Forwarded-For from a reverse proxy you control, so rate limiting and auth-failure logs use the real client IP. false (default) for direct exposure; a hop count (1 = one proxy in front), or an IP/CIDR string or array. true trusts every hop and lets any client spoof its IP if the service is reachable directly.",
          "default": false,
          "oneOf": [
            { "type": "boolean" },
            { "type": "integer", "minimum": 0, "maximum": 10 },
            { "type": "string", "minLength": 1 },
            { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } }
          ],
          "examples": [false, 1, "172.18.0.2", ["loopback", "172.18.0.2"]]
        },
        "hsts": {
          "type": "boolean",
          "description": "Send the Strict-Transport-Security header (max-age 180 days, no includeSubDomains). Off by default because the service is plain HTTP in most deployments; enable it only when browsers reach it over HTTPS (normally TLS and HSTS are the reverse proxy's job).",
          "default": false
        },
        "auth": {
          "type": "object",
          "additionalProperties": false,
          "description": "Credential for GET /metrics and GET /metrics/prometheus (#247), separate from dashboard.auth so rotating one never breaks the other. Off by default. /health and /ready are never authenticated: they are liveness and readiness probes. With type basic or token the credential must be set, or startup fails.",
          "properties": {
            "type": {
              "type": "string",
              "description": "Authentication type for the metrics endpoints",
              "enum": ["none", "basic", "token"],
              "default": "none"
            },
            "username": {
              "type": "string",
              "description": "Username for basic authentication (required if type=basic)"
            },
            "password": {
              "type": "string",
              "description": "Password for basic authentication (required if type=basic)"
            },
            "token": {
              "type": "string",
              "description": "Bearer token for token authentication (required if type=token). Prefer this over basic: a scrape credential should be a long random value, not a human password."
            }
          },
          "allOf": [
            {
              "if": { "required": ["type"], "properties": { "type": { "const": "basic" } } },
              "then": {
                "required": ["username", "password"],
                "properties": {
                  "username": { "type": "string", "minLength": 1 },
                  "password": { "type": "string", "minLength": 1 }
                }
              }
            },
            {
              "if": { "required": ["type"], "properties": { "type": { "const": "token" } } },
              "then": {
                "required": ["token"],
                "properties": { "token": { "type": "string", "minLength": 1 } }
              }
            }
          ]
        },
        "dashboard": {
          "type": "object",
          "additionalProperties": false,
          "description": "Dashboard authentication and access control",
          "properties": {
            "enabled": {
              "type": "boolean",
              "description": "Enable web dashboard access",
              "default": true
            },
            "allowedOrigins": {
              "type": "array",
              "description": "Extra browser origins (scheme://host[:port]) allowed to open the /ws/logs live log stream and to submit state-changing requests (POST and other writes) to the dashboard API. Same-origin is always allowed. Needed only when the dashboard is served under a different origin than the one the browser connects to, e.g. behind some reverse proxies. Review this list before enabling allowConfigWrites.",
              "items": { "type": "string", "pattern": "^https?://[^/\\s]+$" },
              "default": []
            },
            "allowConfigWrites": {
              "type": "boolean",
              "description": "Allow the dashboard to change configuration (alert settings). Off by default; also refused while auth.type is none.",
              "default": false
            },
            "auth": {
              "type": "object",
              "additionalProperties": false,
              "description": "Authentication configuration for dashboard",
              "properties": {
                "type": {
                  "type": "string",
                  "description": "Authentication type",
                  "enum": ["none", "basic", "token"],
                  "default": "none"
                },
                "username": {
                  "type": "string",
                  "description": "Username for basic authentication (required if type=basic)"
                },
                "password": {
                  "type": "string",
                  "description": "Password for basic authentication (required if type=basic)"
                },
                "token": {
                  "type": "string",
                  "description": "Bearer token for token authentication (required if type=token)"
                }
              },
              "allOf": [
                {
                  "if": { "required": ["type"], "properties": { "type": { "const": "basic" } } },
                  "then": {
                    "required": ["username", "password"],
                    "properties": {
                      "username": { "type": "string", "minLength": 1 },
                      "password": { "type": "string", "minLength": 1 }
                    }
                  }
                },
                {
                  "if": { "required": ["type"], "properties": { "type": { "const": "token" } } },
                  "then": {
                    "required": ["token"],
                    "properties": { "token": { "type": "string", "minLength": 1 } }
                  }
                }
              ]
            }
          }
        }
      }
    },
    "syncHistory": {
      "type": "object",
      "additionalProperties": false,
      "description": "Sync history persistence configuration",
      "properties": {
        "dbPath": {
          "type": "string",
          "description": "Path to SQLite database file for sync history",
          "minLength": 1,
          "default": "data/sync-history.db"
        },
        "retentionDays": {
          "type": "integer",
          "description": "Number of days to retain sync history",
          "minimum": 1,
          "maximum": 365,
          "default": 90
        }
      }
    },
    "prometheus": {
      "type": "object",
      "additionalProperties": false,
      "description": "Prometheus metrics export configuration",
      "properties": {
        "enabled": {
          "type": "boolean",
          "description": "Enable Prometheus metrics endpoint",
          "default": true
        },
        "includeDefaultMetrics": {
          "type": "boolean",
          "description": "Include Node.js default metrics (memory, CPU, etc.)",
          "default": true
        }
      }
    },
    "notifications": {
      "type": "object",
      "additionalProperties": false,
      "description": "Error notification configuration",
      "properties": {
        "notifyOnSuccess": {
          "type": "string",
          "description": "Global default for when to send sync notifications, overridable per channel. 'always' sends on every result; 'errors_only' sends on failure and partial but not success; 'activity_only' sends on failure and partial, and on success only when the sync imported or updated at least one transaction; 'never' turns the channel off entirely (failures included). Test notifications always send.",
          "enum": ["always", "errors_only", "activity_only", "never"],
          "default": "always"
        },
        "email": {
          "type": "object",
          "additionalProperties": false,
          "description": "Email notification settings",
          "properties": {
            "notifyOnSuccess": {
              "type": "string",
              "description": "When to send sync notifications by email. Overrides notifications.notifyOnSuccess.",
              "enum": ["always", "errors_only", "activity_only", "never"]
            },
            "enabled": {
              "type": "boolean",
              "description": "Enable email notifications",
              "default": false
            },
            "host": {
              "type": "string",
              "description": "SMTP server host",
              "minLength": 1,
              "default": "smtp.gmail.com",
              "examples": ["smtp.gmail.com", "smtp.sendgrid.net"]
            },
            "port": {
              "type": "integer",
              "description": "SMTP server port",
              "minimum": 1,
              "maximum": 65535,
              "default": 587
            },
            "secure": {
              "type": "boolean",
              "description": "Use TLS for SMTP connection",
              "default": false
            },
            "auth": {
              "type": "object",
              "additionalProperties": false,
              "description": "SMTP authentication credentials",
              "properties": {
                "user": {
                  "type": "string",
                  "description": "SMTP username"
                },
                "pass": {
                  "type": "string",
                  "description": "SMTP password or app-specific password"
                }
              }
            },
            "from": {
              "type": "string",
              "description": "From email address. Validated as an email only when email notifications are enabled, so a disabled stub may hold a placeholder."
            },
            "to": {
              "type": "array",
              "description": "Recipient email addresses (validated as emails only when email is enabled)",
              "items": {
                "type": "string",
                "minLength": 1
              }
            }
          },
          "if": {
            "required": ["enabled"],
            "properties": { "enabled": { "const": true } }
          },
          "then": {
            "required": ["from", "to"],
            "properties": {
              "from": { "type": "string", "format": "email" },
              "to": { "type": "array", "minItems": 1, "items": { "type": "string", "format": "email" } }
            }
          }
        },
        "telegram": {
          "type": "object",
          "additionalProperties": false,
          "description": "Telegram bot settings for notifications and commands",
          "properties": {
            "enabled": {
              "type": "boolean",
              "description": "Enable Telegram bot",
              "default": false
            },
            "botToken": {
              "type": "string",
              "description": "Telegram bot token from @BotFather. The 'digits:token' format is enforced only when telegram is enabled, so a disabled stub may hold a placeholder."
            },
            "chatId": {
              "type": "string",
              "description": "Chat ID (user ID, group ID, or channel username)"
            },
            "chatIds": {
              "type": "array",
              "description": "One or more chat IDs (alternative to chatId). The first entry is used for outbound notifications; the bot also accepts numeric IDs.",
              "items": { "type": ["string", "number"], "minLength": 1 }
            },
            "notifyOnSuccess": {
              "type": "string",
              "description": "When to send sync notifications to Telegram. Overrides notifications.notifyOnSuccess. Also settable at runtime with the /notify bot command.",
              "enum": ["always", "errors_only", "activity_only", "never"]
            },
            "pollInterval": {
              "type": "integer",
              "description": "Polling interval in milliseconds for the interactive bot",
              "minimum": 100,
              "maximum": 3600000,
              "default": 2000
            }
          },
          "if": {
            "required": ["enabled"],
            "properties": { "enabled": { "const": true } }
          },
          "then": {
            "required": ["botToken"],
            "properties": {
              "botToken": { "type": "string", "pattern": "^[0-9]+:[A-Za-z0-9_-]+$" }
            },
            "anyOf": [
              { "required": ["chatId"], "properties": { "chatId": { "type": "string", "minLength": 1 } } },
              { "required": ["chatIds"], "properties": { "chatIds": { "type": "array", "minItems": 1 } } }
            ]
          }
        },
        "branding": {
          "type": "boolean",
          "description": "Stamp the Actual-sync project name and icon onto Slack/Discord/ntfy notifications. Set false to keep a webhook's own configured app identity, or to stop ntfy from fetching the project icon from GitHub on an air-gapped network.",
          "default": true
        },
        "webhooks": {
          "type": "object",
          "additionalProperties": false,
          "description": "Webhook notification settings",
          "properties": {
            "telegram": {
              "type": "array",
              "description": "Legacy per-webhook Telegram bot configs (an alternative to the top-level notifications.telegram). Each entry posts to one bot + chat.",
              "items": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "name": { "type": "string", "description": "Webhook name for identification" },
                  "botToken": { "type": "string", "description": "Telegram bot token from @BotFather", "minLength": 1 },
                  "chatId": { "type": "string", "description": "Target chat ID", "minLength": 1 }
                },
                "required": ["botToken", "chatId"]
              }
            },
            "slack": {
              "type": "array",
              "description": "Slack webhook configurations",
              "items": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Webhook name for identification"
                  },
                  "url": {
                    "type": "string",
                    "description": "Slack webhook URL",
                    "pattern": "^https://hooks\\.slack\\.com/.*"
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Set false to disable this webhook without removing it",
                    "default": true
                  },
                  "notifyOnSuccess": {
                    "type": "string",
                    "description": "When to send sync notifications to this webhook. Overrides notifications.notifyOnSuccess.",
                    "enum": ["always", "errors_only", "activity_only", "never"]
                  }
                },
                "required": ["url"]
              }
            },
            "discord": {
              "type": "array",
              "description": "Discord webhook configurations",
              "items": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Webhook name for identification"
                  },
                  "url": {
                    "type": "string",
                    "description": "Discord webhook URL",
                    "pattern": "^https://discord\\.com/api/webhooks/.*"
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Set false to disable this webhook without removing it",
                    "default": true
                  },
                  "notifyOnSuccess": {
                    "type": "string",
                    "description": "When to send sync notifications to this webhook. Overrides notifications.notifyOnSuccess.",
                    "enum": ["always", "errors_only", "activity_only", "never"]
                  }
                },
                "required": ["url"]
              }
            },
            "generic": {
              "type": "array",
              "description": "Generic webhook configurations. Each receives a documented JSON payload (status, server, account counts and lists) via HTTP POST. Works with ntfy (JSON publishing), Gotify, Home Assistant, n8n, and custom endpoints.",
              "items": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Webhook name for identification"
                  },
                  "url": {
                    "type": "string",
                    "description": "Destination URL the JSON payload is POSTed to",
                    "minLength": 1,
                    "pattern": "^https?://.+"
                  },
                  "notifyOnSuccess": {
                    "type": "string",
                    "description": "When to send sync notifications to this webhook. Overrides notifications.notifyOnSuccess.",
                    "enum": ["always", "errors_only", "activity_only", "never"]
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Set false to disable this webhook without removing it",
                    "default": true
                  },
                  "headers": {
                    "type": "object",
                    "description": "Extra HTTP headers to send (e.g. Authorization, Priority). Secrets are redacted in logs.",
                    "additionalProperties": { "type": "string" }
                  },
                  "contentType": {
                    "type": "string",
                    "description": "Override the request Content-Type",
                    "default": "application/json"
                  }
                },
                "required": ["url"]
              }
            }
          }
        },
        "ntfy": {
          "type": "object",
          "additionalProperties": false,
          "description": "ntfy push notifications (https://ntfy.sh). Posts to a topic URL with Title/Priority/Tags headers.",
          "properties": {
            "notifyOnSuccess": {
              "type": "string",
              "description": "When to send sync notifications to ntfy. Overrides notifications.notifyOnSuccess.",
              "enum": ["always", "errors_only", "activity_only", "never"]
            },
            "enabled": {
              "type": "boolean",
              "description": "Enable ntfy notifications",
              "default": false
            },
            "url": {
              "type": "string",
              "description": "Full topic URL, e.g. https://ntfy.sh/my-topic or https://ntfy.example.com/my-topic"
            },
            "token": {
              "type": "string",
              "description": "Optional access token sent as 'Authorization: Bearer <token>'. Redacted in logs."
            },
            "priorityOnFailure": {
              "type": "string",
              "description": "ntfy priority for failure notifications",
              "enum": ["max", "urgent", "high", "default", "low", "min"],
              "default": "high"
            },
            "priorityOnSuccess": {
              "type": "string",
              "description": "ntfy priority for success/info notifications",
              "enum": ["max", "urgent", "high", "default", "low", "min"],
              "default": "default"
            },
            "tags": {
              "type": "array",
              "description": "Base ntfy tags/emoji added to every notification",
              "items": { "type": "string", "minLength": 1 }
            },
            "icon": {
              "type": "string",
              "anyOf": [
                { "const": "" },
                { "format": "uri" }
              ],
              "description": "URL of an icon shown in the ntfy notification (sent as the Icon header). Defaults to the Actual-sync project icon; set to an empty string to send no icon."
            }
          },
          "if": { "required": ["enabled"], "properties": { "enabled": { "const": true } } },
          "then": { "required": ["url"], "properties": { "url": { "type": "string", "minLength": 1 } } }
        },
        "thresholds": {
          "type": "object",
          "additionalProperties": false,
          "description": "Notification threshold settings",
          "properties": {
            "consecutiveFailures": {
              "type": "integer",
              "description": "Number of consecutive failures before notification",
              "minimum": 1,
              "maximum": 20,
              "default": 3
            },
            "failureRate": {
              "type": "number",
              "description": "Failure rate (0-1) threshold over rate period",
              "minimum": 0,
              "maximum": 1,
              "default": 0.5
            },
            "ratePeriodMinutes": {
              "type": "integer",
              "description": "Time period in minutes for failure rate calculation",
              "minimum": 5,
              "maximum": 1440,
              "default": 60
            }
          }
        },
        "rateLimit": {
          "type": "object",
          "additionalProperties": false,
          "description": "Notification rate limiting settings",
          "properties": {
            "minIntervalMinutes": {
              "type": "integer",
              "description": "Minimum minutes between notifications",
              "minimum": 1,
              "maximum": 1440,
              "default": 15
            },
            "maxPerHour": {
              "type": "integer",
              "description": "Maximum notifications per hour",
              "minimum": 1,
              "maximum": 20,
              "default": 4
            }
          }
        }
      }
    }
  }
}