{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://paulsdashboard.com/schemas/policy.schema.json",
  "title": "Paul's Dashboard business policy",
  "description": "Read-only policy file for managed installs. Windows: %ProgramData%\\Paul's Dashboard\\policy.json. macOS: /Library/Application Support/Paul's Dashboard/policy.json. Linux: /etc/pauls-dashboard/policy.json. Make the folder writable by admins only. Read once at startup. A policy can turn features off and lock settings; it can never turn on anything that watches the user (analyticsEnabled, the Connections and Quick Ask widgets' keepHistory option, or a Clipboard widget in the first-run layout) and has no way to read the user's data. Entries that would do so are ignored with a warning; display options such as showHistory, showLog and showRecord are allowed. See docs/deployment-it.md.",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "$schema": { "type": "string" },
    "version": { "const": 1, "description": "Policy format version. Only 1 exists." },
    "organization": { "type": "string", "maxLength": 300, "description": "Shown in Settings > About: \"<organization> has installed a policy\"." },
    "contact": { "type": "string", "maxLength": 300, "description": "Who users should ask (email, URL or team name). Shown in Settings > About." },
    "message": { "type": "string", "maxLength": 300, "description": "Optional note shown in Settings > About." },

    "defaults": {
      "type": "object",
      "description": "Values applied once per machine user. The user can change them later unless the setting is also in \"locked\". Changing a default in the policy applies the new value once more.",
      "additionalProperties": false,
      "properties": {
        "preferences": { "$ref": "#/$defs/preferences" },
        "windowAppearance": { "$ref": "#/$defs/windowAppearance" },
        "notificationPrefs": { "$ref": "#/$defs/notificationPrefs" },
        "dockSettings": { "$ref": "#/$defs/dockSettings" },
        "globalHotkey": { "type": "string", "description": "Electron accelerator, e.g. \"CommandOrControl+Shift+D\". Empty string turns the global shortcut off." },
        "analyticsEnabled": { "const": false, "description": "Only false is accepted. A policy can never switch analytics on." }
      }
    },

    "locked": {
      "type": "array",
      "description": "Settings pinned to their value in \"defaults\" (or the built-in default when none is given). Locked settings are greyed out with a lock and can't be changed in the app or by an import. Use a group (\"theme\", \"preferences\", \"windowAppearance\", \"notificationPrefs\", \"dockSettings\") or one setting (\"preferences.accentColor\", \"globalHotkey\", \"analyticsEnabled\"). Locking analyticsEnabled always pins it off.",
      "items": {
        "type": "string",
        "pattern": "^(theme|preferences|windowAppearance|notificationPrefs|dockSettings|globalHotkey|analyticsEnabled|(preferences|windowAppearance|notificationPrefs|dockSettings)\\.[A-Za-z]+)$"
      },
      "uniqueItems": true
    },

    "widgetDefaults": {
      "type": "object",
      "description": "Default options per widget type, applied to widgets created after the policy is in place (and to the first-run layout). Option names are the widget's own option keys (see src/renderer/lib/widget-options.types.ts). Options that record, keep history or capture can't be set to true.",
      "propertyNames": { "$ref": "#/$defs/widgetType" },
      "additionalProperties": {
        "type": "object",
        "propertyNames": { "pattern": "^[A-Za-z][A-Za-z0-9_]{0,63}$" }
      }
    },

    "lockedWidgetOptions": {
      "type": "object",
      "description": "Per widget type, option names pinned to their value in widgetDefaults. Applies to existing widgets too.",
      "propertyNames": { "$ref": "#/$defs/widgetType" },
      "additionalProperties": {
        "type": "array",
        "items": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9_]{0,63}$" },
        "uniqueItems": true
      }
    },

    "hiddenWidgets": {
      "type": "array",
      "description": "Widget types removed from Add Widget. Copies already on a dashboard show a \"turned off by your organization\" card instead (their data is kept).",
      "items": { "$ref": "#/$defs/widgetType" },
      "uniqueItems": true
    },

    "disabledFeatures": {
      "type": "array",
      "description": "Features to switch off. workflows: no workflow runs, is created or changed (existing ones stop; they can still be deleted). integrations: no connection can be added or changed (existing ones keep working). runPrograms: App Launcher, Dev Servers, Docker start/stop/restart, Action Button webhooks, and opening programs or scripts from file widgets. aiCustomEndpoints: only the built-in AI providers (OpenAI, Anthropic, Google, xAI, OpenRouter) at their own addresses; no Custom, Ollama / LM Studio or changed base URL. Each of these four is refused in the app's main process, not only hidden. supportPrompts: hides the Support buttons in Settings > About and the one-time support reminder (the app already hides them whenever a policy is installed).",
      "items": {
        "enum": ["ai", "updateChecks", "feedback", "networkWidgets", "analytics", "clipboardHistory", "connectionHistory", "screenshots", "workflows", "integrations", "runPrograms", "aiCustomEndpoints", "supportPrompts"]
      },
      "uniqueItems": true
    },

    "backups": {
      "type": "object",
      "description": "Where a backup of the dashboard may go. Applies to Settings > General, the File menu and the command line (exit code 6 when refused). Keys left out keep their default.",
      "additionalProperties": false,
      "properties": {
        "allowExport": { "type": "boolean", "default": true, "description": "false: the dashboard can't be exported at all." },
        "allowImport": { "type": "boolean", "default": true, "description": "false: no backup can be imported (including --import and --import-replace)." },
        "requireEncryption": { "type": "boolean", "default": false, "description": "true: every export must be encrypted with a password (AES-256-GCM)." },
        "allowSecretsInExport": { "type": "boolean", "default": true, "description": "false: API keys, tokens, Secrets widget values, the lock password hash and clipboard history are never exported (--include-secrets is refused)." }
      }
    },

    "allowedHosts": {
      "type": "array",
      "description": "If set, the app may only contact these hosts: every other request it makes (from its windows, Electron's network stack and its main process) is refused. \"*.contoso.com\" covers contoso.com and every subdomain. An empty list blocks all internet access. Host names only; a pasted URL is read as its host. Programs the app starts (dev servers, launched apps, docker) and the operating system are not covered: use a firewall for those. See the network map in deployment-it.md.",
      "items": { "type": "string", "maxLength": 260, "pattern": "^(\\*\\.)?[A-Za-z0-9.-]+$|^\\[[0-9A-Fa-f:.]+\\]$" },
      "maxItems": 500,
      "uniqueItems": true
    },

    "initialLayout": {
      "type": "object",
      "description": "The dashboard a brand-new install starts with (only when the user has no screens yet, and only once). A Clipboard widget can't be placed (it records the clipboard as soon as it is on screen).",
      "additionalProperties": false,
      "required": ["screens"],
      "properties": {
        "screens": {
          "type": "array",
          "minItems": 1,
          "maxItems": 20,
          "items": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "name": { "type": "string", "maxLength": 80 },
              "background_color": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" },
              "widgets": {
                "type": "array",
                "maxItems": 100,
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": ["type"],
                  "properties": {
                    "type": { "$ref": "#/$defs/widgetType" },
                    "title": { "type": "string", "maxLength": 120 },
                    "x": { "type": "number", "minimum": 0 },
                    "y": { "type": "number", "minimum": 0 },
                    "width": { "type": "number", "minimum": 80 },
                    "height": { "type": "number", "minimum": 60 },
                    "options": { "type": "object" }
                  }
                }
              }
            }
          }
        }
      }
    }
  },

  "$defs": {
    "widgetType": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,63}$", "description": "Widget type key, e.g. \"clock\", \"weather\", \"todo\" (see src/renderer/lib/widget-registry.tsx)." },
    "fontSize": { "enum": ["small", "medium", "large", "xl", "xxl", "xxxl"] },
    "color": { "type": "string", "pattern": "^#[0-9a-fA-F]{6}$" },
    "preferences": {
      "type": "object",
      "description": "Global appearance (Appearance studio).",
      "additionalProperties": false,
      "properties": {
        "backgroundColor": { "$ref": "#/$defs/color" },
        "accentColor": { "$ref": "#/$defs/color" },
        "textColor": { "$ref": "#/$defs/color" },
        "hoverColor": { "$ref": "#/$defs/color" },
        "scrollbarColor": { "$ref": "#/$defs/color" },
        "shadowColor": { "$ref": "#/$defs/color" },
        "fontFamily": { "type": "string", "description": "A Google Fonts family name or \"system-ui\"." },
        "headingFontFamily": { "type": "string" },
        "monoFontFamily": { "type": "string" },
        "fontSize": { "$ref": "#/$defs/fontSize" },
        "titleFontSize": { "$ref": "#/$defs/fontSize" },
        "bodyFontSize": { "$ref": "#/$defs/fontSize" },
        "dataPointLabelSize": { "$ref": "#/$defs/fontSize" },
        "dataPointValueSize": { "$ref": "#/$defs/fontSize" },
        "calendarDateSize": { "$ref": "#/$defs/fontSize" },
        "fontWeight": { "enum": ["light", "normal", "medium", "semibold", "bold"] },
        "textCase": { "enum": ["normal", "uppercase", "lowercase"] },
        "titleTextCase": { "enum": ["normal", "uppercase", "lowercase"] },
        "opacity": { "type": "number", "minimum": 20, "maximum": 100 },
        "borderRadius": { "type": "number", "minimum": 0, "maximum": 24 },
        "borderWidth": { "enum": ["none", "thin", "medium", "thick"] },
        "boxShadow": { "enum": ["none", "subtle", "medium", "strong"] },
        "padding": { "enum": ["compact", "normal", "spacious"] },
        "defaultIcon": { "type": "string" },
        "fontZoom": { "type": "number", "minimum": 0.5, "maximum": 2 },
        "customLogo": { "type": "string", "description": "A data: URL of the logo image." },
        "allowOverlap": { "type": "boolean" },
        "snapToGrid": { "type": "boolean" },
        "gridSize": { "enum": [5, 10, 20, 40] },
        "widgetGap": { "enum": [8, 16, 24, 32], "description": "Space the Arrange actions leave between widgets, in px." },
        "showGridWhileDragging": { "type": "boolean" },
        "hideTitles": { "type": "boolean" },
        "hideFocusButton": { "type": "boolean", "description": "Leave the focus-mode button out of widget headers (the ... menu keeps it)." },
        "eyeCareMode": { "enum": ["off", "warm", "cool", "dim", "custom"], "description": "Eye care: a colour tone over the whole dashboard." },
        "eyeCareStrength": { "type": "number", "minimum": 0, "maximum": 100 },
        "eyeCareHue": { "type": "number", "minimum": 0, "maximum": 359, "description": "The hue of the custom tone." },
        "eyeCareSchedule": { "type": "boolean", "description": "Only show the tone between eyeCareFrom and eyeCareTo." },
        "eyeCareFrom": { "type": "string", "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$" },
        "eyeCareTo": { "type": "string", "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$" }
      }
    },
    "windowAppearance": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "transparent": { "type": "boolean" },
        "alwaysOnTop": { "type": "boolean" },
        "hideFromTaskbar": { "type": "boolean" },
        "altTabWhileOpen": { "type": "boolean", "description": "With hideFromTaskbar on: keep a taskbar button while the dashboard is open, so it stays in Alt+Tab on Windows." },
        "rememberWindow": { "type": "boolean" },
        "autoHideTopBar": { "type": "boolean" },
        "restWhenAway": { "type": "boolean", "description": "Widgets poll much less often while the screen is locked, the PC sleeps or nothing is touched for 15 minutes. Turn off for an untouched wall screen." },
        "dashboardName": { "type": "string", "maxLength": 40, "description": "Supporter extra: the name on the window titles and the tray. Only shown on a machine with a supporter code; locking it stops it being changed." }
      }
    },
    "notificationPrefs": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "osToasts": { "type": "boolean" },
        "inAppModal": { "enum": ["always", "whenVisible", "never"] },
        "sound": { "type": "boolean" },
        "volume": { "type": "number", "minimum": 0, "maximum": 100 },
        "defaultSound": { "type": "string" },
        "reminderSound": { "type": "string" },
        "birthdaySound": { "type": "string" },
        "speakAlerts": { "type": "boolean" },
        "persistence": { "enum": ["untilDismissed", "autoDismiss"] },
        "autoDismissSeconds": { "type": "number" },
        "birthdayNotifyTime": { "type": "string", "pattern": "^[0-2][0-9]:[0-5][0-9]$" },
        "paulmojiAlerts": { "type": "boolean" }
      }
    },
    "dockSettings": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "enabled": { "type": "boolean" },
        "mode": { "enum": ["edge", "float"] },
        "edge": { "enum": ["left", "right", "top", "bottom"] },
        "width": { "type": "number", "minimum": 240, "maximum": 4000, "description": "Panel size in px. The dock keeps it within half of the screen." },
        "hoverToExpand": { "type": "boolean" },
        "hoverAutoHide": { "type": "boolean", "description": "With hoverToExpand: the dock folds back to its tab shortly after the pointer leaves it." },
        "autoCollapse": { "type": "boolean" },
        "pin": { "enum": ["none", "open", "closed"] },
        "columns": { "enum": [1, 2] },
        "source": { "enum": ["dashboard", "custom"], "description": "\"dashboard\" mirrors a screen; \"custom\" shows only the widgets people pin to the dock." },
        "locked": { "type": "boolean", "description": "Stops resizing, reordering and unpinning from the dock itself." }
      }
    }
  }
}
