{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "https://tikoci.github.io/restraml/routeros-app-yaml-schema.editor.json",
  "title": "RouterOS /app YAML",
  "description": "MikroTik RouterOS /app container application definition (7.22+). Editor-friendly variant: relaxed port validation (no regex), case-insensitive env var names. Use routeros-app-yaml-schema.latest.json for strict CI validation.",
  "$comment": "SchemaStore submission target. Differences from .latest.json: (1) ports items have no pattern (see per-item $comment for strict regex); (2) environment patternProperties allows lowercase names.",
  "type": "object",
  "required": ["services"],
  "additionalProperties": false,
  "properties": {
    "name": {
      "type": "string",
      "description": "Unique /app identifier"
    },
    "descr": {
      "type": "string",
      "description": "Human-readable description shown in the app UI"
    },
    "page": {
      "type": "string",
      "format": "uri",
      "description": "Project homepage URL"
    },
    "category": {
      "type": "string",
      "enum": [
        "productivity",
        "storage",
        "networking",
        "development",
        "communication",
        "file-management",
        "search",
        "video",
        "media",
        "media-management",
        "home-automation",
        "monitoring",
        "database",
        "automation",
        "ai",
        "messaging",
        "radio",
        "security",
        "business"
      ],
      "description": "App store category. Validated against RouterOS-accepted values; build CI raises an issue when new categories appear."
    },
    "icon": {
      "type": "string",
      "format": "uri",
      "description": "Icon image URL. With `show-in-webfig=yes`, displayed on the WebFig login screen."
    },
    "default-credentials": {
      "type": ["string", "null"],
      "description": "Credentials shown in the app UI. Format: `username:password`. Use `null` for no credentials.",
      "examples": ["admin:admin", "admin:", null]
    },
    "url-path": {
      "type": "string",
      "description": "URL path suffix appended to the access URL for browser access",
      "examples": ["/", "/ui", "/admin"]
    },
    "credentials": {
      "type": "string",
      "description": "Credential hint displayed in the app UI (alternative to default-credentials)"
    },
    "option": {
      "type": "boolean",
      "description": "Whether this /app is optional"
    },
    "auto-update": {
      "type": "boolean",
      "description": "Pull and restart all service containers on every router boot"
    },
    "services": {
      "type": "object",
      "description": "Container services. At least one entry required.",
      "minProperties": 1,
      "patternProperties": {
        "^[a-zA-Z0-9_-]+$": {
          "type": "object",
          "additionalProperties": false,
          "required": ["image"],
          "properties": {
            "image": {
              "type": "string",
              "description": "Container image reference. Omit registry to use `registry-url` from `/container/config`.",
              "examples": ["nginx:alpine", "ghcr.io/owner/app:latest"]
            },
            "container_name": {
              "type": "string",
              "description": "Explicit container name; also used as the base for file paths under `/container`"
            },
            "hostname": {
              "type": "string",
              "description": "Container hostname"
            },
            "entrypoint": {
              "oneOf": [
                {"type": "string"},
                {"type": "array", "items": {"type": "string"}}
              ],
              "description": "Override the default container entrypoint (string or list)",
              "examples": ["/bin/bash", ["/bin/sh", "-c"]]
            },
            "command": {
              "oneOf": [
                {"type": "string"},
                {"type": "array", "items": {"type": "string"}}
              ],
              "description": "Override the default container command (string or list)"
            },
            "ports": {
              "type": "array",
              "description": "Port mappings",
              "items": {
                "oneOf": [
                  {
                    "type": "string",
                    "description": "Two formats supported: (1) `[ip:]host:container[/tcp|/udp][:label]` (OCI-style, pre-7.23) or (2) `[ip:]host:container[:label][:tcp|:udp]` (RouterOS 7.23+, protocol after label). RouterOS placeholders `[accessIP]`, `[accessPort]`, `[accessPort2]`, `[containerIP]`, `[routerIP]` expand at deploy time. The `:label` suffix (e.g. `web`) is RouterOS-specific — `web` marks this port for browser forwarding and appears in the admin URL shown in Winbox/WebFig.",
                    "$comment": "Two strict patterns used in .latest.json (anyOf): (1) old OCI-style: ^(ip:)?host:container(/tcp|/udp)?(:label)?$ and (2) new RouterOS 7.23+ style: ^(ip:)?host:container(:label)?(:tcp|:udp)?$ where label may not contain / or :. Mixing both protocol styles in one entry is rejected.",
                    "examples": [
                      "80:80",
                      "80:80:web",
                      "8080:8080/tcp:rest api",
                      "8080:8080:web:tcp",
                      "[accessIP]:[accessPort]:80:web",
                      "127.0.0.1:9000:9000"
                    ]
                  },
                  {
                    "type": "object",
                    "$comment": "Long syntax port mapping with named fields",
                    "required": ["target", "published"],
                    "properties": {
                      "name": {
                        "type": "string",
                        "description": "Port mapping label"
                      },
                      "target": {
                        "type": "integer",
                        "description": "Container port"
                      },
                      "published": {
                        "type": "integer",
                        "description": "Host port"
                      },
                      "protocol": {
                        "type": "string",
                        "enum": ["tcp", "udp"],
                        "description": "Protocol (tcp or udp)"
                      },
                      "app_protocol": {
                        "type": "string",
                        "description": "Application protocol hint"
                      }
                    },
                    "additionalProperties": false
                  }
                ]
              }
            },
            "environment": {
              "oneOf": [
                {
                  "type": "object",
                  "description": "Map of env var name → value. Keys: letters, digits, underscore (case-sensitive).",
                  "patternProperties": {
                    "^[a-zA-Z_][a-zA-Z0-9_]*$": {
                      "oneOf": [
                        {"type": "string"},
                        {"type": "number"},
                        {"type": "boolean"},
                        {"type": "null"}
                      ]
                    }
                  }
                },
                {
                  "type": "array",
                  "items": {
                    "type": "string",
                    "description": "`KEY=value` string"
                  }
                },
                {"type": "null"}
              ],
              "description": "Service environment variables (object, KEY=value array, or null)"
            },
            "volumes": {
              "type": "array",
              "description": "Volume mounts",
              "items": {
                "type": "string",
                "description": "Format: `source:target[:options]`",
                "examples": ["mydata:/data", "/host/path:/container/path:ro"]
              }
            },
            "configs": {
              "type": "array",
              "description": "Config file mounts. Each `source` must match a top-level `configs` key.",
              "items": {
                "type": "object",
                "required": ["source", "target"],
                "additionalProperties": false,
                "properties": {
                  "source": {
                    "type": "string",
                    "description": "Top-level `configs` key name"
                  },
                  "target": {
                    "type": "string",
                    "description": "Absolute path inside the container"
                  },
                  "mode": {
                    "type": ["integer", "string"],
                    "description": "File permissions, octal (e.g. `0644`)"
                  }
                }
              }
            },
            "restart": {
              "type": "string",
              "enum": ["no", "always", "on-failure", "unless-stopped"],
              "description": "Container restart policy"
            },
            "depends_on": {
              "oneOf": [
                {"type": "array", "items": {"type": "string"}},
                {"type": "object"}
              ],
              "description": "Start after these named services"
            },
            "devices": {
              "type": "array",
              "description": "Host device mappings passed into the container (e.g. for USB/serial hardware access)",
              "items": {
                "type": "string",
                "description": "Format: `host-device[:container-device]` (e.g. `/dev/ttyACM0:/dev/ttyACM0`)"
              }
            },
            "user": {
              "type": "string",
              "description": "Run container as this user (name or `uid:gid`)"
            },
            "security_opt": {
              "type": "array",
              "items": {"type": "string"},
              "description": "Security options (e.g. `no-new-privileges:true`)"
            },
            "shm_size": {
              "type": "string",
              "description": "Shared memory size (e.g. `128m`, `1g`)"
            },
            "stop_grace_period": {
              "type": ["string", "integer"],
              "description": "Wait this long before sending SIGKILL (e.g. `10s`, `1m30s`)"
            },
            "ulimits": {
              "type": "object",
              "description": "Resource limits (e.g. nofile soft/hard)",
              "patternProperties": {
                "^[a-zA-Z0-9_-]+$": {
                  "oneOf": [
                    {
                      "type": "object",
                      "required": ["soft", "hard"],
                      "additionalProperties": false,
                      "properties": {
                        "soft": {"type": "integer"},
                        "hard": {"type": "integer"}
                      }
                    },
                    {"type": "integer"}
                  ]
                }
              }
            },
            "build": {
              "oneOf": [
                {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "context": {"type": "string"},
                    "dockerfile": {"type": "string"},
                    "args": {"type": "object"}
                  }
                },
                {"type": "string"}
              ],
              "description": "Build configuration for the container image"
            },
            "healthcheck": {
              "type": "object",
              "description": "Container health check configuration",
              "additionalProperties": false,
              "properties": {
                "test": {
                  "oneOf": [
                    {"type": "array", "items": {"type": "string"}},
                    {"type": "string"}
                  ]
                },
                "interval": {"type": "string"},
                "timeout": {"type": "string"},
                "retries": {"type": "integer"},
                "start_period": {"type": "string"}
              }
            },
            "stdin_open": {
              "type": "boolean",
              "description": "Keep stdin open even if not attached"
            },
            "expose": {
              "type": "array",
              "description": "Expose ports without publishing them to the host",
              "items": {"type": ["string", "integer"]}
            },
            "secrets": {
              "type": "array",
              "description": "Secrets to expose to the service",
              "items": {"type": "string"}
            },
            "attach": {
              "type": "boolean",
              "description": "Whether to attach to the container's stdio"
            }
          }
        }
      }
    },
    "volumes": {
      "type": "object",
      "description": "Named volume definitions. Referenced by service `volumes` entries.",
      "patternProperties": {
        "^[a-zA-Z0-9_-]+$": {
          "type": ["object", "null"]
        }
      }
    },
    "networks": {
      "type": "object",
      "description": "Network definitions",
      "patternProperties": {
        "^[a-zA-Z0-9_-]+$": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "name": {
              "type": "string",
              "description": "Network name on the router"
            },
            "external": {
              "type": "boolean",
              "description": "Reference a pre-existing network rather than creating one"
            }
          }
        }
      }
    },
    "configs": {
      "type": "object",
      "description": "Config file content definitions. Each key is referenced by a service's `configs[].source`.",
      "patternProperties": {
        "^[a-zA-Z0-9_-]+$": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "content": {
              "type": "string",
              "description": "Inline file content injected into the container at deploy time"
            }
          }
        }
      }
    }
  }
}
