{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://schemas.revenexx.com/cockpit.schema.json",
  "title": "revenexx App Cockpit Extensions",
  "description": "Declarative UI extension point definitions for the Cockpit admin interface. Cockpit reads this file at runtime and dynamically composes the admin UI from all installed apps. Views are schema-driven — apps declare list / detail / form views with their columns, fields, filters, and actions; Cockpit renders them against the app's PostgREST-generated APIs without the app shipping any custom Vue code.",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "$schema": {
      "type": "string",
      "description": "Reference to this JSON Schema for editor validation and autocompletion."
    },
    "navigation": {
      "type": "array",
      "description": "Navigation items added to the Cockpit sidebar (under the studio that hosts this app — e.g. Commerce Studio for commerce-domain apps).",
      "items": { "$ref": "#/$defs/navigationItem" }
    },
    "views": {
      "type": "array",
      "description": "Page-level views with routes registered in Cockpit. Each view is one of the typed shapes below; Cockpit dispatches on `type` to the matching built-in renderer.",
      "items": { "$ref": "#/$defs/view" }
    },
    "widgets": {
      "type": "array",
      "description": "Dashboard widgets displayed on the Cockpit home screen.",
      "items": { "$ref": "#/$defs/widget" }
    },
    "action_buttons": {
      "type": "array",
      "description": "Buttons injected into other apps' entity views for cross-app actions.",
      "items": { "$ref": "#/$defs/actionButton" }
    }
  },
  "$defs": {
    "navigationItem": {
      "type": "object",
      "required": ["label", "icon", "route"],
      "additionalProperties": false,
      "properties": {
        "label": { "type": "string", "description": "Display label in the sidebar. Short (1-2 words)." },
        "icon": { "type": "string", "description": "Icon identifier from the Studio design system icon set." },
        "route": { "type": "string", "pattern": "^/", "description": "Route path registered in Cockpit (e.g. '/products'). Must start with '/'. Cockpit mounts these under the studio that hosts the app (typically `/commerce`)." },
        "position": { "type": "integer", "minimum": 0, "description": "Sort order in the sidebar. Lower numbers appear first." },
        "badge": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "type": { "type": "string", "enum": ["count", "dot"], "description": "'count' shows a number badge, 'dot' shows an indicator dot." },
            "source": { "type": "string", "description": "API endpoint that returns the badge value (e.g. '/api/orders?status=eq.pending&select=count')." }
          },
          "description": "Optional badge indicator on the navigation item."
        },
        "children": {
          "type": "array",
          "items": { "$ref": "#/$defs/navigationItem" },
          "description": "Nested sub-navigation items."
        }
      }
    },

    "view": {
      "oneOf": [
        { "$ref": "#/$defs/listView" },
        { "$ref": "#/$defs/detailView" },
        { "$ref": "#/$defs/formView" }
      ],
      "description": "A view declares its shape with `type` and Cockpit renders it via a built-in generic renderer."
    },

    "listView": {
      "type": "object",
      "required": ["route", "type", "entity", "columns"],
      "additionalProperties": false,
      "properties": {
        "route": { "type": "string", "pattern": "^/" },
        "type": { "const": "list" },
        "title": { "type": "string", "description": "Page title shown in the Cockpit header and browser tab." },
        "entity": { "type": "string", "description": "PostgREST entity path (typically the app's table name, e.g. 'products'). Cockpit fetches `/api/{entity}` with the column projection derived from `columns[]`." },
        "columns": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/column" } },
        "filters": { "type": "array", "items": { "$ref": "#/$defs/filter" }, "description": "Inline filters rendered above the table." },
        "sort": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "column": { "type": "string" },
            "direction": { "type": "string", "enum": ["asc", "desc"], "default": "asc" }
          },
          "description": "Default sort. The user can override per-column from the UI."
        },
        "page_size": { "type": "integer", "minimum": 1, "default": 25 },
        "row_action": {
          "type": "string",
          "description": "Where a row-click navigates to. Use ':id' as the placeholder, e.g. '/products/:id'. Omit to disable row-click navigation."
        },
        "actions": { "type": "array", "items": { "$ref": "#/$defs/action" }, "description": "Toolbar actions rendered in the page header (typically 'Create', 'Export')." },
        "row_actions": {
          "type": "array",
          "items": { "$ref": "#/$defs/action" },
          "description": "Per-row action buttons rendered in a trailing column (typically 'View' / 'Edit' / 'Delete'). Same action shape as toolbar actions; ':id' and any other row attribute can be substituted into `to` / `endpoint` paths."
        },
        "io": { "$ref": "#/$defs/viewIo" },
        "permissions": { "type": "array", "items": { "type": "string" } }
      }
    },

    "detailView": {
      "type": "object",
      "required": ["route", "type", "entity", "sections"],
      "additionalProperties": false,
      "properties": {
        "route": { "type": "string", "pattern": "^/", "description": "Use ':id' as the entity-id placeholder, e.g. '/products/:id'." },
        "type": { "const": "detail" },
        "title": { "type": "string" },
        "entity": { "type": "string" },
        "sections": {
          "type": "array",
          "minItems": 1,
          "items": {
            "type": "object",
            "required": ["title", "fields"],
            "additionalProperties": false,
            "properties": {
              "title": { "type": "string" },
              "description": { "type": "string" },
              "fields": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/field" } }
            }
          }
        },
        "actions": { "type": "array", "items": { "$ref": "#/$defs/action" } },
        "io": { "$ref": "#/$defs/viewIo" },
        "permissions": { "type": "array", "items": { "type": "string" } }
      }
    },

    "formView": {
      "type": "object",
      "required": ["route", "type", "entity", "fields"],
      "additionalProperties": false,
      "properties": {
        "route": { "type": "string", "pattern": "^/", "description": "For create-forms use a static route (e.g. '/products/new'). For edit-forms include ':id' (e.g. '/products/:id/edit')." },
        "type": { "const": "form" },
        "title": { "type": "string" },
        "entity": { "type": "string", "description": "PostgREST entity path. POST for create, PATCH (with ?id=eq.{id}) for edit." },
        "mode": { "type": "string", "enum": ["create", "edit"], "default": "create" },
        "fields": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/field" } },
        "submit_label": { "type": "string", "description": "Label for the submit button. Defaults to 'Save'." },
        "on_success": { "type": "string", "description": "Route to navigate to after a successful submit. ':id' is replaced with the created/updated id." },
        "permissions": { "type": "array", "items": { "type": "string" } }
      }
    },

    "viewIo": {
      "type": "object",
      "additionalProperties": false,
      "description": "Import / Export buttons wired to Baseline IO. Present + `enabled !== false` shows the button. List views support import + export; detail views support export.",
      "properties": {
        "export": { "$ref": "#/$defs/viewIoAction" },
        "import": { "$ref": "#/$defs/viewIoAction" }
      }
    },

    "viewIoAction": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "enabled": { "type": "boolean", "default": true },
        "label": { "type": "string" },
        "format": { "type": "string", "enum": ["csv", "xml", "json", "xlsx"], "description": "Source/target file format for an ad-hoc run." },
        "profile": { "type": "string", "description": "Saved IO profile name to run instead of an ad-hoc run." },
        "keys": { "type": "array", "items": { "type": "string" }, "description": "Import-only: natural-key columns for upsert matching (ad-hoc)." },
        "mode": { "type": "string", "enum": ["upsert", "full-sync", "append"], "description": "Import-only: apply mode for an ad-hoc run." }
      }
    },

    "column": {
      "type": "object",
      "required": ["name", "label"],
      "additionalProperties": false,
      "properties": {
        "name": { "type": "string", "description": "Attribute name on the entity (PostgREST column)." },
        "label": { "type": "string" },
        "type": {
          "type": "string",
          "enum": ["text", "number", "money", "boolean", "date", "datetime", "badge", "image", "relation"],
          "default": "text",
          "description": "Determines the cell renderer. 'badge' colours by value; 'money' formats per tenant currency; 'relation' resolves via the column 'relation' attribute."
        },
        "format": { "type": "string", "description": "Format string for date/number columns (e.g. 'YYYY-MM-DD', '0.00'). Cockpit interprets per `type`." },
        "relation": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "entity": { "type": "string", "description": "Target entity to resolve the relation against." },
            "label_field": { "type": "string", "description": "Field on the target to render (default 'name')." }
          },
          "description": "Required when `type` is 'relation'."
        },
        "width": { "type": "string", "description": "Optional CSS width (e.g. '120px', '20%')." },
        "sortable": { "type": "boolean", "default": true },
        "hidden": { "type": "boolean", "default": false, "description": "Render the column off-screen on small breakpoints." }
      }
    },

    "field": {
      "type": "object",
      "required": ["name", "type"],
      "additionalProperties": false,
      "properties": {
        "name": { "type": "string" },
        "label": { "type": "string" },
        "type": {
          "type": "string",
          "enum": ["text", "textarea", "number", "money", "boolean", "date", "datetime", "select", "multi-select", "relation", "json", "image", "email", "url", "tel"]
        },
        "required": { "type": "boolean", "default": false },
        "readonly": { "type": "boolean", "default": false, "description": "Surfaces the value but disables editing. Detail views default to readonly when omitted." },
        "placeholder": { "type": "string" },
        "description": { "type": "string", "description": "Helper text below the field." },
        "default": { "description": "Default value used for create-forms." },
        "options": {
          "type": "array",
          "items": {
            "oneOf": [
              { "type": "string" },
              {
                "type": "object",
                "required": ["value", "label"],
                "additionalProperties": false,
                "properties": {
                  "value": { "type": ["string", "number", "boolean"] },
                  "label": { "type": "string" }
                }
              }
            ]
          },
          "description": "Required for 'select' and 'multi-select'."
        },
        "relation": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "entity": { "type": "string" },
            "label_field": { "type": "string" },
            "value_field": { "type": "string", "default": "id" }
          },
          "description": "Required when `type` is 'relation'."
        },
        "validation": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "min": { "type": "number" },
            "max": { "type": "number" },
            "min_length": { "type": "integer", "minimum": 0 },
            "max_length": { "type": "integer", "minimum": 0 },
            "pattern": { "type": "string" }
          }
        }
      }
    },

    "filter": {
      "type": "object",
      "required": ["name", "label", "type"],
      "additionalProperties": false,
      "properties": {
        "name": { "type": "string", "description": "Form identifier and v-model key. When neither `column` nor `columns` is set, doubles as the column name (back-compat)." },
        "label": { "type": "string" },
        "type": { "type": "string", "enum": ["text", "select", "multi-select", "boolean", "date-range"] },
        "column": { "type": "string", "description": "Single Postgres column the filter targets. Overrides defaulting from `name`." },
        "columns": {
          "type": "array",
          "items": { "type": "string" },
          "minItems": 1,
          "description": "Multiple columns OR'd together for free-text search. Only meaningful for `type: text`. Takes precedence over `column`."
        },
        "options": {
          "type": "array",
          "items": {
            "oneOf": [
              { "type": "string" },
              {
                "type": "object",
                "required": ["value", "label"],
                "additionalProperties": false,
                "properties": {
                  "value": { "type": ["string", "number", "boolean"] },
                  "label": { "type": "string" }
                }
              }
            ]
          }
        }
      }
    },

    "action": {
      "type": "object",
      "required": ["label", "kind"],
      "additionalProperties": false,
      "properties": {
        "label": { "type": "string" },
        "icon": { "type": "string" },
        "kind": {
          "type": "string",
          "enum": ["navigate", "api", "link"],
          "description": "'navigate' moves to a Cockpit route. 'api' calls an entity endpoint with an optional confirm. 'link' opens an external URL."
        },
        "to": { "type": "string", "description": "Route path for 'navigate' (':id' placeholder substituted from context)." },
        "endpoint": { "type": "string", "description": "Endpoint for 'api' (e.g. '/api/products?id=eq.:id'). ':id' is substituted from context." },
        "method": { "type": "string", "enum": ["POST", "PATCH", "DELETE", "PUT"], "default": "POST" },
        "url": { "type": "string", "description": "External URL for 'link'." },
        "variant": { "type": "string", "enum": ["primary", "secondary", "danger", "ghost"], "default": "secondary" },
        "confirm": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "title": { "type": "string" },
            "message": { "type": "string" }
          }
        }
      }
    },

    "widget": {
      "type": "object",
      "required": ["id"],
      "additionalProperties": false,
      "description": "Dashboard widget. The Cockpit ships built-in renderers selected by `component` (e.g. 'EntityCount', 'EntityList'). When the widget reads data, declare the PostgREST `entity` and any `filters`/`columns`/`limit` it needs. Apps that want the renderer to navigate somewhere on click set `link_to` to a cockpit route.",
      "properties": {
        "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
        "component": { "type": "string", "description": "Built-in widget renderer key: 'EntityCount' (numeric KPI), 'EntityList' (top-N rows). Unknown values render a placeholder so app authors see the typo without the dashboard going blank." },
        "title": { "type": "string" },
        "size": { "type": "string", "enum": ["small", "medium", "large", "full"], "default": "medium" },
        "position": { "type": "integer", "minimum": 0 },
        "refresh_interval": { "type": "integer", "minimum": 5, "description": "Auto-refresh interval in seconds. Minimum 5." },
        "entity": { "type": "string", "description": "PostgREST entity path the widget queries (bare name; cockpit namespaces to {vendor}__{app}__{entity})." },
        "filters": { "type": "object", "additionalProperties": true, "description": "PostgREST-style filters merged into the query string. Values may be bare (eq-shorthand) or operator-prefixed (`ilike.*foo*`)." },
        "limit": { "type": "integer", "minimum": 1, "description": "EntityList only — row count to fetch. Default 5." },
        "columns": { "type": "array", "items": { "$ref": "#/$defs/column" }, "description": "EntityList only — column projection. Same shape as a list view's columns." },
        "link_to": { "type": "string", "pattern": "^/", "description": "Cockpit route the widget header / view-all navigates to. Same studio-relative path semantics as navigation items." }
      }
    },

    "actionButton": {
      "type": "object",
      "required": ["label", "target_entity", "kind"],
      "additionalProperties": false,
      "description": "Cross-app action injected into another App's detail / list views. App A's manifest can declare action_buttons that target App B's entities — when Cockpit renders App B's DetailView for an entity whose name matches `target_entity`, A's buttons appear in the toolbar alongside B's own actions. Same `kind` / `to` / `endpoint` / `url` shape as in-view actions; the row's id is exposed for `:id` substitution.",
      "properties": {
        "label": { "type": "string" },
        "icon": { "type": "string" },
        "target_entity": { "type": "string", "description": "Bare entity name whose views host this button (e.g. 'products', 'orders'). Matched against the qualified entity stripped of `{vendor}__{app}__` prefix." },
        "kind": { "type": "string", "enum": ["navigate", "api", "link"] },
        "to": { "type": "string", "description": "Cockpit route for `navigate`. ':id' substituted from the target row." },
        "endpoint": { "type": "string", "description": "PostgREST / API endpoint for `kind: api`. ':id' substituted." },
        "method": { "type": "string", "enum": ["POST", "PATCH", "DELETE", "PUT"], "default": "POST" },
        "url": { "type": "string", "description": "External URL for `kind: link`. Opens in a new tab." },
        "variant": { "type": "string", "enum": ["primary", "secondary", "danger", "ghost"], "default": "secondary" },
        "confirm": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "title": { "type": "string" },
            "message": { "type": "string" }
          }
        }
      }
    }
  }
}
